Skip to content

Commit 7c06c1f

Browse files
benMainpamapa
authored andcommitted
feat(extraTokenProperties): Add extra token properties
1 parent ea395bf commit 7c06c1f

6 files changed

Lines changed: 113 additions & 6 deletions

File tree

docs/oidc-client-ts.api.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,12 +969,13 @@ export class User {
969969
expires_at?: number;
970970
userState?: unknown;
971971
url_state?: string;
972-
});
972+
}, extraTokenResponseKeys?: string[]);
973973
access_token: string;
974974
get expired(): boolean | undefined;
975975
expires_at?: number;
976976
get expires_in(): number | undefined;
977977
set expires_in(value: number | undefined);
978+
extraTokenResponseProperties?: Record<string, unknown>;
978979
// (undocumented)
979980
static fromStorageString(storageString: string): User;
980981
id_token?: string;
@@ -1122,6 +1123,7 @@ export interface UserManagerSettings extends OidcClientSettings {
11221123
accessTokenExpiringNotificationTimeInSeconds?: number;
11231124
automaticSilentRenew?: boolean;
11241125
checkSessionIntervalInSeconds?: number;
1126+
extraTokenResponseKeys?: string[];
11251127
iframeAttributes?: Record<string, string> | undefined;
11261128
iframeNotifyParentOrigin?: string;
11271129
iframeScriptOrigin?: string;
@@ -1160,6 +1162,8 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
11601162
// (undocumented)
11611163
readonly checkSessionIntervalInSeconds: number;
11621164
// (undocumented)
1165+
readonly extraTokenResponseKeys: string[];
1166+
// (undocumented)
11631167
readonly iframeAttributes?: Record<string, string>;
11641168
// (undocumented)
11651169
readonly iframeNotifyParentOrigin: string | undefined;

src/User.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { User } from "./User";
33
import { Timer } from "./utils";
44

55
describe("User", () => {
6-
76
let now: number;
87

98
beforeEach(() => {
@@ -46,4 +45,60 @@ describe("User", () => {
4645
expect(subject.scopes).toEqual(["foo", "bar", "baz"]);
4746
});
4847
});
48+
49+
describe("extraTokenResponseProperties", () => {
50+
it("should not provide extraTokenResponseProperties if extraTokenResponseKeys is not provided", () => {
51+
// arrange
52+
const args = {
53+
access_token: "notAToken",
54+
id_token: "not",
55+
patient: "12345", // extra
56+
token_type: "Bearer",
57+
extra1: 0, // extra
58+
extra2: null,
59+
extra3: true,
60+
};
61+
62+
// act
63+
const subject = new User(args as never);
64+
65+
// assert
66+
expect(subject.extraTokenResponseProperties).not.toBeDefined();
67+
});
68+
69+
it("should provide extraTokenResponseProperties if extraTokenResponseKeys is provided", () => {
70+
// arrange
71+
const args = {
72+
access_token: "access_token",
73+
id_token: "id_token",
74+
patient: "12345", // extra
75+
token_type: "Bearer",
76+
extra1: 0, // extra
77+
extra2: null,
78+
extra3: true, // extra but not specified
79+
};
80+
81+
// act
82+
const subject = new User(args as never, [
83+
"patient",
84+
"extra1",
85+
"extra2",
86+
]);
87+
88+
// assert
89+
expect(subject.extraTokenResponseProperties).toBeDefined();
90+
const actualTokenProps = subject.extraTokenResponseProperties ?? {};
91+
const patient = actualTokenProps["patient"];
92+
expect(patient).toBeDefined();
93+
expect(patient).toEqual(args.patient);
94+
const extra1 = actualTokenProps["extra1"];
95+
expect(extra1).toBeDefined();
96+
expect(extra1).toEqual(args.extra1);
97+
const extra2 = actualTokenProps["extra2"];
98+
expect(extra2).toBeDefined();
99+
expect(extra2).toEqual(args.extra2);
100+
const extra3 = actualTokenProps["extra3"];
101+
expect(extra3).not.toBeDefined();
102+
});
103+
});
49104
});

src/User.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ export class User {
4949
/** The expires at returned from the OIDC provider. */
5050
public expires_at?: number;
5151

52+
/**
53+
* The additional token response properties the user has requested to keep via settings.
54+
* @see {@link UserManagerSettings.extraTokenResponseKeys}
55+
*/
56+
public extraTokenResponseProperties?: Record<string, unknown>;
57+
5258
/** custom state data set during the initial signin request */
5359
public readonly state: unknown;
5460
public readonly url_state?: string;
@@ -64,7 +70,7 @@ export class User {
6470
expires_at?: number;
6571
userState?: unknown;
6672
url_state?: string;
67-
}) {
73+
}, extraTokenResponseKeys?: string[]) {
6874
this.id_token = args.id_token;
6975
this.session_state = args.session_state ?? null;
7076
this.access_token = args.access_token;
@@ -76,6 +82,16 @@ export class User {
7682
this.expires_at = args.expires_at;
7783
this.state = args.userState;
7884
this.url_state = args.url_state;
85+
86+
if (extraTokenResponseKeys?.length) {
87+
this.extraTokenResponseProperties = {};
88+
for (const key of extraTokenResponseKeys) {
89+
const extraProperty = (args as Record<string, unknown>)[key];
90+
if (extraProperty !== undefined) {
91+
this.extraTokenResponseProperties[key] = extraProperty;
92+
}
93+
}
94+
}
7995
}
8096

8197
/** Computed number of seconds the access token has remaining. */

src/UserManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ export class UserManager {
373373
timeoutInSeconds: this.settings.silentRequestTimeoutInSeconds,
374374
...args,
375375
});
376-
const user = new User({ ...args.state, ...response });
376+
const user = new User({ ...args.state, ...response }, this.settings.extraTokenResponseKeys);
377377

