Skip to content

Commit 2221169

Browse files
committed
feat: add activeStorage, getDecodedToken and getUserOrganisations
1 parent bfb356f commit 2221169

9 files changed

+220
-3
lines changed

lib/utils/base64UrlEncode.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
*
2+
* Encodes the provided ArrayBuffer string to base-64 format.
33
* @param str String to encode
44
* @returns encoded string
55
*/
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it, beforeEach } from "vitest";
2+
import { getDecodedToken } from "./getDecodedToken";
3+
import { MemoryStorage, StorageKeys } from "../../sessionManager";
4+
import { setActiveStorage } from ".";
5+
import { createMockAccessToken } from "./testUtils";
6+
7+
describe("getDecodedToken", () => {
8+
it("error when no active storage is set", () => {
9+
expect(() => getDecodedToken("idToken")).rejects.toThrowError(
10+
"Session manager is not initialized",
11+
);
12+
});
13+
});
14+
15+
describe("getDecodedToken", () => {
16+
beforeEach(() => {
17+
setActiveStorage(new MemoryStorage());
18+
});
19+
it("error when no active storage is set", async () => {
20+
const idToken = await getDecodedToken("idToken");
21+
expect(idToken).toBe(null);
22+
});
23+
});
24+
25+
describe("getDecodedToken idToken", () => {
26+
beforeEach(() => {
27+
const storage = new MemoryStorage();
28+
setActiveStorage(storage);
29+
storage.setSessionItem(StorageKeys.idToken, createMockAccessToken());
30+
});
31+
it("error when no active storage is set", async () => {
32+
const idToken = await getDecodedToken("idToken");
33+
if (idToken === null) {
34+
throw new Error("idToken is null");
35+
}
36+
expect(idToken.org_code).toBe("org_123456789");
37+
});
38+
});
39+
40+
describe("getDecodedToken accessToken", () => {
41+
beforeEach(() => {
42+
const storage = new MemoryStorage();
43+
setActiveStorage(storage);
44+
storage.setSessionItem(StorageKeys.accessToken, createMockAccessToken());
45+
});
46+
it("error when no active storage is set", async () => {
47+
const accessToken = await getDecodedToken("accessToken");
48+
if (accessToken === null) {
49+
throw new Error("idToken is null");
50+
}
51+
expect(accessToken.org_code).toBe("org_123456789");
52+
});
53+
});

lib/utils/token/getDecodedToken.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { jwtDecoder, JWTDecoded } from "@kinde/jwt-decoder";
2+
import { getActiveStorage } from ".";
3+
import { StorageKeys } from "../../sessionManager";
4+
/**
5+
*
6+
* @param tokenType Type of token to decode
7+
* @returns { Promise<JWTDecoded | null> }
8+
*/
9+
export const getDecodedToken = async <
10+
T = JWTDecoded & {
11+
permissions: string[];
12+
org_code: string;
13+
},
14+
>(
15+
tokenType: "accessToken" | "idToken" = "accessToken",
16+
): Promise<T | null> => {
17+
const activeStorage = getActiveStorage();
18+
if (!activeStorage) {
19+
throw new Error("Session manager is not initialized");
20+
}
21+
22+
const token = (await activeStorage.getSessionItem(
23+
tokenType === "accessToken" ? StorageKeys.accessToken : StorageKeys.idToken,
24+
)) as string;
25+
console.log("token", token);
26+
if (!token) {
27+
return null;
28+
}
29+
30+
return jwtDecoder<T>(token);
31+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it, beforeEach } from "vitest";
2+
import { MemoryStorage, StorageKeys } from "../../sessionManager";
3+
import { setActiveStorage } from ".";
4+
import { createMockAccessToken } from "./testUtils";
5+
import { getUserOrganizations } from "./getUserOrganistaions";
6+
7+
describe("getDecodedToken idToken", () => {
8+
beforeEach(() => {
9+
const storage = new MemoryStorage();
10+
setActiveStorage(storage);
11+
storage.setSessionItem(
12+
StorageKeys.idToken,
13+
createMockAccessToken({ org_codes: ["org_123456789"] }),
14+
);
15+
});
16+
it("error when no active storage is set", async () => {
17+
const idToken = await getUserOrganizations();
18+
19+
expect(idToken).toStrictEqual(["org_123456789"]);
20+
});
21+
});
22+
23+
describe("getDecodedToken idToken", () => {
24+
beforeEach(() => {
25+
const storage = new MemoryStorage();
26+
setActiveStorage(storage);
27+
storage.setSessionItem(
28+
StorageKeys.idToken,
29+
createMockAccessToken({ org_codes: ["org_123456789", "org_1234567"] }),
30+
);
31+
});
32+
it("error when no active storage is set", async () => {
33+
const idToken = await getUserOrganizations();
34+
35+
expect(idToken).toStrictEqual(["org_123456789", "org_1234567"]);
36+
});
37+
});
38+
39+
describe("getDecodedToken idToken", () => {
40+
beforeEach(() => {
41+
const storage = new MemoryStorage();
42+
setActiveStorage(storage);
43+
storage.setSessionItem(
44+
StorageKeys.idToken,
45+
createMockAccessToken({ org_codes: null }),
46+
);
47+
});
48+
it("error when no active storage is set", async () => {
49+
const idToken = await getUserOrganizations();
50+
51+
expect(idToken).toStrictEqual(null);
52+
});
53+
});
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getDecodedToken } from "./getDecodedToken";
2+
3+
/**
4+
* Gets all the code of the organizations the user belongs to.
5+
* @returns { Promise<string[] | null> }
6+
*/
7+
export const getUserOrganizations = async (): Promise<string[] | null> => {
8+
return (
9+
(
10+
await getDecodedToken<{
11+
org_codes: string[];
12+
}>("idToken")
13+
)?.org_codes || null
14+
);
15+
};

