Skip to content

Commit

Permalink
feat: support cookie based refresh tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielRivers committed Jan 16, 2025
1 parent 87cbd4f commit 9b67701
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 65 deletions.
4 changes: 0 additions & 4 deletions lib/utils/checkAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@ export const checkAuth = async ({
}): Promise<RefreshTokenResult> => {
const usingCustomDomain = isCustomDomain(domain);
const forceLocalStorage = storageSettings.useInsecureForRefreshToken;
console.log("usingCustomDomain", usingCustomDomain);
console.log("forceLocalStorage", forceLocalStorage);
let kbrteCookie = null;
if (usingCustomDomain && !forceLocalStorage) {
console.log("getting cookie");
kbrteCookie = getCookie(kindeCookieName);
console.log("kbrteCookie", kbrteCookie);
}

return await refreshToken({
Expand Down
184 changes: 179 additions & 5 deletions lib/utils/exchangeAuthCode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,24 @@ import * as main from "../main";
const fetchMock = createFetchMock(vi);

describe("exchangeAuthCode", () => {
const mockStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
getSessionItem: vi.fn(),
setSessionItem: vi.fn(),
removeSessionItem: vi.fn(),
destroySession: vi.fn(),
setItems: vi.fn(),
};

beforeEach(() => {
fetchMock.enableMocks();
vi.spyOn(refreshTokenTimer, "setRefreshTimer");
vi.spyOn(main, "refreshToken");
vi.useFakeTimers();
main.storageSettings.useInsecureForRefreshToken = false;
main.clearInsecureStorage();
});

afterEach(() => {
Expand Down Expand Up @@ -153,10 +165,6 @@ describe("exchangeAuthCode", () => {
expect((options?.headers as Headers).get("Content-type")).toEqual(
"application/x-www-form-urlencoded; charset=UTF-8",
);
expect((options?.headers as Headers).get("Cache-Control")).toEqual(
"no-store",
);
expect((options?.headers as Headers).get("Pragma")).toEqual("no-cache");
});

it("uses insecure storage for code verifier when storage setting applies", async () => {
Expand Down Expand Up @@ -187,7 +195,6 @@ describe("exchangeAuthCode", () => {

main.storageSettings.useInsecureForRefreshToken = true;

console.log('here');
const result = await exchangeAuthCode({
urlParams,
domain: "http://test.kinde.com",
Expand Down Expand Up @@ -336,4 +343,171 @@ describe("exchangeAuthCode", () => {
vi.advanceTimersByTime(3600 * 1000);
expect(main.refreshToken).toHaveBeenCalledTimes(1);
});

it("should return error if state or code is missing", async () => {
const urlParams = new URLSearchParams();
const result = await exchangeAuthCode({
urlParams,
domain: "test.com",
clientId: "test",
redirectURL: "test.com",
});

expect(result).toEqual({
success: false,
error: "Invalid state or code",
});
});

it("should return error if storage is not available", async () => {
const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");

const result = await exchangeAuthCode({
urlParams,
domain: "test.com",
clientId: "test",
redirectURL: "test.com",
});

expect(result).toEqual({
success: false,
error: "Invalid state; supplied test, expected null",
});
});

it("should return error if state is invalid", async () => {
const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");
mockStorage.getItem.mockReturnValue("different-state");

const result = await exchangeAuthCode({
urlParams,
domain: "test.com",
clientId: "test",
redirectURL: "test.com",
});

expect(result).toEqual({
success: false,
error: "Invalid state; supplied test, expected null",
});
});

it("should return error if code verifier is missing", async () => {
const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");
mockStorage.getItem.mockImplementation((key) => {
if (key === StorageKeys.state) return "test";
return null;
});

const result = await exchangeAuthCode({
urlParams,
domain: "test.com",
clientId: "test",
redirectURL: "test.com",
});

expect(result).toEqual({
success: false,
error: "Invalid state; supplied test, expected null",
});
});

it("should return error if fetch fails", async () => {
const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");
mockStorage.getItem.mockImplementation((key) => {
if (key === StorageKeys.state) return "test";
if (key === StorageKeys.codeVerifier) return "verifier";
return null;
});
fetchMock.mockRejectOnce(new Error("Fetch failed"));

const result = await exchangeAuthCode({
urlParams,
domain: "test.com",
clientId: "test",
redirectURL: "test.com",
});

expect(result).toEqual({
success: false,
error: "Invalid state; supplied test, expected null",
});
});

it("should return error if token response is invalid", async () => {
const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");
mockStorage.getItem.mockImplementation((key) => {
if (key === StorageKeys.state) return "test";
if (key === StorageKeys.codeVerifier) return "verifier";
return null;
});
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({}),
} as Response);

const result = await exchangeAuthCode({
urlParams,
domain: "test.com",
clientId: "test",
redirectURL: "test.com",
});

expect(result).toEqual({
success: false,
error: "Invalid state; supplied test, expected null",
});
});

it("should handle auto refresh correctly", async () => {
const store = new MemoryStorage();

setActiveStorage(store);
await store.setItems({
[StorageKeys.state]: "test",
});
vi.spyOn(store, "setSessionItem");
const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");
mockStorage.getItem.mockImplementation((key) => {
if (key === StorageKeys.state) return "test";
if (key === StorageKeys.codeVerifier) return "verifier";
return null;
});

vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
access_token: "access",
id_token: "id",
refresh_token: "refresh",
}),
} as Response);

const result = await exchangeAuthCode({
urlParams,
domain: "test.com",
clientId: "test",
redirectURL: "test.com",
autoRefresh: true,
});

expect(result.success).toBe(true);
expect(store.setSessionItem).toHaveBeenCalledWith(
StorageKeys.refreshToken,
"refresh",
);
});
});
12 changes: 3 additions & 9 deletions lib/utils/exchangeAuthCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
StorageKeys,
storageSettings,
} from "../main";
import { isCustomDomain } from ".";
import { clearRefreshTimer, setRefreshTimer } from "./refreshTimer";

export const frameworkSettings: {
Expand Down Expand Up @@ -66,7 +67,6 @@ export const exchangeAuthCode = async ({
}

const storedState = await activeStorage.getSessionItem(StorageKeys.state);
console.log('----', state, storedState);
if (state !== storedState) {
console.error("Invalid state");
return {
Expand All @@ -81,24 +81,18 @@ export const exchangeAuthCode = async ({

const headers: {
"Content-type": string;
"Cache-Control": string;
Pragma: string;
"Kinde-SDK"?: string;
} = {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cache-Control": "no-store",
Pragma: "no-cache",
};

if (frameworkSettings.framework) {
headers["Kinde-SDK"] =
`${frameworkSettings.framework}/${frameworkSettings.frameworkVersion}`;
}

const response = await fetch(`${domain}/oauth2/token`, {
method: "POST",
// ...(isUseCookie && {credentials: 'include'}),
// credentials: "include",
...(isCustomDomain(domain) && { credentials: "include" }),
headers: new Headers(headers),
body: new URLSearchParams({
client_id: clientId,
Expand Down Expand Up @@ -165,4 +159,4 @@ export const exchangeAuthCode = async ({
[StorageKeys.idToken]: data.id_token,
[StorageKeys.refreshToken]: data.refresh_token,
};
};
};
5 changes: 4 additions & 1 deletion lib/utils/token/isAuthenticated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ describe("isAuthenticated", () => {
});

expect(result).toBe(true);
expect(mockRefreshToken).toHaveBeenCalledWith({domain: "test.com", clientId: "123"});
expect(mockRefreshToken).toHaveBeenCalledWith({
domain: "test.com",
clientId: "123",
});
});

it("should return false if token refresh fails", async () => {
Expand Down
11 changes: 8 additions & 3 deletions lib/utils/token/refreshToken.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { MemoryStorage, StorageKeys, storageSettings } from "../../sessionManager";
import {
MemoryStorage,
StorageKeys,
storageSettings,
} from "../../sessionManager";
import * as tokenUtils from ".";

describe("refreshToken", () => {
const mockDomain = "https://example.com";
const mockKindeDomain = "https://example.kinde.com";
const mockClientId = "test-client-id";
const mockRefreshTokenValue = "mock-refresh-token";
const memoryStorage = new MemoryStorage();
Expand Down Expand Up @@ -136,7 +141,7 @@ describe("refreshToken", () => {
} as Response);

const result = await tokenUtils.refreshToken({
domain: mockDomain,
domain: mockKindeDomain,
clientId: mockClientId,
});

Expand Down Expand Up @@ -180,7 +185,7 @@ describe("refreshToken", () => {
);
});

it('should use insecure storage for refresh token if useInsecureForRefreshToken is true', async () => {
it("should use insecure storage for refresh token if useInsecureForRefreshToken is true", async () => {
const mockResponse = {
access_token: "new-access-token",
id_token: "new-id-token",
Expand Down
Loading

0 comments on commit 9b67701

Please sign in to comment.