Skip to content

Commit 01c2c95

Browse files
authored
Merge pull request #69 from kinde-oss/fix/refresh_insecure_token_storage
fix: insecure token storage when using no non custom domain
2 parents 8fe5d9b + c889b9c commit 01c2c95

11 files changed

+239
-21
lines changed

CHANGELOG.md

+54
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,60 @@
11
# Changelog
22

33

4+
## 0.7.1...fix/refresh_insecure_token_storage
5+
6+
[compare changes](https://github.com/kinde-oss/js-utils/compare/0.7.1...fix/refresh_insecure_token_storage)
7+
8+
### 🩹 Fixes
9+
10+
- Insecure token storage when using no non custom domain ([7338321](https://github.com/kinde-oss/js-utils/commit/7338321))
11+
- Only show production warning when explicity setting the 'useInsecureForRefreshToken' ([958f4ed](https://github.com/kinde-oss/js-utils/commit/958f4ed))
12+
- Non prod kinde domains ([24be712](https://github.com/kinde-oss/js-utils/commit/24be712))
13+
14+
### ✅ Tests
15+
16+
- Add tests ([979974d](https://github.com/kinde-oss/js-utils/commit/979974d))
17+
18+
### ❤️ Contributors
19+
20+
- Daniel Rivers ([@DanielRivers](http://github.com/DanielRivers))
21+
22+
## 0.7.1...fix/refresh_insecure_token_storage
23+
24+
[compare changes](https://github.com/kinde-oss/js-utils/compare/0.7.1...fix/refresh_insecure_token_storage)
25+
26+
### 🩹 Fixes
27+
28+
- Insecure token storage when using no non custom domain ([7338321](https://github.com/kinde-oss/js-utils/commit/7338321))
29+
- Only show production warning when explicity setting the 'useInsecureForRefreshToken' ([958f4ed](https://github.com/kinde-oss/js-utils/commit/958f4ed))
30+
- Non prod kinde domains ([24be712](https://github.com/kinde-oss/js-utils/commit/24be712))
31+
32+
### ✅ Tests
33+
34+
- Add tests ([979974d](https://github.com/kinde-oss/js-utils/commit/979974d))
35+
36+
### ❤️ Contributors
37+
38+
- Daniel Rivers ([@DanielRivers](http://github.com/DanielRivers))
39+
40+
## 0.7.1...fix/refresh_insecure_token_storage
41+
42+
[compare changes](https://github.com/kinde-oss/js-utils/compare/0.7.1...fix/refresh_insecure_token_storage)
43+
44+
### 🩹 Fixes
45+
46+
- Insecure token storage when using no non custom domain ([7338321](https://github.com/kinde-oss/js-utils/commit/7338321))
47+
- Only show production warning when explicity setting the 'useInsecureForRefreshToken' ([958f4ed](https://github.com/kinde-oss/js-utils/commit/958f4ed))
48+
- Non prod kinde domains ([24be712](https://github.com/kinde-oss/js-utils/commit/24be712))
49+
50+
### ✅ Tests
51+
52+
- Add tests ([979974d](https://github.com/kinde-oss/js-utils/commit/979974d))
53+
54+
### ❤️ Contributors
55+
56+
- Daniel Rivers ([@DanielRivers](http://github.com/DanielRivers))
57+
458
## 0.7.0...main
559

660
[compare changes](https://github.com/kinde-oss/js-utils/compare/0.7.0...main)

lib/sessionManager/stores/localStorage.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect, beforeEach, vi } from "vitest";
22
import { LocalStorage } from "./localStorage";
33
import { StorageKeys } from "../types";
4+
import { storageSettings } from "..";
45

56
enum ExtraKeys {
67
testKey = "testKey2",
@@ -33,6 +34,22 @@ describe("LocalStorage standard keys", () => {
3334
sessionManager = new LocalStorage();
3435
});
3536

37+
it("should show warning when using local storage access token explicity", async () => {
38+
storageSettings.useInsecureForRefreshToken = true;
39+
const consoleSpy = vi.spyOn(console, "warn");
40+
new LocalStorage();
41+
expect(consoleSpy).toHaveBeenCalledWith(
42+
"LocalStorage store should not be used in production",
43+
);
44+
storageSettings.useInsecureForRefreshToken = false;
45+
});
46+
47+
it("should not show warning when using secure refresh tokens", async () => {
48+
const consoleSpy = vi.spyOn(console, "warn");
49+
new LocalStorage();
50+
expect(consoleSpy).not.toHaveBeenCalled();
51+
});
52+
3653
it("should set and get an item in session storage", async () => {
3754
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
3855
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(

lib/sessionManager/stores/localStorage.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export class LocalStorage<V extends string = StorageKeys>
1212
{
1313
constructor() {
1414
super();
15-
console.warn("LocalStorage store should not be used in production");
15+
if (storageSettings.useInsecureForRefreshToken) {
16+
console.warn("LocalStorage store should not be used in production");
17+
}
1618
}
1719

1820
private internalItems: Set<V | StorageKeys> = new Set<V>();

lib/utils/exchangeAuthCode.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,25 @@ interface ExchangeAuthCodeParams {
2626
autoRefresh?: boolean;
2727
}
2828

29-
interface ExchangeAuthCodeResult {
30-
success: boolean;
31-
error?: string;
29+
type ExchangeAuthCodeResultSuccess = {
30+
success: true;
31+
error?: never;
3232
[StorageKeys.accessToken]?: string;
3333
[StorageKeys.idToken]?: string;
3434
[StorageKeys.refreshToken]?: string;
35-
}
35+
};
36+
37+
type ExchangeAuthCodeResultError = {
38+
success: false;
39+
error: string;
40+
[StorageKeys.accessToken]?: never;
41+
[StorageKeys.idToken]?: never;
42+
[StorageKeys.refreshToken]?: never;
43+
};
44+
45+
type ExchangeAuthCodeResult =
46+
| ExchangeAuthCodeResultSuccess
47+
| ExchangeAuthCodeResultError;
3648

3749
export const exchangeAuthCode = async ({
3850
urlParams,
@@ -99,7 +111,7 @@ export const exchangeAuthCode = async ({
99111
headers["Kinde-SDK"] =
100112
`${frameworkSettings.framework}/${frameworkSettings.sdkVersion}/${frameworkSettings.frameworkVersion}/Javascript`;
101113
}
102-
const response = await fetch(`${domain}/oauth2/token`, {
114+
const fetchOptions: RequestInit = {
103115
method: "POST",
104116
...(isCustomDomain(domain) && { credentials: "include" }),
105117
headers: new Headers(headers),
@@ -110,7 +122,9 @@ export const exchangeAuthCode = async ({
110122
grant_type: "authorization_code",
111123
redirect_uri: redirectURL,
112124
}),
113-
});
125+
};
126+
127+
const response = await fetch(`${domain}/oauth2/token`, fetchOptions);
114128
if (!response?.ok) {
115129
const errorText = await response.text();
116130
console.error("Token exchange failed:", response.status, errorText);
@@ -137,7 +151,7 @@ export const exchangeAuthCode = async ({
137151
});
138152
}
139153

140-
if (storageSettings.useInsecureForRefreshToken) {
154+
if (storageSettings.useInsecureForRefreshToken || !isCustomDomain(domain)) {
141155
activeStorage.setSessionItem(StorageKeys.refreshToken, data.refresh_token);
142156
}
143157

lib/utils/generateAuthUrl.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,24 @@ describe("generateAuthUrl", () => {
177177
expect(nonce).toBeDefined();
178178
expect(codeVerifier).toBeDefined();
179179
});
180+
181+
it("if state is defined, ensure its stored in correctly", async () => {
182+
const store = new MemoryStorage();
183+
setActiveStorage(store);
184+
185+
const testState = "testState:123";
186+
const domain = "https://auth.example.com";
187+
const options: LoginOptions = {
188+
clientId: "client123",
189+
redirectURL: "https://example.com",
190+
prompt: PromptTypes.login,
191+
state: testState,
192+
};
193+
194+
await generateAuthUrl(domain, IssuerRouteTypes.login, options);
195+
196+
const state = await store.getSessionItem(StorageKeys.state);
197+
198+
expect(state).toEqual(testState);
199+
});
180200
});

lib/utils/generateAuthUrl.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export const generateAuthUrl = async (
3030

3131
if (!options.state) {
3232
options.state = generateRandomString(32);
33-
if (activeStorage) {
34-
activeStorage.setSessionItem(StorageKeys.state, options.state);
35-
}
33+
}
34+
if (activeStorage) {
35+
activeStorage.setSessionItem(StorageKeys.state, options.state);
3636
}
3737
searchParams["state"] = options.state;
3838

lib/utils/isCustomDomain.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ describe("isCustomDomain", () => {
1414
const result = isCustomDomain("test.kinde.com");
1515
expect(result).toEqual(false);
1616
});
17+
it("works on no prod kinde domains", () => {
18+
const result = isCustomDomain("https://test-test.au.kinde.com");
19+
expect(result).toEqual(false);
20+
});
1721
});

lib/utils/isCustomDomain.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export const isCustomDomain = (domain: string): boolean => {
22
return !domain.match(
3-
/^(?:https?:\/\/)?[a-zA-Z0-9][-a-zA-Z0-9]*\.kinde\.com$/i,
3+
/^(?:https?:\/\/)?[a-zA-Z0-9][.-a-zA-Z0-9]*\.kinde\.com$/i,
44
);
55
};

lib/utils/token/refreshToken.test.ts

+99-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
storageSettings,
66
} from "../../sessionManager";
77
import * as tokenUtils from ".";
8+
import * as refreshTimer from "../refreshTimer";
89

910
describe("refreshToken", () => {
1011
const mockDomain = "https://example.com";
@@ -17,6 +18,8 @@ describe("refreshToken", () => {
1718
vi.resetAllMocks();
1819
vi.spyOn(tokenUtils, "getDecodedToken").mockResolvedValue(null);
1920
vi.spyOn(memoryStorage, "setSessionItem");
21+
vi.spyOn(refreshTimer, "setRefreshTimer");
22+
vi.spyOn(tokenUtils, "refreshToken");
2023
tokenUtils.setActiveStorage(memoryStorage);
2124
global.fetch = vi.fn();
2225
vi.spyOn(console, "error").mockImplementation(() => {});
@@ -192,10 +195,19 @@ describe("refreshToken", () => {
192195
refresh_token: "new-refresh-token",
193196
};
194197
storageSettings.useInsecureForRefreshToken = true;
198+
199+
const insecureStorage = new MemoryStorage();
200+
201+
tokenUtils.setInsecureStorage(insecureStorage);
202+
await insecureStorage.setSessionItem(
203+
StorageKeys.refreshToken,
204+
mockRefreshTokenValue,
205+
);
206+
vi.spyOn(insecureStorage, "setSessionItem");
195207
memoryStorage.getSessionItem = vi
196208
.fn()
197209
.mockResolvedValue(mockRefreshTokenValue);
198-
vi.mocked(global.fetch).mockResolvedValue({
210+
vi.mocked(global.fetch).mockResolvedValueOnce({
199211
ok: true,
200212
json: () => Promise.resolve(mockResponse),
201213
} as Response);
@@ -219,9 +231,94 @@ describe("refreshToken", () => {
219231
StorageKeys.idToken,
220232
"new-id-token",
221233
);
222-
expect(memoryStorage.setSessionItem).toHaveBeenCalledWith(
234+
expect(insecureStorage.setSessionItem).toHaveBeenCalledWith(
235+
StorageKeys.refreshToken,
236+
"new-refresh-token",
237+
);
238+
expect(memoryStorage.setSessionItem).not.toHaveBeenCalledWith(
223239
StorageKeys.refreshToken,
224240
"new-refresh-token",
225241
);
242+
243+
// reset storageSettings to default
244+
storageSettings.useInsecureForRefreshToken = false;
245+
});
246+
247+
it("raise error when no session storage is found when useInsecureForRefreshToken", async () => {
248+
const mockResponse = {
249+
access_token: "new-access-token",
250+
id_token: "new-id-token",
251+
refresh_token: "new-refresh-token",
252+
};
253+
storageSettings.useInsecureForRefreshToken = true;
254+
255+
tokenUtils.clearActiveStorage();
256+
257+
const insecureStorage = new MemoryStorage();
258+
tokenUtils.setInsecureStorage(insecureStorage);
259+
await insecureStorage.setSessionItem(
260+
StorageKeys.refreshToken,
261+
mockRefreshTokenValue,
262+
);
263+
264+
vi.mocked(global.fetch).mockResolvedValueOnce({
265+
ok: true,
266+
json: () => Promise.resolve(mockResponse),
267+
} as Response);
268+
269+
const result = await tokenUtils.refreshToken({
270+
domain: mockDomain,
271+
clientId: mockClientId,
272+
});
273+
274+
expect(result).toStrictEqual({
275+
error: "No active storage found",
276+
success: false,
277+
});
278+
279+
storageSettings.useInsecureForRefreshToken = false;
280+
});
281+
282+
it("triggers new timer when expires_in supplied and calls refreshToken", async () => {
283+
vi.useFakeTimers();
284+
const mockResponse = {
285+
access_token: "new-access-token",
286+
id_token: "new-id-token",
287+
refresh_token: "new-refresh-token",
288+
expires_in: 1000,
289+
};
290+
291+
const insecureStorage = new MemoryStorage();
292+
tokenUtils.setInsecureStorage(insecureStorage);
293+
await insecureStorage.setSessionItem(
294+
StorageKeys.refreshToken,
295+
mockRefreshTokenValue,
296+
);
297+
298+
vi.mocked(global.fetch).mockResolvedValueOnce({
299+
ok: true,
300+
json: () => Promise.resolve(mockResponse),
301+
} as Response);
302+
303+
const result = await tokenUtils.refreshToken({
304+
domain: mockKindeDomain,
305+
clientId: mockClientId,
306+
});
307+
308+
expect(refreshTimer.setRefreshTimer).toHaveBeenCalledWith(
309+
1000,
310+
expect.any(Function),
311+
);
312+
vi.runAllTimers();
313+
expect(tokenUtils.refreshToken).toHaveBeenCalledWith({
314+
domain: mockKindeDomain,
315+
clientId: mockClientId,
316+
});
317+
expect(result).toStrictEqual({
318+
accessToken: "new-access-token",
319+
idToken: "new-id-token",
320+
refreshToken: "new-refresh-token",
321+
success: true,
322+
});
226323
});
227324
});

0 commit comments

Comments
 (0)