lib/utils/token/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SessionManager } from "../../sessionManager"; // Replace 'path/to/SessionManager' with the actual path to the SessionManager module
2+
3+
const storage = {
4+
value: null as SessionManager | null,
5+
};
6+
7+
export const setActiveStorage = (store: SessionManager) => {
8+
storage.value = store;
9+
console.log("store", store);
10+
};
11+
12+
export const getActiveStorage = () => {
13+
return storage.value;
14+
};

lib/utils/token/testUtils/index.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createHmac } from "crypto";
2+
3+
export const createMockAccessToken = (values = {}) => {
4+
const header = {
5+
alg: "HS256", // Algorithm (HMAC with SHA-256)
6+
typ: "JWT", // Type
7+
};
8+
const payload = {
9+
aud: [],
10+
azp: "b9da18c441b44d81bab3e8232de2e18d",
11+
billing: {
12+
has_payment_details: false,
13+
},
14+
exp: 1168335720000,
15+
iat: 1168335720000,
16+
iss: "https://kinde.com",
17+
jti: "27daa125-2fb2-4e14-9270-742cd56e764b",
18+
org_code: "org_123456789",
19+
permissions: ["read:users", "read:competitions"],
20+
scp: ["openid", "profile", "email", "offline"],
21+
sub: "kp_cfcb1ae5b9254ad99521214014c54f43",
22+
...values,
23+
};
24+
const secretKey = "asecretkey";
25+
26+
// Encode header and payload as Base64URL
27+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
28+
"base64url",
29+
);
30+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
31+
"base64url",
32+
);
33+
// Create signature
34+
const signature = createHmac("sha256", secretKey)
35+
.update(`${encodedHeader}.${encodedPayload}`)
36+
.digest("base64url");
37+
38+
return `${encodedHeader}.${encodedPayload}.${signature}`;
39+
};

package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
"@eslint/js": "^9.9.1",
3232
"@types/chrome": "^0.0.270",
3333
"@types/node": "^22.5.1",
34-
"@vitest/coverage-v8": "^2.0.5",
3534
"@vitejs/plugin-react": "^4.3.1",
35+
"@vitest/coverage-v8": "^2.0.5",
3636
"eslint": "^9.9.1",
3737
"globals": "^15.9.0",
3838
"prettier": "^3.3.3",
@@ -45,5 +45,8 @@
4545
"optionalDependencies": {
4646
"expo-secure-store": "*"
4747
},
48-
"packageManager": "[email protected]+sha256.2df78e65d433d7693b9d3fbdaf431b2d96bb4f96a2ffecd51a50efe16e50a6a8"
48+
"packageManager": "[email protected]+sha256.2df78e65d433d7693b9d3fbdaf431b2d96bb4f96a2ffecd51a50efe16e50a6a8",
49+
"dependencies": {
50+
"@kinde/jwt-decoder": "^0.2.0"
51+
}
4952
}

pnpm-lock.yaml

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)