From 7c06c1faecb600cbfa21c9171e3814e8387e8eda Mon Sep 17 00:00:00 2001 From: Benjmain Main Date: Wed, 24 Jun 2026 09:28:47 +0200 Subject: [PATCH] feat(extraTokenProperties): Add extra token properties --- docs/oidc-client-ts.api.md | 6 +++- src/User.test.ts | 57 ++++++++++++++++++++++++++++++++- src/User.ts | 18 ++++++++++- src/UserManager.ts | 4 +-- src/UserManagerSettings.test.ts | 20 ++++++++++++ src/UserManagerSettings.ts | 14 +++++++- 6 files changed, 113 insertions(+), 6 deletions(-) diff --git a/docs/oidc-client-ts.api.md b/docs/oidc-client-ts.api.md index 51e9145f2..fbc87fe80 100644 --- a/docs/oidc-client-ts.api.md +++ b/docs/oidc-client-ts.api.md @@ -969,12 +969,13 @@ export class User { expires_at?: number; userState?: unknown; url_state?: string; - }); + }, extraTokenResponseKeys?: string[]); access_token: string; get expired(): boolean | undefined; expires_at?: number; get expires_in(): number | undefined; set expires_in(value: number | undefined); + extraTokenResponseProperties?: Record; // (undocumented) static fromStorageString(storageString: string): User; id_token?: string; @@ -1122,6 +1123,7 @@ export interface UserManagerSettings extends OidcClientSettings { accessTokenExpiringNotificationTimeInSeconds?: number; automaticSilentRenew?: boolean; checkSessionIntervalInSeconds?: number; + extraTokenResponseKeys?: string[]; iframeAttributes?: Record | undefined; iframeNotifyParentOrigin?: string; iframeScriptOrigin?: string; @@ -1160,6 +1162,8 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore { // (undocumented) readonly checkSessionIntervalInSeconds: number; // (undocumented) + readonly extraTokenResponseKeys: string[]; + // (undocumented) readonly iframeAttributes?: Record; // (undocumented) readonly iframeNotifyParentOrigin: string | undefined; diff --git a/src/User.test.ts b/src/User.test.ts index 6c1de6b33..7563c44bd 100644 --- a/src/User.test.ts +++ b/src/User.test.ts @@ -3,7 +3,6 @@ import { User } from "./User"; import { Timer } from "./utils"; describe("User", () => { - let now: number; beforeEach(() => { @@ -46,4 +45,60 @@ describe("User", () => { expect(subject.scopes).toEqual(["foo", "bar", "baz"]); }); }); + + describe("extraTokenResponseProperties", () => { + it("should not provide extraTokenResponseProperties if extraTokenResponseKeys is not provided", () => { + // arrange + const args = { + access_token: "notAToken", + id_token: "not", + patient: "12345", // extra + token_type: "Bearer", + extra1: 0, // extra + extra2: null, + extra3: true, + }; + + // act + const subject = new User(args as never); + + // assert + expect(subject.extraTokenResponseProperties).not.toBeDefined(); + }); + + it("should provide extraTokenResponseProperties if extraTokenResponseKeys is provided", () => { + // arrange + const args = { + access_token: "access_token", + id_token: "id_token", + patient: "12345", // extra + token_type: "Bearer", + extra1: 0, // extra + extra2: null, + extra3: true, // extra but not specified + }; + + // act + const subject = new User(args as never, [ + "patient", + "extra1", + "extra2", + ]); + + // assert + expect(subject.extraTokenResponseProperties).toBeDefined(); + const actualTokenProps = subject.extraTokenResponseProperties ?? {}; + const patient = actualTokenProps["patient"]; + expect(patient).toBeDefined(); + expect(patient).toEqual(args.patient); + const extra1 = actualTokenProps["extra1"]; + expect(extra1).toBeDefined(); + expect(extra1).toEqual(args.extra1); + const extra2 = actualTokenProps["extra2"]; + expect(extra2).toBeDefined(); + expect(extra2).toEqual(args.extra2); + const extra3 = actualTokenProps["extra3"]; + expect(extra3).not.toBeDefined(); + }); + }); }); diff --git a/src/User.ts b/src/User.ts index 67a07c9cf..b7796046f 100644 --- a/src/User.ts +++ b/src/User.ts @@ -49,6 +49,12 @@ export class User { /** The expires at returned from the OIDC provider. */ public expires_at?: number; + /** + * The additional token response properties the user has requested to keep via settings. + * @see {@link UserManagerSettings.extraTokenResponseKeys} + */ + public extraTokenResponseProperties?: Record; + /** custom state data set during the initial signin request */ public readonly state: unknown; public readonly url_state?: string; @@ -64,7 +70,7 @@ export class User { expires_at?: number; userState?: unknown; url_state?: string; - }) { + }, extraTokenResponseKeys?: string[]) { this.id_token = args.id_token; this.session_state = args.session_state ?? null; this.access_token = args.access_token; @@ -76,6 +82,16 @@ export class User { this.expires_at = args.expires_at; this.state = args.userState; this.url_state = args.url_state; + + if (extraTokenResponseKeys?.length) { + this.extraTokenResponseProperties = {}; + for (const key of extraTokenResponseKeys) { + const extraProperty = (args as Record)[key]; + if (extraProperty !== undefined) { + this.extraTokenResponseProperties[key] = extraProperty; + } + } + } } /** Computed number of seconds the access token has remaining. */ diff --git a/src/UserManager.ts b/src/UserManager.ts index 500aaf246..58808ca75 100644 --- a/src/UserManager.ts +++ b/src/UserManager.ts @@ -373,7 +373,7 @@ export class UserManager { timeoutInSeconds: this.settings.silentRequestTimeoutInSeconds, ...args, }); - const user = new User({ ...args.state, ...response }); + const user = new User({ ...args.state, ...response }, this.settings.extraTokenResponseKeys); await this.storeUser(user); await this._events.load(user); @@ -548,7 +548,7 @@ export class UserManager { protected async _buildUser(signinResponse: SigninResponse, verifySub?: string) { const logger = this._logger.create("_buildUser"); - const user = new User(signinResponse); + const user = new User(signinResponse, this.settings.extraTokenResponseKeys); if (verifySub) { if (verifySub !== user.profile.sub) { logger.debug("current user does not match user returned from signin. sub from signin:", user.profile.sub); diff --git a/src/UserManagerSettings.test.ts b/src/UserManagerSettings.test.ts index 13e0d11b7..0a022fff3 100644 --- a/src/UserManagerSettings.test.ts +++ b/src/UserManagerSettings.test.ts @@ -421,4 +421,24 @@ describe("UserManagerSettings", () => { expect(subject.silentRequestTimeoutInSeconds).toEqual(10); }); }); + + describe("extraTokenResponseKeys", () => { + it("should return value from the initial settings", () => { + const subject = new UserManagerSettingsStore({ + authority: "authority", + client_id: "client", + redirect_uri: "redirect", + extraTokenResponseKeys: ["testProp"], + }); + expect(subject.extraTokenResponseKeys).toEqual(["testProp"]); + }); + it("should return the default value", () => { + const subject = new UserManagerSettingsStore({ + authority: "authority", + client_id: "client", + redirect_uri: "redirect", + }); + expect(subject.extraTokenResponseKeys).toEqual([]); + }); + }); }); diff --git a/src/UserManagerSettings.ts b/src/UserManagerSettings.ts index 6bc88b017..accc1f774 100644 --- a/src/UserManagerSettings.ts +++ b/src/UserManagerSettings.ts @@ -92,6 +92,13 @@ export interface UserManagerSettings extends OidcClientSettings { */ maxSilentRenewTimeoutRetries?: number; + /** + * By default the user object only preserves the standard properties + * (access_token, session_state, id_token, refresh_token, token_type, scope, profile, and expiration). + * Any additional properties returned by the OIDC/OAuth2 token response will be ignored unless explicitly listed here. (default: []) + */ + extraTokenResponseKeys?: string[]; + /** * Storage object used to persist User for currently authenticated user (default: window.sessionStorage, InMemoryWebStorage iff no window). * E.g. `userStore: new WebStorageStateStore({ store: window.localStorage })` @@ -136,6 +143,8 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore { public readonly accessTokenExpiringNotificationTimeInSeconds: number; public readonly maxSilentRenewTimeoutRetries?: number; + public readonly extraTokenResponseKeys: string[]; + public readonly userStore: StateStore; public constructor(args: UserManagerSettings) { @@ -169,9 +178,10 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore { includeIdTokenInSilentSignout = false, accessTokenExpiringNotificationTimeInSeconds = DefaultAccessTokenExpiringNotificationTimeInSeconds, - maxSilentRenewTimeoutRetries, + extraTokenResponseKeys = [], + userStore, } = args; @@ -207,6 +217,8 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore { this.accessTokenExpiringNotificationTimeInSeconds = accessTokenExpiringNotificationTimeInSeconds; this.maxSilentRenewTimeoutRetries = maxSilentRenewTimeoutRetries; + this.extraTokenResponseKeys = extraTokenResponseKeys; + if (userStore) { this.userStore = userStore; }