Skip to content

Commit ea43219

Browse files
committed
feat: refresh fixes
1 parent a366af2 commit ea43219

15 files changed

+476
-50
lines changed

lib/main.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe("index exports", () => {
4343
"exchangeAuthCode",
4444
"isAuthenticated",
4545
"refreshToken",
46+
"checkAuth",
4647
"isCustomDomain",
4748

4849
// session manager

lib/sessionManager/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ export const storageSettings: StorageSettingsType = {
1111
* If the length is exceeded the items will be split into multiple storage items.
1212
*/
1313
maxLength: 2000,
14+
15+
/**
16+
* Use insecure storage for refresh token.
17+
*
18+
* Warning: This should only be used when you're not using a custom domain and no backend app to authenticate on.
19+
*/
20+
useInsecureForRefreshToken: false,
1421
};
1522

1623
export { MemoryStorage } from "./stores/memory.js";

lib/sessionManager/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export enum StorageKeys {
1717
export type StorageSettingsType = {
1818
keyPrefix: string;
1919
maxLength: number;
20+
useInsecureForRefreshToken: boolean;
2021
};
2122

2223
export abstract class SessionBase<V extends string = StorageKeys>

lib/utils/checkAuth.test.ts

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import * as CheckAuth from "./checkAuth";
3+
import * as RefreshToken from "./token/refreshToken";
4+
import * as GetCookie from "./getCookie";
5+
6+
describe("checkAuth", () => {
7+
const domain = "auth.test.com";
8+
const clientId = "client-id";
9+
10+
beforeEach(() => {
11+
vi.clearAllMocks();
12+
});
13+
14+
it("should use cookie refresh type when using not custom domain and no _kbrte cookie", async () => {
15+
vi.spyOn(RefreshToken, "refreshToken").mockResolvedValue({
16+
success: true,
17+
});
18+
vi.spyOn(GetCookie, "getCookie").mockResolvedValue("value");
19+
20+
await CheckAuth.checkAuth({ domain: "test.kinde.com", clientId });
21+
22+
expect(GetCookie.getCookie).not.toHaveBeenCalled();
23+
expect(RefreshToken.refreshToken).toHaveBeenCalledWith({
24+
domain: "test.kinde.com",
25+
clientId,
26+
refreshType: RefreshToken.RefreshType.refreshToken,
27+
});
28+
});
29+
30+
it("should use cookie refresh type when using custom domain and _kbrte cookie exists", async () => {
31+
vi.spyOn(RefreshToken, "refreshToken").mockResolvedValue({
32+
success: true,
33+
});
34+
vi.spyOn(GetCookie, "getCookie").mockReturnValue("value");
35+
36+
await CheckAuth.checkAuth({ domain, clientId });
37+
38+
expect(GetCookie.getCookie).toHaveBeenCalledWith("_kbrte");
39+
40+
expect(RefreshToken.refreshToken).toHaveBeenCalledWith({
41+
domain,
42+
clientId,
43+
refreshType: RefreshToken.RefreshType.cookie,
44+
});
45+
});
46+
47+
it("should use cookie refresh type when using custom domain and no _kbrte cookie", async () => {
48+
vi.spyOn(RefreshToken, "refreshToken").mockResolvedValue({
49+
success: true,
50+
});
51+
vi.spyOn(GetCookie, "getCookie").mockReturnValue(null);
52+
53+
await CheckAuth.checkAuth({ domain, clientId });
54+
55+
expect(GetCookie.getCookie).toHaveBeenCalledWith("_kbrte");
56+
57+
expect(RefreshToken.refreshToken).toHaveBeenCalledWith({
58+
domain,
59+
clientId,
60+
refreshType: RefreshToken.RefreshType.refreshToken,
61+
});
62+
});
63+
64+
// it.only('should use cookie refresh type when using custom domain and no _kbrte cookie', async () => {
65+
// vi.spyOn(refreshToken, "refreshToken").mockResolvedValue({
66+
// success: true,
67+
// });
68+
69+
// const result = await checkAuth({ domain: "test.kinde.com", clientId });
70+
71+
// expect(refreshToken.refreshToken).toHaveBeenCalledWith({
72+
// domain: "test.kinde.com",
73+
// clientId,
74+
// refreshType: refreshToken.RefreshType.refreshToken,
75+
// });
76+
// expect(result).toEqual({});
77+
// });
78+
79+
// it('should use refresh token type when not using custom domain', async () => {
80+
// (refreshToken as vi.Mock).mockResolvedValue({} as RefreshTokenResult);
81+
82+
// const result = await checkAuth({ domain: 'test.kinde.com', clientId });
83+
84+
// expect(refreshToken).toHaveBeenCalledWith({
85+
// domain: 'not-custom.com',
86+
// clientId,
87+
// refreshType: RefreshType.refreshToken,
88+
// });
89+
// expect(result).toEqual({});
90+
// });
91+
92+
// it('should use refresh token type when forceLocalStorage is true', async () => {
93+
// (refreshToken as vi.Mock).mockResolvedValue({} as RefreshTokenResult);
94+
95+
// // Mock storageSettings to force local storage
96+
// const originalStorageSettings = storageSettings;
97+
// storageSettings.useInsecureForRefreshToken = true;
98+
99+
// const result = await checkAuth({ domain, clientId });
100+
101+
// expect(refreshToken).toHaveBeenCalledWith({
102+
// domain,
103+
// clientId,
104+
// refreshType: RefreshType.refreshToken,
105+
// });
106+
// expect(result).toEqual({});
107+
108+
// // Restore original storageSettings
109+
// storageSettings.useInsecureForRefreshToken = originalStorageSettings.useInsecureForRefreshToken;
110+
// });
111+
});

lib/utils/checkAuth.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { isCustomDomain, refreshToken, storageSettings } from "../main";
2+
import { getCookie } from "./getCookie";
3+
import { RefreshType, RefreshTokenResult } from "./token/refreshToken";
4+
5+
const kindeCookieName = "_kbrte";
6+
7+
export const checkAuth = async ({
8+
domain,
9+
clientId,
10+
}: {
11+
domain: string;
12+
clientId: string;
13+
}): Promise<RefreshTokenResult> => {
14+
const usingCustomDomain = isCustomDomain(domain);
15+
const forceLocalStorage = storageSettings.useInsecureForRefreshToken;
16+
console.log("usingCustomDomain", usingCustomDomain);
17+
console.log("forceLocalStorage", forceLocalStorage);
18+
let kbrteCookie = null;
19+
if (usingCustomDomain && !forceLocalStorage) {
20+
console.log("getting cookie");
21+
kbrteCookie = getCookie(kindeCookieName);
22+
console.log("kbrteCookie", kbrteCookie);
23+
}
24+
25+
return await refreshToken({
26+
domain,
27+
clientId,
28+
refreshType: kbrteCookie ? RefreshType.cookie : RefreshType.refreshToken,
29+
});
30+
};

lib/utils/exchangeAuthCode.test.ts

+54
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe("exchangeAuthCode", () => {
1515
vi.spyOn(refreshTokenTimer, "setRefreshTimer");
1616
vi.spyOn(main, "refreshToken");
1717
vi.useFakeTimers();
18+
main.storageSettings.useInsecureForRefreshToken = false;
1819
});
1920

2021
afterEach(() => {
@@ -158,6 +159,59 @@ describe("exchangeAuthCode", () => {
158159
expect((options?.headers as Headers).get("Pragma")).toEqual("no-cache");
159160
});
160161

162+
it("uses insecure storage for code verifier when storage setting applies", async () => {
163+
const store = new MemoryStorage();
164+
main.setInsecureStorage(store);
165+
166+
const store2 = new MemoryStorage();
167+
const state = "state";
168+
169+
await store.setItems({
170+
[StorageKeys.state]: state,
171+
});
172+
173+
const input = "hello";
174+
175+
const urlParams = new URLSearchParams();
176+
urlParams.append("code", input);
177+
urlParams.append("state", state);
178+
urlParams.append("client_id", "test");
179+
180+
fetchMock.mockResponseOnce(
181+
JSON.stringify({
182+
access_token: "access_token",
183+
refresh_token: "refresh_token",
184+
id_token: "id_token",
185+
}),
186+
);
187+
188+
main.storageSettings.useInsecureForRefreshToken = true;
189+
190+
console.log('here');
191+
const result = await exchangeAuthCode({
192+
urlParams,
193+
domain: "http://test.kinde.com",
194+
clientId: "test",
195+
redirectURL: "http://test.kinde.com",
196+
});
197+
198+
expect(result).toStrictEqual({
199+
accessToken: "access_token",
200+
refreshToken: "refresh_token",
201+
idToken: "id_token",
202+
success: true,
203+
});
204+
205+
const postCodeVerifier = await store.getSessionItem(
206+
StorageKeys.codeVerifier,
207+
);
208+
expect(postCodeVerifier).toBeNull();
209+
const insecureRefreshToken = await store2.getSessionItem(
210+
StorageKeys.refreshToken,
211+
);
212+
expect(insecureRefreshToken).toBeNull();
213+
});
214+
161215
it("set the framework and version on header", async () => {
162216
const store = new MemoryStorage();
163217
setActiveStorage(store);

lib/utils/exchangeAuthCode.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getInsecureStorage,
44
refreshToken,
55
StorageKeys,
6+
storageSettings,
67
} from "../main";
78
import { clearRefreshTimer, setRefreshTimer } from "./refreshTimer";
89

@@ -65,6 +66,7 @@ export const exchangeAuthCode = async ({
6566
}
6667

6768
const storedState = await activeStorage.getSessionItem(StorageKeys.state);
69+
console.log('----', state, storedState);
6870
if (state !== storedState) {
6971
console.error("Invalid state");
7072
return {
@@ -124,15 +126,21 @@ export const exchangeAuthCode = async ({
124126
} = await response.json();
125127

126128
const secureStore = getActiveStorage();
127-
secureStore!.setItems({
128-
[StorageKeys.accessToken]: data.access_token,
129-
[StorageKeys.idToken]: data.id_token,
130-
[StorageKeys.refreshToken]: data.refresh_token,
131-
});
129+
if (secureStore) {
130+
secureStore.setItems({
131+
[StorageKeys.accessToken]: data.access_token,
132+
[StorageKeys.idToken]: data.id_token,
133+
[StorageKeys.refreshToken]: data.refresh_token,
134+
});
135+
}
136+
137+
if (storageSettings.useInsecureForRefreshToken) {
138+
activeStorage.setSessionItem(StorageKeys.refreshToken, data.refresh_token);
139+
}
132140

133141
if (autoRefresh) {
134142
setRefreshTimer(data.expires_in, async () => {
135-
refreshToken(domain, clientId);
143+
refreshToken({ domain, clientId });
136144
});
137145
}
138146

@@ -157,4 +165,4 @@ export const exchangeAuthCode = async ({
157165
[StorageKeys.idToken]: data.id_token,
158166
[StorageKeys.refreshToken]: data.refresh_token,
159167
};
160-
};
168+
};

lib/utils/getCookie.test.ts

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import { getCookie } from "./getCookie";
3+
4+
describe("getCookie", () => {
5+
let cookieStore: { [key: string]: string } = {};
6+
7+
beforeEach(() => {
8+
// Reset the cookie store before each test
9+
cookieStore = {};
10+
11+
// Mock document.cookie
12+
Object.defineProperty(document, "cookie", {
13+
get: () => {
14+
return Object.entries(cookieStore)
15+
.map(([key, value]) => `${key}=${value}`)
16+
.join("; ");
17+
},
18+
set: (cookie) => {
19+
const [keyValue] = cookie.split(";");
20+
const [key, value] = keyValue.split("=");
21+
cookieStore[key.trim()] = value;
22+
},
23+
configurable: true,
24+
});
25+
});
26+
27+
it("should return the value of the cookie if it exists", () => {
28+
document.cookie = "_kbrte=cookie-value;path=/";
29+
const result = getCookie("_kbrte");
30+
expect(result).toBe("cookie-value");
31+
});
32+
33+
it("should return null if the cookie does not exist", () => {
34+
const result = getCookie("_kbrte");
35+
expect(result).toBeNull();
36+
});
37+
38+
it("should handle multiple cookies and return the correct one", () => {
39+
document.cookie = "cookie1=value1;path=/";
40+
document.cookie = "_kbrte=cookie-value;path=/";
41+
document.cookie = "cookie2=value2;path=/";
42+
const result = getCookie("_kbrte");
43+
expect(result).toBe("cookie-value");
44+
});
45+
46+
it("should return null if the cookie name is a substring of another cookie name", () => {
47+
document.cookie = "not_kbrte=value;path=/";
48+
const result = getCookie("_kbrte");
49+
expect(result).toBeNull();
50+
});
51+
52+
it("should return null if the cookie name is a substring of another cookie name with space", () => {
53+
document.cookie = "not _kbrte=value;path=/";
54+
const result = getCookie("_kbrte");
55+
expect(result).toBeNull();
56+
});
57+
58+
it("should return null if the cookie value is empty", () => {
59+
document.cookie = "_kbrte=;path=/";
60+
const result = getCookie("_kbrte");
61+
expect(result).toBeNull();
62+
});
63+
64+
it("should return null if the cookie value is undefined", () => {
65+
document.cookie = "_kbrte=undefined;path=/";
66+
const result = getCookie("_kbrte");
67+
expect(result).toBe("undefined");
68+
});
69+
70+
it("should return null if the cookie value is null", () => {
71+
document.cookie = "_kbrte=null;path=/";
72+
const result = getCookie("_kbrte");
73+
expect(result).toBe("null");
74+
});
75+
76+
it("should return null if parts.length is not equal to 2", () => {
77+
document.cookie = "cookie1=value1;path=/";
78+
const result = getCookie("_kbrte");
79+
expect(result).toBeNull();
80+
});
81+
82+
it("should return null if parts.pop() returns undefined", () => {
83+
document.cookie = "_kbrte=;path=/";
84+
const result = getCookie("_kbrte");
85+
expect(result).toBeNull();
86+
});
87+
});

lib/utils/getCookie.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function getCookie(name: string): string | null {
2+
const value = `; ${document.cookie}`;
3+
console.log("value", value);
4+
const parts = value.split(`; ${name}=`);
5+
console.log("parts.length", parts);
6+
if (parts.length === 2) return parts.pop()?.split(";").shift() || null;
7+
return null;
8+
}

0 commit comments

Comments
 (0)