378378
await this.storeUser(user);
379379
await this._events.load(user);
@@ -548,7 +548,7 @@ export class UserManager {
548548

549549
protected async _buildUser(signinResponse: SigninResponse, verifySub?: string) {
550550
const logger = this._logger.create("_buildUser");
551-
const user = new User(signinResponse);
551+
const user = new User(signinResponse, this.settings.extraTokenResponseKeys);
552552
if (verifySub) {
553553
if (verifySub !== user.profile.sub) {
554554
logger.debug("current user does not match user returned from signin. sub from signin:", user.profile.sub);

src/UserManagerSettings.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,24 @@ describe("UserManagerSettings", () => {
421421
expect(subject.silentRequestTimeoutInSeconds).toEqual(10);
422422
});
423423
});
424+
425+
describe("extraTokenResponseKeys", () => {
426+
it("should return value from the initial settings", () => {
427+
const subject = new UserManagerSettingsStore({
428+
authority: "authority",
429+
client_id: "client",
430+
redirect_uri: "redirect",
431+
extraTokenResponseKeys: ["testProp"],
432+
});
433+
expect(subject.extraTokenResponseKeys).toEqual(["testProp"]);
434+
});
435+
it("should return the default value", () => {
436+
const subject = new UserManagerSettingsStore({
437+
authority: "authority",
438+
client_id: "client",
439+
redirect_uri: "redirect",
440+
});
441+
expect(subject.extraTokenResponseKeys).toEqual([]);
442+
});
443+
});
424444
});

src/UserManagerSettings.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ export interface UserManagerSettings extends OidcClientSettings {
9292
*/
9393
maxSilentRenewTimeoutRetries?: number;
9494

95+
/**
96+
* By default the user object only preserves the standard properties
97+
* (access_token, session_state, id_token, refresh_token, token_type, scope, profile, and expiration).
98+
* Any additional properties returned by the OIDC/OAuth2 token response will be ignored unless explicitly listed here. (default: [])
99+
*/
100+
extraTokenResponseKeys?: string[];
101+
95102
/**
96103
* Storage object used to persist User for currently authenticated user (default: window.sessionStorage, InMemoryWebStorage iff no window).
97104
* E.g. `userStore: new WebStorageStateStore({ store: window.localStorage })`
@@ -136,6 +143,8 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
136143
public readonly accessTokenExpiringNotificationTimeInSeconds: number;
137144
public readonly maxSilentRenewTimeoutRetries?: number;
138145

146+
public readonly extraTokenResponseKeys: string[];
147+
139148
public readonly userStore: StateStore;
140149

141150
public constructor(args: UserManagerSettings) {
@@ -169,9 +178,10 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
169178
includeIdTokenInSilentSignout = false,
170179

171180
accessTokenExpiringNotificationTimeInSeconds = DefaultAccessTokenExpiringNotificationTimeInSeconds,
172-
173181
maxSilentRenewTimeoutRetries,
174182

183+
extraTokenResponseKeys = [],
184+
175185
userStore,
176186
} = args;
177187

@@ -207,6 +217,8 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
207217
this.accessTokenExpiringNotificationTimeInSeconds = accessTokenExpiringNotificationTimeInSeconds;
208218
this.maxSilentRenewTimeoutRetries = maxSilentRenewTimeoutRetries;
209219

220+
this.extraTokenResponseKeys = extraTokenResponseKeys;
221+
210222
if (userStore) {
211223
this.userStore = userStore;
212224
}

0 commit comments

Comments
 (0)