Skip to content
Draft
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
80 changes: 78 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,82 @@ oidcClient.setRefreshPath("refresh-token");

> Note: the `refreshPath` property defaults to **token/refresh**

### Storage Providers

The library now supports flexible storage providers, allowing you to run multiple instances of OIDCClient in parallel without conflicts.

#### Browser Storage Provider (Default)

By default, the library uses `BrowserStorageProvider` which stores tokens in browser's localStorage and sessionStorage:

```js
import { OIDCClient } from "@fundwave/oidc-client";

const oidcClient = new OIDCClient();
// Access token is stored in sessionStorage with key "token"
// Refresh token is stored in localStorage with key "refreshToken"
```

#### Custom Token Names

You can customize the token names to avoid conflicts between multiple instances:

```js
import { OIDCClient } from "@fundwave/oidc-client";

const oidcClient1 = new OIDCClient({
tokenNames: { token: "token", refreshToken: "refreshToken" }
});

const oidcClient2 = new OIDCClient({
tokenNames: { token: "token2", refreshToken: "refreshToken2" }
});

// Now both instances can work in parallel using different storage keys
```

#### In-Memory Storage Provider

For temporary storage that doesn't persist between page reloads:

```js
import { OIDCClient, InMemoryStorageProvider } from "@fundwave/oidc-client";

const memoryStorage = new InMemoryStorageProvider();
const oidcClient = new OIDCClient({
storageProvider: memoryStorage
});

// Tokens are stored in memory and cleared on page reload
```

#### Custom Storage Provider

You can implement your own storage provider (e.g., for MongoDB, IndexedDB, etc.):

```js
import { OIDCClient, StorageProvider } from "@fundwave/oidc-client";

class MyCustomStorageProvider implements StorageProvider {
async getItem(key: string): Promise<string | null> {
// Your custom implementation
}

async setItem(key: string, value: string): Promise<void> {
// Your custom implementation
}

async removeItem(key: string): Promise<void> {
// Your custom implementation
}
}

const customStorage = new MyCustomStorageProvider();
const oidcClient = new OIDCClient({
storageProvider: customStorage
});
```

### Usage

Once the class has been instantiated, you can
Expand All @@ -49,8 +125,8 @@ Once the class has been instantiated, you can

- If your client app makes parallel calls to the same object of oidc-client, this library will still make only one active call to your OIDC server. This will reduce network calls and avoid exceeding any rate limits with your OIDC server.

- **Access Token** is maintained at browser's _session storage_ with the key being `token`
- **Multiple Instances**: You can now run multiple instances of OIDCClient in parallel by using different storage providers or custom token names. This solves the problem of one instance overriding another instance's tokens.

- **Refresh Token** is maintained at browser's _local storage_ with the key being `refreshToken`
- By default, **Access Token** is maintained at browser's _session storage_ with the key being `token`, and **Refresh Token** is maintained at browser's _local storage_ with the key being `refreshToken`. These can be customized using the `tokenNames` option.

- The library will read tokens sent by your OIDC server from either the response **body** or **headers**
106 changes: 95 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,85 @@
import jwt_decode from "jwt-decode";

export interface StorageProvider {
getItem(key: string): string | null | Promise<string | null>;
setItem(key: string, value: string): void | Promise<void>;
removeItem(key: string): void | Promise<void>;
}

export interface TokenNames {
token?: string;
refreshToken?: string;
}

export class BrowserStorageProvider implements StorageProvider {
private tokenNames: Required<TokenNames>;

constructor(tokenNames?: TokenNames) {
this.tokenNames = {
token: tokenNames?.token || "token",
refreshToken: tokenNames?.refreshToken || "refreshToken",
};
}

getItem(key: string): string | null {
if (key === "token") {
return (typeof sessionStorage !== "undefined") ? (sessionStorage.getItem(this.tokenNames.token)) : null;
}
if (key === "refreshToken") {
return (typeof localStorage !== "undefined") ? (localStorage.getItem(this.tokenNames.refreshToken)) : null;
}
return null;
}

setItem(key: string, value: string): void {
if (key === "token" && typeof sessionStorage !== "undefined") {
sessionStorage.setItem(this.tokenNames.token, value);
}
if (key === "refreshToken" && typeof localStorage !== "undefined") {
localStorage.setItem(this.tokenNames.refreshToken, value);
}
}

removeItem(key: string): void {
if (key === "token" && typeof sessionStorage !== "undefined") {
sessionStorage.removeItem(this.tokenNames.token);
}
if (key === "refreshToken" && typeof localStorage !== "undefined") {
localStorage.removeItem(this.tokenNames.refreshToken);
}
}
}

