Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: codeExchange, storage updates, generateAuthUrl fixes #15

Merged
merged 30 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e0e2422
fix: couple of small updates to generateAuthUrl, including auto setti…
DanielRivers Oct 24, 2024
3c00453
fix: remove exception when no state
DanielRivers Oct 25, 2024
c110df8
tests: fix tests
DanielRivers Oct 25, 2024
d8313d7
test: fix tests
DanielRivers Oct 25, 2024
b4fde7a
Merge branch 'main' into fix/generateAuthUrl
DanielRivers Oct 25, 2024
19ccb41
test: fix tests
DanielRivers Oct 25, 2024
8daa59d
feat: added hasActiveStorage and JSDocs
DanielRivers Oct 25, 2024
78a96b6
BREAKING CHANGE: Make generateAuthUrl async
DanielRivers Oct 25, 2024
3658bc4
fix: PR updates
DanielRivers Oct 25, 2024
05e7203
fix: add missing StorageKey
DanielRivers Oct 25, 2024
6e2399f
feat: add clearActiveStorage and tests
DanielRivers Oct 28, 2024
bd82a91
chore: lint
DanielRivers Oct 28, 2024
f8f605a
chore: clean up tests
DanielRivers Oct 28, 2024
7c649b4
fix: extend code verifier
DanielRivers Oct 28, 2024
8c9474c
feat: add remove multiple items support from store
DanielRivers Oct 28, 2024
eb514ce
feat: add exchangeAuthCode method
DanielRivers Oct 28, 2024
912a7cb
chore: lint and remove only from tests
DanielRivers Oct 28, 2024
b33533e
chore: update lock
DanielRivers Oct 28, 2024
5ba4864
chore: remove incomplete store.
DanielRivers Oct 28, 2024
62ebed6
feat: added framework settings config and expanded tests
DanielRivers Oct 29, 2024
3243c4e
test: update tests
DanielRivers Oct 29, 2024
eff28e6
chore: lint
DanielRivers Oct 29, 2024
7c866c1
chore: update test config
DanielRivers Oct 29, 2024
c78f029
feat: improve splitString
DanielRivers Oct 30, 2024
4f0e74b
fix: await removing storage items
DanielRivers Oct 31, 2024
d033a0d
feat: add handling bad responses from token endpoint
DanielRivers Oct 31, 2024
00a554f
fix: error handling improvements and improved security
DanielRivers Oct 31, 2024
b6877c0
test: minor updates
DanielRivers Oct 31, 2024
e0d39fd
fix: preseve browser state
DanielRivers Oct 31, 2024
034021b
chore: remove redundant import
DanielRivers Oct 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe("index exports", () => {
"generateRandomString",
"mapLoginMethodParamsForUrl",
"sanatizeURL",
"exchangeAuthCode",

// session manager
"MemoryStorage",
Expand All @@ -49,6 +50,8 @@ describe("index exports", () => {

// token utils
"getActiveStorage",
"hasActiveStorage",
"clearActiveStorage",
"getClaim",
"getClaims",
"getCurrentOrganization",
Expand All @@ -60,6 +63,9 @@ describe("index exports", () => {
"getUserOrganizations",
"getUserProfile",
"setActiveStorage",

// config
"frameworkSettings",
];

expect(actualExports.sort()).toEqual(expectedExports.sort());
Expand Down
15 changes: 15 additions & 0 deletions lib/sessionManager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum StorageKeys {
refreshToken = "refreshToken",
state = "state",
nonce = "nonce",
codeVerifier = "codeVerifier",
}

export type StorageSettingsType = {
Expand Down Expand Up @@ -40,6 +41,14 @@ export abstract class SessionBase<V extends string = StorageKeys>
),
);
}

async removeItems(...items: V[]): Awaitable<void> {
await Promise.all(
items.map((item) => {
return this.removeSessionItem(item);
}),
);
}
}

export interface SessionManager<V extends string = StorageKeys> {
Expand Down Expand Up @@ -80,4 +89,10 @@ export interface SessionManager<V extends string = StorageKeys> {
* @returns {Promise<void>}
*/
setItems(items: Partial<Record<V, unknown>>): Awaitable<void>;

/**
* Removes multiple items simultaneously.
* @param items
*/
removeItems(...items: V[]): Awaitable<void>;
}
1 change: 0 additions & 1 deletion lib/sessionManager/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// utils.test.ts
import { splitString } from "./utils";
import { describe, expect, it } from "vitest";

Expand Down
6 changes: 1 addition & 5 deletions lib/sessionManager/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,5 @@ export function splitString(str: string, length: number): string[] {
if (length <= 0) {
return [];
}
const result = [];
for (let i = 0; i < str.length; i += length) {
result.push(str.slice(i, i + length));
}
return result;
return str.match(new RegExp(`.{1,${length}}`, "g")) || [];
}
229 changes: 229 additions & 0 deletions lib/utils/exchangeAuthCode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { exchangeAuthCode } from ".";
import { MemoryStorage, StorageKeys } from "../sessionManager";
import { setActiveStorage } from "./token";
import createFetchMock from "vitest-fetch-mock";
import { frameworkSettings } from "./exchangeAuthCode";

const fetchMock = createFetchMock(vi);

describe("exchangeAuthCode", () => {
beforeEach(() => {
fetchMock.enableMocks();
});

afterEach(() => {
fetchMock.resetMocks();
});

it("missing state param", async () => {
const urlParams = new URLSearchParams();
urlParams.append("code", "test");

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

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

it("missing code param", async () => {
const urlParams = new URLSearchParams();
urlParams.append("state", "test");

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

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

it("missing active storage", async () => {
const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");

expect(
await exchangeAuthCode({
urlParams,
domain: "http://test.kinde.com",
clientId: "test",
redirectURL: "http://test.kinde.coma",
}),
).toStrictEqual({
error: "Authentication storage is not initialized",
success: false,
});
});

it("state mismatch", async () => {
const store = new MemoryStorage();
setActiveStorage(store);

await store.setItems({
[StorageKeys.state]: "storedState",
});

const urlParams = new URLSearchParams();
urlParams.append("state", "test");
urlParams.append("code", "test");

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

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

it("should exchange tokens, set storage and clear temp values", async () => {
const store = new MemoryStorage();
setActiveStorage(store);

const state = "state";

await store.setItems({
[StorageKeys.state]: state,
});

const input = "hello";

const urlParams = new URLSearchParams();
urlParams.append("code", input);
urlParams.append("state", state);
urlParams.append("client_id", "test");

fetchMock.mockResponseOnce(
JSON.stringify({
access_token: "access_token",
refresh_token: "refresh_token",
id_token: "id_token",
}),
);

const result = await exchangeAuthCode({
urlParams,
domain: "http://test.kinde.com",
clientId: "test",
redirectURL: "http://test.kinde.com",
});
expect(result).toStrictEqual({
accessToken: "access_token",
refreshToken: "refresh_token",
idToken: "id_token",
success: true,
});

const postStoredState = await store.getSessionItem(StorageKeys.state);
expect(postStoredState).toBeNull();
const postCodeVerifier = await store.getSessionItem(
StorageKeys.codeVerifier,
);
expect(postCodeVerifier).toBeNull();
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, options] = fetchMock.mock.calls[0];
expect(url).toBe("http://test.kinde.com/oauth2/token");
expect(options).toMatchObject({
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
});
});

it("set the framework and version on header", async () => {
const store = new MemoryStorage();
setActiveStorage(store);

const state = "state";

await store.setItems({
[StorageKeys.state]: state,
});

frameworkSettings.framework = "Framework";
frameworkSettings.frameworkVersion = "Version";

const input = "hello";

const urlParams = new URLSearchParams();
urlParams.append("code", input);
urlParams.append("state", state);
urlParams.append("client_id", "test");

fetchMock.mockResponseOnce(
JSON.stringify({
access_token: "access_token",
refresh_token: "refresh_token",
id_token: "id_token",
}),
);

await exchangeAuthCode({
urlParams,
domain: "http://test.kinde.com",
clientId: "test",
redirectURL: "http://test.kinde.com",
});

expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, options] = fetchMock.mock.calls[0];
expect(url).toBe("http://test.kinde.com/oauth2/token");
expect(options).toMatchObject({
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"Kinde-SDK": "Framework/Version",
},
});
});

it("should handle token exchange failure", async () => {
const store = new MemoryStorage();
setActiveStorage(store);

const state = "state";

await store.setItems({
[StorageKeys.state]: state,
});

const input = "hello";

const urlParams = new URLSearchParams();
urlParams.append("code", input);
urlParams.append("state", state);
urlParams.append("client_id", "test");

fetchMock.mockOnce({ status: 500, ok: false, body: "error" });

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

expect(result).toStrictEqual({
success: false,
error: "Token exchange failed: 500 - error",
});
});
});
Loading
Loading