Skip to content

Commit eb514ce

Browse files
committed
feat: add exchangeAuthCode method
1 parent 8c9474c commit eb514ce

5 files changed

+229
-57
lines changed

lib/main.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ describe("index exports", () => {
3939
"generateRandomString",
4040
"mapLoginMethodParamsForUrl",
4141
"sanatizeURL",
42+
"exchangeAuthCode",
4243

4344
// session manager
4445
"MemoryStorage",

lib/utils/exchangeAuthCode.test.ts

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2+
import { exchangeAuthCode } from ".";
3+
import { MemoryStorage, StorageKeys } from "../sessionManager";
4+
import { setActiveStorage } from "./token";
5+
import createFetchMock from 'vitest-fetch-mock';
6+
7+
const fetchMock = createFetchMock(vi);
8+
9+
describe.only("exhangeAuthCode", () => {
10+
beforeEach(() => {
11+
fetchMock.enableMocks();
12+
});
13+
14+
afterEach(() => {
15+
fetchMock.resetMocks();
16+
});
17+
18+
it.only("missing state param", async () => {
19+
const urlParams = new URLSearchParams();
20+
urlParams.append("code", "test");
21+
22+
const result = await exchangeAuthCode({
23+
urlParams,
24+
domain: "http://test.kinde.com",
25+
clientId: "test",
26+
redirectURL: "http://test.kinde.com",
27+
});
28+
29+
expect(result).toStrictEqual({
30+
success: false,
31+
error: "Invalid state or code",
32+
});
33+
});
34+
35+
it.only("missing code param", async () => {
36+
const urlParams = new URLSearchParams();
37+
urlParams.append("state", "test");
38+
39+
const result = await exchangeAuthCode({
40+
urlParams,
41+
domain: "http://test.kinde.com",
42+
clientId: "test",
43+
redirectURL: "http://test.kinde.com",
44+
});
45+
46+
expect(result).toStrictEqual({
47+
success: false,
48+
error: "Invalid state or code",
49+
});
50+
});
51+
52+
it.only("missing active storage", async () => {
53+
const urlParams = new URLSearchParams();
54+
urlParams.append("state", "test");
55+
urlParams.append("code", "test");
56+
57+
expect(exchangeAuthCode({
58+
urlParams,
59+
domain: "http://test.kinde.com",
60+
clientId: "test",
61+
redirectURL: "http://test.kinde.com",
62+
})).rejects.toThrowError("No active storage found");
63+
});
64+
65+
it.only("state mismatch", async () => {
66+
const store = new MemoryStorage();
67+
setActiveStorage(store);
68+
69+
await store.setItems({
70+
[StorageKeys.state]: "storedState",
71+
});
72+
73+
const urlParams = new URLSearchParams();
74+
urlParams.append("state", "test");
75+
urlParams.append("code", "test");
76+
77+
const result = await exchangeAuthCode({
78+
urlParams,
79+
domain: "http://test.kinde.com",
80+
clientId: "test",
81+
redirectURL: "http://test.kinde.com",
82+
})
83+
84+
expect(result).toStrictEqual({
85+
success: false,
86+
error: "Invalid state; supplied test, expected storedState",
87+
});
88+
});
89+
90+
91+
it("should encode a simple string", async () => {
92+
const store = new MemoryStorage();
93+
setActiveStorage(store);
94+
95+
const state = "state";
96+
97+
await store.setItems({
98+
[StorageKeys.state]: state,
99+
});
100+
101+
const input = "hello";
102+
103+
const urlParams = new URLSearchParams();
104+
urlParams.append("code", input);
105+
urlParams.append("state", state);
106+
urlParams.append("client_id", "test");
107+
108+
fetchMock.mockResponseOnce(JSON.stringify({ access_token: "access_token", refresh_token: "refresh_token", id_token: "id_token" }));
109+
110+
const result = await exchangeAuthCode({
111+
urlParams,
112+
domain: "http://test.kinde.com",
113+
clientId: "test",
114+
redirectURL: "http://test.kinde.com",
115+
});
116+
expect(result).toStrictEqual({
117+
accessToken: "access_token",
118+
refreshToken: "refresh_token",
119+
idToken: "id_token",
120+
success: true,
121+
});
122+
123+
const postStoredState = await store.getSessionItem(StorageKeys.state);
124+
expect(postStoredState).toBeNull();
125+
const postCodeVerifier = await store.getSessionItem(StorageKeys.codeVerifier);
126+
expect(postCodeVerifier).toBeNull();
127+
});
128+
129+
});

lib/utils/exchangeAuthCode.ts

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { getActiveStorage, StorageKeys } from "../main";
2+
3+
// TODO: Set the framework and version
4+
const _framework = "";
5+
const _frameworkVersion = "";
6+
7+
export const exchangeAuthCode = async ({
8+
urlParams,
9+
domain,
10+
clientId,
11+
redirectURL,
12+
}: {
13+
urlParams: URLSearchParams,
14+
domain: string,
15+
clientId: string,
16+
redirectURL: string,
17+
}): Promise<unknown> => {
18+
const state = urlParams.get("state");
19+
const code = urlParams.get("code");
20+
21+
if (!state || !code) {
22+
console.error("Invalid state or code");
23+
return {
24+
success: false,
25+
error: "Invalid state or code",
26+
};
27+
}
28+
29+
const activeStorage = getActiveStorage();
30+
if (!activeStorage) {
31+
throw new Error("No active storage found");
32+
}
33+
activeStorage.getSessionItem(StorageKeys.state);
34+
35+
// warn if framework and version has not been set
36+
if (!_framework || !_frameworkVersion) {
37+
console.warn(
38+
"Framework and version not set. Please set the framework and version in the config object",
39+
);
40+
}
41+
42+
const storedState = await activeStorage.getSessionItem(StorageKeys.state)
43+
if (state !== storedState) {
44+
console.error("Invalid state");
45+
return {
46+
success: false,
47+
error: `Invalid state; supplied ${state}, expected ${storedState}`,
48+
};
49+
}
50+
51+
const codeVerifier = (await activeStorage.getSessionItem(
52+
StorageKeys.codeVerifier,
53+
)) as string;
54+
55+
const response = await fetch(`${domain}/oauth2/token`, {
56+
method: "POST",
57+
// ...(isUseCookie && {credentials: 'include'}),
58+
credentials: "include",
59+
headers: new Headers({
60+
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
61+
"Kinde-SDK": `${_framework}/${_frameworkVersion}`,
62+
}),
63+
body: new URLSearchParams({
64+
client_id: clientId,
65+
code,
66+
code_verifier: codeVerifier,
67+
grant_type: "authorization_code",
68+
redirect_uri: redirectURL,
69+
}),
70+
});
71+
72+
const data: {
73+
access_token: string;
74+
id_token: string;
75+
refresh_token: string;
76+
} = await response.json();
77+
78+
activeStorage.setItems({
79+
[StorageKeys.accessToken]: data.access_token,
80+
[StorageKeys.idToken]: data.id_token,
81+
[StorageKeys.refreshToken]: data.refresh_token,
82+
});
83+
84+
activeStorage.removeItems(StorageKeys.state, StorageKeys.codeVerifier);
85+
86+
// Clear all url params
87+
// const url = new URL(window.location.toString());
88+
// url.search = "";
89+
// window.history.pushState({}, "", url);
90+
91+
return {
92+
success: true,
93+
[StorageKeys.accessToken]: data.access_token,
94+
[StorageKeys.idToken]: data.id_token,
95+
[StorageKeys.refreshToken]: data.refresh_token,
96+
};
97+
};

lib/utils/exhangeAuthCode._ts

-57
This file was deleted.

lib/utils/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { extractAuthResults } from "./extractAuthResults";
55
import { sanatizeURL } from "./sanatizeUrl";
66
import { generateAuthUrl } from "./generateAuthUrl";
77
import { mapLoginMethodParamsForUrl } from "./mapLoginMethodParamsForUrl";
8+
import { exchangeAuthCode } from "./exchangeAuthCode";
89

910
export {
1011
base64UrlEncode,
@@ -13,4 +14,5 @@ export {
1314
sanatizeURL,
1415
generateAuthUrl,
1516
mapLoginMethodParamsForUrl,
17+
exchangeAuthCode
1618
};

0 commit comments

Comments
 (0)