export class InMemoryStorageProvider implements StorageProvider {
private store: Map<string, string>;

constructor() {
this.store = new Map();
}

getItem(key: string): string | null {
return this.store.get(key) ?? null;
}

setItem(key: string, value: string): void {
this.store.set(key, value);
}

removeItem(key: string): void {
this.store.delete(key);
}

clear(): void {
this.store.clear();
}
}

interface OIDCClientOptions {
refreshPath?: string;
baseUrl?: string;
headers?: Record<string, string>;
storageProvider?: StorageProvider;
tokenNames?: TokenNames;
}

interface TokenResponse {
Expand All @@ -17,6 +93,7 @@ export class OIDCClient {
private refreshPath: string;
private baseUrl: string | undefined;
private BASE_HEADERS: Record<string, string>;
private storageProvider: StorageProvider;

constructor(options?: OIDCClientOptions) {
this.refreshTokenLock = false;
Expand All @@ -26,6 +103,11 @@ export class OIDCClient {
"Content-Type": "application/json; charset=UTF-8",
Accept: "application/json, text/javascript, */*; q=0.01",
};
const tokenNames = {
token: options?.tokenNames?.token || "token",
refreshToken: options?.tokenNames?.refreshToken || "refreshToken",
};
this.storageProvider = options?.storageProvider || new BrowserStorageProvider(tokenNames);
this.#refreshTokenPromise = null;
}

Expand Down Expand Up @@ -68,9 +150,11 @@ export class OIDCClient {
}

async getAccessToken(): Promise<string | undefined> {
if (typeof localStorage === "undefined" || !localStorage.getItem("refreshToken"))
const refreshToken = await this.storageProvider.getItem("refreshToken");
if (!refreshToken) {
// Either we're in a non-browser environment, or session security is used
return;
}

try {
for (let count = 0; this.refreshTokenLock && count < 15; count++) {
Expand All @@ -79,15 +163,15 @@ export class OIDCClient {
}

if (this.#refreshTokenPromise) await this.#refreshTokenPromise;
else if (!this.verifyTokenValidity()) await this._refreshToken();
else if (!await this.verifyTokenValidity()) await this._refreshToken();
} catch (err) {
console.log(err);
sessionStorage.removeItem("token");
localStorage.removeItem("refreshToken");
await this.storageProvider.removeItem("token");
await this.storageProvider.removeItem("refreshToken");
document.dispatchEvent(new CustomEvent("logged-out", { bubbles: true, composed: true }));
}

return sessionStorage.getItem("token") || undefined;
return await this.storageProvider.getItem("token") || undefined;
}

async _wait(time = 1200): Promise<void> {
Expand All @@ -98,8 +182,8 @@ export class OIDCClient {
});
}

verifyTokenValidity(): boolean {
const token = sessionStorage.getItem("token");
async verifyTokenValidity(): Promise<boolean> {
const token = await this.storageProvider.getItem("token");
if (!token) return false;
try {
const exp = jwt_decode<{ exp: number }>(token);
Expand All @@ -110,8 +194,8 @@ export class OIDCClient {
}

async _refreshToken(): Promise<void> {
const token = sessionStorage.getItem("token");
const refreshToken = localStorage.getItem("refreshToken");
const token = await this.storageProvider.getItem("token");
const refreshToken = await this.storageProvider.getItem("refreshToken");
const headers: Record<string, string> = { ...this.BASE_HEADERS };

if (!refreshToken) throw new Error("No refresh token");
Expand All @@ -136,8 +220,8 @@ export class OIDCClient {
const refreshToken = data?.["refreshToken"] || response.headers.get("refreshToken");

if (!token && !refreshToken) throw new Error("Couldn't fetch `access-token` or `refresh-token`");
if (token) sessionStorage.setItem("token", token);
if (refreshToken) localStorage.setItem("refreshToken", refreshToken);
if (token) await this.storageProvider.setItem("token", token);
if (refreshToken) await this.storageProvider.setItem("refreshToken", refreshToken);
})
.catch((err) => {
console.log("Failed to refresh tokens", err);
Expand Down
Loading