Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions lib/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,29 @@ describe("index exports", () => {
"hasInsecureStorage",
"setInsecureStorage",
"getClaim",
"getClaimSync",
"getClaims",
"getClaimsSync",
"getCurrentOrganization",
"getCurrentOrganizationSync",
"getDecodedToken",
"getDecodedTokenSync",
"getEntitlements",
"getEntitlement",
"getRawToken",
"getRawTokenSync",
"getFlag",
"getFlagSync",
"getPermission",
"getPermissionSync",
"getPermissions",
"getPermissionsSync",
"getRoles",
"getRolesSync",
"getUserOrganizations",
"getUserOrganizationsSync",
"getUserProfile",
"getUserProfileSync",
"setActiveStorage",

// config
Expand Down
11 changes: 11 additions & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,29 @@ export {

export {
getClaim,
getClaimSync,
getClaims,
getClaimsSync,
getCurrentOrganization,
getCurrentOrganizationSync,
getRawToken,
getRawTokenSync,
getDecodedToken,
getDecodedTokenSync,
getFlag,
getFlagSync,
getUserProfile,
getUserProfileSync,
getPermission,
getPermissionSync,
getEntitlement,
getEntitlements,
getPermissions,
getPermissionsSync,
getUserOrganizations,
getUserOrganizationsSync,
getRoles,
getRolesSync,
isAuthenticated,
isTokenExpired,
refreshToken,
Expand Down
1 change: 1 addition & 0 deletions lib/sessionManager/stores/chromeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class ChromeStore<V extends string = StorageKeys>
extends SessionBase<V>
implements SessionManager<V>
{
asyncStore = true;
/**
* Clears all items from session store.
* @returns {void}
Expand Down
1 change: 1 addition & 0 deletions lib/sessionManager/stores/expoSecureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async function waitForExpoSecureStore() {
export class ExpoSecureStore<
V extends string = StorageKeys,
> extends SessionBase<V> {
asyncStore = true;
constructor() {
super();
this.loadExpoStore();
Expand Down
2 changes: 1 addition & 1 deletion lib/sessionManager/stores/localStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ describe("LocalStorage subscription/listening mechanism", () => {
await sessionManager.setSessionItem(StorageKeys.idToken, "mixedTest");

// Wait for setTimeout to fire and async listener to complete
await new Promise((resolve) => setTimeout(resolve, 10));
await new Promise((resolve) => setTimeout(resolve, 20));

expect(syncCalled).toBe(true);
expect(asyncCalled).toBe(true);
Expand Down
18 changes: 8 additions & 10 deletions lib/sessionManager/stores/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class LocalStorage<V extends string = StorageKeys>
extends SessionBase<V>
implements SessionManager<V>
{
asyncStore = false;
constructor() {
super();
if (storageSettings.useInsecureForRefreshToken) {
Expand All @@ -23,9 +24,9 @@ export class LocalStorage<V extends string = StorageKeys>
* Clears all items from session store.
* @returns {void}
*/
async destroySession(): Promise<void> {
await Promise.all(
Array.from(this.internalItems).map((key) => this.removeSessionItem(key)),
destroySession(): void {
Array.from(this.internalItems).forEach((key) =>
this.removeSessionItem(key),
);

this.notifyListeners();
Expand All @@ -37,12 +38,9 @@ export class LocalStorage<V extends string = StorageKeys>
* @param {unknown} itemValue
* @returns {void}
*/
async setSessionItem(
itemKey: V | StorageKeys,
itemValue: unknown,
): Promise<void> {
setSessionItem(itemKey: V | StorageKeys, itemValue: unknown): void {
// clear items first
await this.removeSessionItem(itemKey);
this.removeSessionItem(itemKey);
this.internalItems.add(itemKey);

if (typeof itemValue === "string") {
Expand Down Expand Up @@ -70,7 +68,7 @@ export class LocalStorage<V extends string = StorageKeys>
* @param {string} itemKey
* @returns {unknown | null}
*/
async getSessionItem(itemKey: V | StorageKeys): Promise<unknown | null> {
getSessionItem(itemKey: V | StorageKeys): unknown | null {
if (
localStorage.getItem(`${storageSettings.keyPrefix}${itemKey}0`) === null
) {
Expand All @@ -94,7 +92,7 @@ export class LocalStorage<V extends string = StorageKeys>
* @param {V} itemKey
* @returns {void}
*/
async removeSessionItem(itemKey: V | StorageKeys): Promise<void> {
removeSessionItem(itemKey: V | StorageKeys): void {
// Remove all items with the key prefix
let index = 0;
while (
Expand Down
14 changes: 6 additions & 8 deletions lib/sessionManager/stores/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ export class MemoryStorage<V extends string = StorageKeys>
extends SessionBase<V>
implements SessionManager<V>
{
asyncStore = false;
private memCache: Record<string, unknown> = {};

/**
* Clears all items from session store.
* @returns {void}
*/
async destroySession(): Promise<void> {
destroySession(): void {
this.memCache = {};
this.notifyListeners();
}
Expand All @@ -27,12 +28,9 @@ export class MemoryStorage<V extends string = StorageKeys>
* @param {unknown} itemValue
* @returns {void}
*/
async setSessionItem(
itemKey: V | StorageKeys,
itemValue: unknown,
): Promise<void> {
setSessionItem(itemKey: V | StorageKeys, itemValue: unknown): void {
// clear items first
await this.removeSessionItem(itemKey);
this.removeSessionItem(itemKey);

if (typeof itemValue === "string") {
splitString(itemValue, storageSettings.maxLength).forEach(
Expand All @@ -55,7 +53,7 @@ export class MemoryStorage<V extends string = StorageKeys>
* @param {string} itemKey
* @returns {unknown | null}
*/
async getSessionItem(itemKey: V | StorageKeys): Promise<unknown | null> {
getSessionItem(itemKey: V | StorageKeys): unknown | null {
if (
this.memCache[`${storageSettings.keyPrefix}${String(itemKey)}0`] ===
undefined
Expand All @@ -80,7 +78,7 @@ export class MemoryStorage<V extends string = StorageKeys>
* @param {string} itemKey
* @returns {void}
*/
async removeSessionItem(itemKey: V | StorageKeys): Promise<void> {
removeSessionItem(itemKey: V | StorageKeys): void {
// Remove all items with the key prefix
for (const key in this.memCache) {
if (key.startsWith(`${storageSettings.keyPrefix}${String(itemKey)}`)) {
Expand Down
10 changes: 6 additions & 4 deletions lib/sessionManager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RefreshTokenResult, RefreshType } from "../types";
* satisfiy in order to work with this SDK, please vist the example provided in the
* README, to understand how this works.
*/
type Awaitable<T> = Promise<T>;
type Awaitable<T> = T | Promise<T>;

type StoreListener = () => void | Promise<void>;

Expand Down Expand Up @@ -44,6 +44,7 @@ export type StorageSettingsType = {
export abstract class SessionBase<V extends string = StorageKeys>
implements SessionManager<V>
{
abstract asyncStore: boolean;
private listeners: Set<StoreListener> = new Set();
private notificationScheduled = false;

Expand Down Expand Up @@ -89,7 +90,7 @@ export abstract class SessionBase<V extends string = StorageKeys>
};
}

async setItems(items: Partial<Record<V, unknown>>): Awaitable<void> {
async setItems(items: Partial<Record<V, unknown>>): Promise<void> {
await Promise.all(
(Object.entries(items) as [V | StorageKeys, unknown][]).map(
([key, value]) => {
Expand All @@ -99,7 +100,7 @@ export abstract class SessionBase<V extends string = StorageKeys>
);
}

async getItems(...items: V[]): Awaitable<Partial<Record<V, unknown>>> {
async getItems(...items: V[]): Promise<Partial<Record<V, unknown>>> {
const promises = items.map(async (item) => {
const value = await this.getSessionItem(item);
return [item, value] as const;
Expand All @@ -108,7 +109,7 @@ export abstract class SessionBase<V extends string = StorageKeys>
return Object.fromEntries(entries) as Partial<Record<V, unknown>>;
}

async removeItems(...items: V[]): Awaitable<void> {
async removeItems(...items: V[]): Promise<void> {
await Promise.all(
items.map((item) => {
return this.removeSessionItem(item);
Expand All @@ -118,6 +119,7 @@ export abstract class SessionBase<V extends string = StorageKeys>
}

export interface SessionManager<V extends string = StorageKeys> {
asyncStore: boolean;
/**
*
* Gets the item for the provided key from the storage.
Expand Down
12 changes: 6 additions & 6 deletions lib/utils/activityTracking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,14 @@ describe("Activity Tracking", () => {

await expect(
activeStorage.setSessionItem(StorageKeys.accessToken, "token"),
).resolves.toBeUndefined();
await expect(
activeStorage.getSessionItem(StorageKeys.accessToken),
).resolves.toBe("token");
).toBeUndefined();
await expect(activeStorage.getSessionItem(StorageKeys.accessToken)).toBe(
"token",
);
await expect(
activeStorage.removeSessionItem(StorageKeys.accessToken),
).resolves.toBeUndefined();
await expect(activeStorage.destroySession()).resolves.toBeUndefined();
).toBeUndefined();
await expect(activeStorage.destroySession()).toBeUndefined();
});

it("should properly bind methods and maintain context", async () => {
Expand Down
42 changes: 42 additions & 0 deletions lib/utils/exchangeAuthCode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,4 +576,46 @@ describe("exchangeAuthCode", () => {
"refresh",
);
});

it("returns error when persisting tokens to secure storage fails", async () => {
const store = new MemoryStorage();
setActiveStorage(store);

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

const urlParams = new URLSearchParams();
urlParams.append("code", "hello");
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 setItemsSpy = vi
.spyOn(store, "setItems")
.mockRejectedValue(new Error("Persist failed"));

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

expect(setItemsSpy).toHaveBeenCalled();
expect(result).toStrictEqual({
success: false,
error: expect.stringContaining(
"Failed to persist tokens: Error: Persist failed",
),
});
});
});
18 changes: 13 additions & 5 deletions lib/utils/exchangeAuthCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,19 @@ export const exchangeAuthCode = async ({

const secureStore = getActiveStorage();
if (secureStore) {
secureStore.setItems({
[StorageKeys.accessToken]: data.access_token,
[StorageKeys.idToken]: data.id_token,
[StorageKeys.refreshToken]: data.refresh_token,
});
try {
await secureStore.setItems({
[StorageKeys.accessToken]: data.access_token,
[StorageKeys.idToken]: data.id_token,
[StorageKeys.refreshToken]: data.refresh_token,
});
} catch (error) {
console.error("Failed to persist tokens to secure storage:", error);
return {
success: false,
error: `Failed to persist tokens: ${error}`,
};
}
}

if (storageSettings.useInsecureForRefreshToken || !isCustomDomain(domain)) {
Expand Down
25 changes: 23 additions & 2 deletions lib/utils/token/getClaim.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, beforeEach } from "vitest";
import { getClaim, setActiveStorage } from ".";
import { getClaim, getClaimSync, setActiveStorage } from ".";
import { createMockAccessToken } from "./testUtils";
import { MemoryStorage, StorageKeys } from "../../main";

Expand All @@ -11,7 +11,7 @@ describe("getClaim", () => {
});

it("when no token", async () => {
await storage.setSessionItem(StorageKeys.accessToken, null);
await storage.removeSessionItem(StorageKeys.accessToken);
const value = await getClaim("test");
expect(value).toStrictEqual(null);
});
Expand All @@ -28,3 +28,24 @@ describe("getClaim", () => {
});
});
});

describe("getClaimSync", () => {
beforeEach(() => {
setActiveStorage(storage);
});

it("when no token", () => {
storage.removeSessionItem(StorageKeys.accessToken);
const value = getClaimSync("test");
expect(value).toStrictEqual(null);
});

it("get claim string value", () => {
storage.setSessionItem(
StorageKeys.accessToken,
createMockAccessToken({ test: "org_123456" }),
);
const value = getClaimSync("test");
expect(value).toStrictEqual({ name: "test", value: "org_123456" });
});
});
Loading