oidc-spa 8.0.4 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/angular.d.ts +191 -0
  2. package/angular.js +351 -0
  3. package/angular.js.map +1 -0
  4. package/core/createOidc.d.ts +7 -7
  5. package/core/createOidc.js +15 -2
  6. package/core/createOidc.js.map +1 -1
  7. package/core/oidcClientTsUserToTokens.js +7 -0
  8. package/core/oidcClientTsUserToTokens.js.map +1 -1
  9. package/esm/angular.d.ts +191 -0
  10. package/esm/angular.js +314 -0
  11. package/esm/angular.js.map +1 -0
  12. package/esm/core/createOidc.d.ts +7 -7
  13. package/esm/core/createOidc.js +15 -2
  14. package/esm/core/createOidc.js.map +1 -1
  15. package/esm/core/oidcClientTsUserToTokens.js +7 -0
  16. package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
  17. package/esm/keycloak/keycloakUtils.js.map +1 -1
  18. package/esm/mock/oidc.js +2 -1
  19. package/esm/mock/oidc.js.map +1 -1
  20. package/esm/tools/ConcreteClass.d.ts +3 -0
  21. package/esm/tools/ConcreteClass.js +2 -0
  22. package/esm/tools/ConcreteClass.js.map +1 -0
  23. package/esm/tools/Deferred.d.ts +7 -0
  24. package/esm/tools/Deferred.js +5 -0
  25. package/esm/tools/Deferred.js.map +1 -1
  26. package/esm/tools/INFINITY_TIME.d.ts +1 -0
  27. package/esm/tools/INFINITY_TIME.js +3 -0
  28. package/esm/tools/INFINITY_TIME.js.map +1 -0
  29. package/esm/tools/ReadonlyBehaviorSubject.d.ts +8 -0
  30. package/esm/tools/ReadonlyBehaviorSubject.js +2 -0
  31. package/esm/tools/ReadonlyBehaviorSubject.js.map +1 -0
  32. package/esm/tools/getBaseHref.d.ts +1 -0
  33. package/esm/tools/getBaseHref.js +8 -0
  34. package/esm/tools/getBaseHref.js.map +1 -0
  35. package/esm/tools/readExpirationTimeInJwt.js +4 -0
  36. package/esm/tools/readExpirationTimeInJwt.js.map +1 -1
  37. package/esm/vendor/frontend/tsafe.d.ts +1 -0
  38. package/esm/vendor/frontend/tsafe.js +1 -1
  39. package/keycloak/keycloakUtils.js.map +1 -1
  40. package/mock/oidc.js +2 -1
  41. package/mock/oidc.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/angular.ts +583 -0
  44. package/src/core/createOidc.ts +28 -10
  45. package/src/core/oidcClientTsUserToTokens.ts +9 -0
  46. package/src/keycloak/keycloakUtils.ts +0 -1
  47. package/src/mock/oidc.ts +2 -1
  48. package/src/tools/ConcreteClass.ts +3 -0
  49. package/src/tools/Deferred.ts +10 -0
  50. package/src/tools/INFINITY_TIME.ts +2 -0
  51. package/src/tools/ReadonlyBehaviorSubject.ts +9 -0
  52. package/src/tools/getBaseHref.ts +7 -0
  53. package/src/tools/readExpirationTimeInJwt.ts +5 -0
  54. package/src/vendor/frontend/tsafe.ts +1 -0
  55. package/tools/ConcreteClass.d.ts +3 -0
  56. package/tools/ConcreteClass.js +3 -0
  57. package/tools/ConcreteClass.js.map +1 -0
  58. package/tools/Deferred.d.ts +7 -0
  59. package/tools/Deferred.js +5 -0
  60. package/tools/Deferred.js.map +1 -1
  61. package/tools/INFINITY_TIME.d.ts +1 -0
  62. package/tools/INFINITY_TIME.js +6 -0
  63. package/tools/INFINITY_TIME.js.map +1 -0
  64. package/tools/ReadonlyBehaviorSubject.d.ts +8 -0
  65. package/tools/ReadonlyBehaviorSubject.js +3 -0
  66. package/tools/ReadonlyBehaviorSubject.js.map +1 -0
  67. package/tools/getBaseHref.d.ts +1 -0
  68. package/tools/getBaseHref.js +11 -0
  69. package/tools/getBaseHref.js.map +1 -0
  70. package/tools/readExpirationTimeInJwt.js +4 -0
  71. package/tools/readExpirationTimeInJwt.js.map +1 -1
  72. package/vendor/frontend/tsafe.d.ts +1 -0
  73. package/vendor/frontend/tsafe.js +1 -1
  74. package/angular/angular.d.ts +0 -72
  75. package/angular/angular.js +0 -253
  76. package/angular/angular.js.map +0 -1
  77. package/angular/index.d.ts +0 -1
  78. package/angular/index.js +0 -6
  79. package/angular/index.js.map +0 -1
  80. package/esm/angular/angular.d.ts +0 -72
  81. package/esm/angular/angular.js +0 -249
  82. package/esm/angular/angular.js.map +0 -1
  83. package/esm/angular/index.d.ts +0 -1
  84. package/esm/angular/index.js +0 -2
  85. package/esm/angular/index.js.map +0 -1
  86. package/esm/mock/angular.d.ts +0 -41
  87. package/esm/mock/angular.js +0 -7
  88. package/esm/mock/angular.js.map +0 -1
  89. package/mock/angular.d.ts +0 -41
  90. package/mock/angular.js +0 -10
  91. package/mock/angular.js.map +0 -1
  92. package/src/angular/angular.ts +0 -428
  93. package/src/angular/index.ts +0 -1
  94. package/src/mock/angular.ts +0 -11
package/src/angular.ts ADDED
@@ -0,0 +1,583 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ import type { Oidc, OidcInitializationError, ParamsOfCreateOidc } from "./core";
3
+ import type { OidcMetadata } from "./core/OidcMetadata";
4
+ import { Deferred } from "./tools/Deferred";
5
+ import { assert, type Equals, is } from "./vendor/frontend/tsafe";
6
+ import { createObjectThatThrowsIfAccessed } from "./tools/createObjectThatThrowsIfAccessed";
7
+ import {
8
+ type Signal,
9
+ inject,
10
+ type EnvironmentProviders,
11
+ makeEnvironmentProviders,
12
+ provideAppInitializer
13
+ } from "@angular/core";
14
+ import { toSignal } from "@angular/core/rxjs-interop";
15
+ import type { ReadonlyBehaviorSubject } from "./tools/ReadonlyBehaviorSubject";
16
+ import { Router, type CanActivateFn } from "@angular/router";
17
+ import type { ValueOrAsyncGetter } from "./tools/ValueOrAsyncGetter";
18
+ import { getBaseHref } from "./tools/getBaseHref";
19
+ import type { ConcreteClass } from "./tools/ConcreteClass";
20
+
21
+ export type ParamsOfProvide = {
22
+ issuerUri: string;
23
+ clientId: string;
24
+ /**
25
+ * The scopes being requested from the OIDC/OAuth2 provider (default: `["profile"]`
26
+ * (the scope "openid" is added automatically as it's mandatory)
27
+ **/
28
+ scopes?: string[];
29
+
30
+ /**
31
+ * Transform the url (authorization endpoint) before redirecting to the login pages.
32
+ *
33
+ * The isSilent parameter is true when the redirect is initiated in the background iframe for silent signin.
34
+ * This can be used to omit ui related query parameters (like `ui_locales`).
35
+ */
36
+ transformUrlBeforeRedirect?: (params: { authorizationUrl: string; isSilent: boolean }) => string;
37
+
38
+ /**
39
+ * Extra query params to be added to the authorization endpoint url before redirecting or silent signing in.
40
+ * You can provide a function that returns those extra query params, it will be called
41
+ * when login() is called.
42
+ *
43
+ * Example: extraQueryParams: ()=> ({ ui_locales: "fr" })
44
+ *
45
+ * This parameter can also be passed to login() directly.
46
+ */
47
+ extraQueryParams?:
48
+ | Record<string, string | undefined>
49
+ | ((params: { isSilent: boolean; url: string }) => Record<string, string | undefined>);
50
+ /**
51
+ * Extra body params to be added to the /token POST request.
52
+ *
53
+ * It will be used when for the initial request, whenever the token is getting refreshed and if you call `renewTokens()`.
54
+ * You can also provide this parameter directly to the `renewTokens()` method.
55
+ *
56
+ * It can be either a string to string record or a function that returns a string to string record.
57
+ *
58
+ * Example: extraTokenParams: ()=> ({ selectedCustomer: "xxx" })
59
+ * extraTokenParams: { selectedCustomer: "xxx" }
60
+ */
61
+ extraTokenParams?: Record<string, string | undefined> | (() => Record<string, string | undefined>);
62
+ /**
63
+ * Usage discouraged, it's here because we don't want to assume too much on your
64
+ * usecase but I can't think of a scenario where you would want anything
65
+ * other than the current page.
66
+ *
67
+ * Where to redirect after successful login.
68
+ * Default: window.location.href (here)
69
+ *
70
+ * It does not need to include the origin, eg: "/dashboard"
71
+ *
72
+ * This parameter can also be passed to login() directly as `redirectUrl`.
73
+ */
74
+ postLoginRedirectUrl?: string;
75
+
76
+ /**
77
+ * This parameter defines after how many seconds of inactivity the user should be
78
+ * logged out automatically.
79
+ *
80
+ * WARNING: It should be configured on the identity server side
81
+ * as it's the authoritative source for security policies and not the client.
82
+ * If you don't provide this parameter it will be inferred from the refresh token expiration time.
83
+ * */
84
+ idleSessionLifetimeInSeconds?: number;
85
+
86
+ /**
87
+ * Usage discouraged, this parameter exists because we don't want to assume
88
+ * too much about your usecase but I can't think of a scenario where you would
89
+ * want anything other than the current page.
90
+ *
91
+ * Default: { redirectTo: "current page" }
92
+ */
93
+ autoLogoutParams?: Parameters<Oidc.LoggedIn<any>["logout"]>[0];
94
+ autoLogin?: boolean;
95
+
96
+ /**
97
+ * Default: false
98
+ *
99
+ * See: https://docs.oidc-spa.dev/v/v8/resources/iframe-related-issues
100
+ */
101
+ noIframe?: boolean;
102
+
103
+ debugLogs?: boolean;
104
+
105
+ /**
106
+ * WARNING: This option exists solely as a workaround
107
+ * for limitations in the Google OAuth API.
108
+ * See: https://docs.oidc-spa.dev/providers-configuration/google-oauth
109
+ *
110
+ * Do not use this for other providers.
111
+ * If you think you need a client secret in a SPA, you are likely
112
+ * trying to use a confidential (private) client in the browser,
113
+ * which is insecure and not supported.
114
+ */
115
+ __unsafe_clientSecret?: string;
116
+
117
+ /**
118
+ * WARNING: Setting this to true is a workaround for provider
119
+ * like Google OAuth that don't support JWT access token.
120
+ * Use at your own risk, this is a hack.
121
+ */
122
+ __unsafe_useIdTokenAsAccessToken?: boolean;
123
+
124
+ /**
125
+ * This option should only be used as a last resort.
126
+ *
127
+ * If your OIDC provider is correctly configured, this should not be necessary.
128
+ *
129
+ * The metadata is normally retrieved automatically from:
130
+ * `${issuerUri}/.well-known/openid-configuration`
131
+ *
132
+ * Use this only if that endpoint is not accessible (e.g. due to missing CORS headers
133
+ * or non-standard deployments), and you cannot fix the server-side configuration.
134
+ */
135
+ __metadata?: Partial<OidcMetadata>;
136
+
137
+ /**
138
+ * You can use oidc.$secondsLeftBeforeAutoLogout to display an overlay/update the tab title
139
+ * to indicate to your user that they are going to be logged out if they don't interact
140
+ * with the app.
141
+ * This value let you define how long before how long before auto logout this warning should
142
+ * start showing.
143
+ * Default is 45 seconds.
144
+ */
145
+ autoLogoutWarningDurationSeconds?: number;
146
+ };
147
+
148
+ assert<
149
+ Equals<
150
+ Omit<ParamsOfProvide, "autoLogoutWarningDurationSeconds">,
151
+ Omit<ParamsOfCreateOidc<any, boolean>, "homeUrl" | "decodedIdTokenSchema">
152
+ >
153
+ >;
154
+
155
+ export type ParamsOfProvideMock = {
156
+ mockIssuerUri?: string;
157
+ mockClientId?: string;
158
+ mockAccessToken?: string;
159
+ isUserInitiallyLoggedIn?: boolean;
160
+ };
161
+
162
+ export abstract class AbstractOidcService<
163
+ T_DecodedIdToken extends Record<string, unknown> = Oidc.Tokens.DecodedIdToken_base
164
+ > {
165
+ protected autoLogin: boolean = false;
166
+ protected providerAwaitsInitialization: boolean = true;
167
+ protected decodedIdTokenSchema:
168
+ | {
169
+ parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_base) => T_DecodedIdToken;
170
+ }
171
+ | undefined = undefined;
172
+
173
+ protected mockDecodedIdToken: (() => Promise<T_DecodedIdToken>) | T_DecodedIdToken | undefined =
174
+ undefined;
175
+
176
+ #autoLogoutWarningDurationSeconds = 45;
177
+
178
+ static provide(params: ValueOrAsyncGetter<ParamsOfProvide>): EnvironmentProviders {
179
+ const paramsOrGetParams = params;
180
+
181
+ assert(is<ConcreteClass<typeof AbstractOidcService>>(this));
182
+
183
+ return makeEnvironmentProviders([
184
+ this,
185
+ provideAppInitializer(async () => {
186
+ const instance = inject(this);
187
+
188
+ instance.#initialize({
189
+ prOidcOrInitializationError: (async () => {
190
+ const [{ createOidc }, { autoLogoutWarningDurationSeconds, ...params }] =
191
+ await Promise.all([
192
+ import("./core"),
193
+ typeof paramsOrGetParams === "function"
194
+ ? paramsOrGetParams()
195
+ : paramsOrGetParams
196
+ ]);
197
+
198
+ if (autoLogoutWarningDurationSeconds !== undefined) {
199
+ instance.#autoLogoutWarningDurationSeconds =
200
+ autoLogoutWarningDurationSeconds;
201
+ }
202
+
203
+ try {
204
+ return createOidc({
205
+ homeUrl: getBaseHref(),
206
+ autoLogin: instance.autoLogin,
207
+ decodedIdTokenSchema: instance.decodedIdTokenSchema,
208
+ ...params
209
+ });
210
+ } catch (initializationError) {
211
+ assert(initializationError instanceof Error);
212
+ assert(is<OidcInitializationError>(initializationError));
213
+ return initializationError;
214
+ }
215
+ })()
216
+ });
217
+
218
+ if (instance.providerAwaitsInitialization) {
219
+ await instance.prInitialized;
220
+ }
221
+ })
222
+ ]);
223
+ }
224
+
225
+ static provideMock(params: ParamsOfProvideMock = {}): EnvironmentProviders {
226
+ assert(is<ConcreteClass<typeof AbstractOidcService>>(this));
227
+
228
+ return makeEnvironmentProviders([
229
+ this,
230
+ provideAppInitializer(async () => {
231
+ const instance = inject(this);
232
+
233
+ instance.#initialize({
234
+ prOidcOrInitializationError: (async () => {
235
+ const { createMockOidc } = await import("./mock");
236
+
237
+ return createMockOidc<Record<string, unknown>, boolean>({
238
+ homeUrl: getBaseHref(),
239
+ autoLogin: instance.autoLogin,
240
+ isUserInitiallyLoggedIn: instance.autoLogin
241
+ ? true
242
+ : params.isUserInitiallyLoggedIn,
243
+ mockedParams: {
244
+ issuerUri: params.mockIssuerUri,
245
+ clientId: params.mockClientId
246
+ },
247
+ mockedTokens: {
248
+ accessToken: params.mockAccessToken,
249
+ decodedIdToken: await (() => {
250
+ if (instance.mockDecodedIdToken === undefined) {
251
+ return undefined;
252
+ }
253
+ if (typeof instance.mockDecodedIdToken === "function") {
254
+ return instance.mockDecodedIdToken();
255
+ }
256
+ })()
257
+ }
258
+ });
259
+ })()
260
+ });
261
+
262
+ await instance.prInitialized;
263
+ })
264
+ ]);
265
+ }
266
+
267
+ static enforceLoginGuard() {
268
+ const canActivateFn = (async route => {
269
+ const instance = inject(this);
270
+ const router = inject(Router);
271
+
272
+ await instance.prInitialized;
273
+
274
+ const oidc = instance.#getOidc({ callerName: "enforceLoginGuard" });
275
+
276
+ if (!oidc.isUserLoggedIn) {
277
+ const redirectUrl = router.serializeUrl(
278
+ router.createUrlTree(
279
+ route.url.map(u => u.path),
280
+ {
281
+ queryParams: route.queryParams,
282
+ fragment: route.fragment ?? undefined
283
+ }
284
+ )
285
+ );
286
+
287
+ const doesCurrentHrefRequiresAuth =
288
+ location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
289
+
290
+ await oidc.login({
291
+ doesCurrentHrefRequiresAuth,
292
+ redirectUrl
293
+ });
294
+ }
295
+
296
+ return true;
297
+ }) satisfies CanActivateFn;
298
+ return canActivateFn;
299
+ }
300
+
301
+ #dState = new Deferred<{
302
+ oidc: Oidc<T_DecodedIdToken> | undefined;
303
+ initializationError: OidcInitializationError | undefined;
304
+ }>();
305
+
306
+ readonly prInitialized: Promise<true> = this.#dState.pr.then(() => true);
307
+
308
+ #initialize(params: {
309
+ prOidcOrInitializationError: Promise<Oidc<T_DecodedIdToken> | OidcInitializationError>;
310
+ }): void {
311
+ const { prOidcOrInitializationError } = params;
312
+
313
+ prOidcOrInitializationError.then(oidcOrInitializationError => {
314
+ let initializationError: OidcInitializationError | undefined = undefined;
315
+ let oidc: Oidc<T_DecodedIdToken> | undefined = undefined;
316
+
317
+ if (oidcOrInitializationError instanceof Error) {
318
+ initializationError = oidcOrInitializationError;
319
+ } else {
320
+ oidc = oidcOrInitializationError;
321
+ initializationError = oidc.isUserLoggedIn ? undefined : oidc.initializationError;
322
+ }
323
+
324
+ this.#dState.resolve({
325
+ oidc,
326
+ initializationError
327
+ });
328
+ });
329
+ }
330
+
331
+ #getPrInitializedNotResolvedErrorMessage(params: { callerName: string }) {
332
+ const { callerName } = params;
333
+ return [
334
+ `oidc-spa: ${callerName} called/accessed before`,
335
+ "`oidc.prInitialized` resolved.",
336
+ "You are using `awaitInitialization: false`.",
337
+ "In your template you should wrap your usage of",
338
+ "oidc.isUserLoggedIn, oidc.$decodedIdToken() ect. into",
339
+ "@defer (when oidc.prInitialized | async) { } @placeholder { Loading... }"
340
+ ].join(" ");
341
+ }
342
+
343
+ #getState(params: { callerName: string }) {
344
+ const { callerName } = params;
345
+ const { hasResolved, value } = this.#dState.getState();
346
+ if (!hasResolved) {
347
+ throw new Error(this.#getPrInitializedNotResolvedErrorMessage({ callerName }));
348
+ }
349
+ return value;
350
+ }
351
+
352
+ get initializationError(): OidcInitializationError | undefined {
353
+ const state = this.#getState({ callerName: "initializationError" });
354
+ return state.initializationError;
355
+ }
356
+
357
+ #getAutoLoginAndInitializationErrorAccessErrorMessage(params: { callerName: string }) {
358
+ const { callerName } = params;
359
+
360
+ return [
361
+ `oidc-spa: ${callerName} was accessed but initialization failed.`,
362
+ "You are using `autoLogin: true`, so there is no anonymous state.",
363
+ "Handle this by gating your UI:",
364
+ "if (oidc.initializationError) show an error/fallback."
365
+ ].join(" ");
366
+ }
367
+
368
+ #getOidc(params: { callerName: string }) {
369
+ const { callerName } = params;
370
+ const state = this.#getState({ callerName });
371
+ if (state.oidc === undefined) {
372
+ // initialization failed
373
+ assert(state.initializationError !== undefined);
374
+ throw new Error(this.#getAutoLoginAndInitializationErrorAccessErrorMessage({ callerName }));
375
+ }
376
+ return state.oidc;
377
+ }
378
+
379
+ get issuerUri() {
380
+ return this.#getOidc({ callerName: "issuerUri" }).params.issuerUri;
381
+ }
382
+
383
+ get clientId() {
384
+ return this.#getOidc({ callerName: "clientId" }).params.clientId;
385
+ }
386
+
387
+ get isUserLoggedIn() {
388
+ return this.#getOidc({ callerName: "isUserLoggedIn" }).isUserLoggedIn;
389
+ }
390
+
391
+ async login(params?: {
392
+ /**
393
+ * Add extra query parameters to the url before redirecting to the login pages.
394
+ */
395
+ extraQueryParams?: Record<string, string | undefined>;
396
+ /**
397
+ * Where to redirect after successful login.
398
+ * Default: window.location.href (here)
399
+ *
400
+ * It does not need to include the origin, eg: "/dashboard"
401
+ */
402
+ redirectUrl?: string;
403
+
404
+ /**
405
+ * Transform the url before redirecting to the login pages.
406
+ * Prefer using the extraQueryParams parameter if you're only adding query parameters.
407
+ */
408
+ transformUrlBeforeRedirect?: (url: string) => string;
409
+ }): Promise<never> {
410
+ await this.prInitialized;
411
+
412
+ const oidc = this.#getOidc({ callerName: "login" });
413
+
414
+ if (oidc.isUserLoggedIn) {
415
+ throw new Error(
416
+ [
417
+ "oidc-spa: login() called but the user is already logged in.",
418
+ "If you wish to send the user to the login page for some update",
419
+ "use oidc.goToAuthServer() instead"
420
+ ].join(" ")
421
+ );
422
+ }
423
+
424
+ return oidc.login({
425
+ ...params,
426
+ doesCurrentHrefRequiresAuth: false
427
+ });
428
+ }
429
+
430
+ async renewTokens(params?: {
431
+ extraTokenParams?: Record<string, string | undefined>;
432
+ }): Promise<void> {
433
+ await this.prInitialized;
434
+
435
+ const oidc = this.#getOidc({ callerName: "renewTokens" });
436
+
437
+ if (!oidc.isUserLoggedIn) {
438
+ throw new Error("oidc-spa: renewTokens() called but the user is not logged in.");
439
+ }
440
+
441
+ return oidc.renewTokens(params);
442
+ }
443
+
444
+ async logout(
445
+ params: { redirectTo: "home" | "current page" } | { redirectTo: "specific url"; url: string }
446
+ ): Promise<never> {
447
+ await this.prInitialized;
448
+
449
+ const oidc = this.#getOidc({ callerName: "logout" });
450
+
451
+ if (!oidc.isUserLoggedIn) {
452
+ throw new Error("oidc-spa: logout() called but the user is not logged in.");
453
+ }
454
+
455
+ return oidc.logout(params);
456
+ }
457
+
458
+ async goToAuthServer(params: {
459
+ extraQueryParams?: Record<string, string | undefined>;
460
+ redirectUrl?: string;
461
+ transformUrlBeforeRedirect?: (url: string) => string;
462
+ }): Promise<never> {
463
+ await this.prInitialized;
464
+
465
+ const oidc = this.#getOidc({ callerName: "goToAuthServer" });
466
+
467
+ if (!oidc.isUserLoggedIn) {
468
+ throw new Error("oidc-spa: goToAuthServer() called but the user is not logged in.");
469
+ }
470
+
471
+ return oidc.goToAuthServer(params);
472
+ }
473
+
474
+ readonly decodedIdToken$: ReadonlyBehaviorSubject<T_DecodedIdToken> = (() => {
475
+ const decodedIdToken$ = new BehaviorSubject<T_DecodedIdToken>(
476
+ createObjectThatThrowsIfAccessed({
477
+ debugMessage: this.#getPrInitializedNotResolvedErrorMessage({
478
+ callerName: "decodedIdToken"
479
+ })
480
+ })
481
+ );
482
+
483
+ (async () => {
484
+ const { initializationError, oidc } = await this.#dState.pr;
485
+
486
+ if (initializationError !== undefined) {
487
+ decodedIdToken$.next(
488
+ createObjectThatThrowsIfAccessed({
489
+ debugMessage: this.#getAutoLoginAndInitializationErrorAccessErrorMessage({
490
+ callerName: "decodedIdToken"
491
+ })
492
+ })
493
+ );
494
+ return;
495
+ }
496
+
497
+ assert(oidc !== undefined);
498
+
499
+ if (!oidc.isUserLoggedIn) {
500
+ decodedIdToken$.next(
501
+ createObjectThatThrowsIfAccessed({
502
+ debugMessage: [
503
+ `oidc-spa: Trying to read properties of decodedIdToken, the user`,
504
+ `isn't currently logged in, this does not make sense.`,
505
+ `You are responsible for controlling the flow of your app and`,
506
+ `not try to read the decodedIdToken when oidc.isUserLoggedIn is false.`
507
+ ].join(" ")
508
+ })
509
+ );
510
+ return;
511
+ }
512
+
513
+ decodedIdToken$.next(oidc.getDecodedIdToken());
514
+
515
+ oidc.subscribeToTokensChange(() => {
516
+ const value_new = oidc.getDecodedIdToken();
517
+ const value_current = decodedIdToken$.getValue();
518
+
519
+ if (value_new === value_current) {
520
+ return;
521
+ }
522
+
523
+ decodedIdToken$.next(value_new);
524
+ });
525
+ })();
526
+
527
+ return decodedIdToken$;
528
+ })();
529
+
530
+ readonly $decodedIdToken = toSignal(this.decodedIdToken$, { requireSync: true });
531
+
532
+ async getAccessToken(): Promise<
533
+ { isUserLoggedIn: false; accessToken?: never } | { isUserLoggedIn: true; accessToken: string }
534
+ > {
535
+ await this.prInitialized;
536
+
537
+ const oidc = this.#getOidc({ callerName: "getAccessToken" });
538
+
539
+ return oidc.isUserLoggedIn
540
+ ? { isUserLoggedIn: true, accessToken: (await oidc.getTokens()).accessToken }
541
+ : {
542
+ isUserLoggedIn: false
543
+ };
544
+ }
545
+
546
+ readonly $secondsLeftBeforeAutoLogout: Signal<number | null> = (() => {
547
+ const secondsLeftBeforeAutoLogout$ = new BehaviorSubject<number | null>(null);
548
+
549
+ (async () => {
550
+ const { oidc } = await this.#dState.pr;
551
+
552
+ if (oidc === undefined) {
553
+ return;
554
+ }
555
+
556
+ if (!oidc.isUserLoggedIn) {
557
+ return;
558
+ }
559
+
560
+ oidc.subscribeToAutoLogoutCountdown(({ secondsLeft }) => {
561
+ if (secondsLeft === undefined || secondsLeft > this.#autoLogoutWarningDurationSeconds) {
562
+ if (secondsLeftBeforeAutoLogout$.getValue() !== null) {
563
+ secondsLeftBeforeAutoLogout$.next(null);
564
+ }
565
+ return;
566
+ }
567
+ secondsLeftBeforeAutoLogout$.next(secondsLeft);
568
+ });
569
+ })();
570
+
571
+ return toSignal(secondsLeftBeforeAutoLogout$, { requireSync: true });
572
+ })();
573
+
574
+ get isNewBrowserSession() {
575
+ const oidc = this.#getOidc({ callerName: "isNewBrowserSession" });
576
+
577
+ if (!oidc.isUserLoggedIn) {
578
+ throw new Error("oidc-spa: isNewBrowserSession was used but the used is not logged in");
579
+ }
580
+
581
+ return oidc.isNewBrowserSession;
582
+ }
583
+ }
@@ -47,6 +47,7 @@ import {
47
47
  import { createGetIsNewBrowserSession } from "./isNewBrowserSession";
48
48
  import { getIsOnline } from "../tools/getIsOnline";
49
49
  import { isKeycloak } from "../keycloak/isKeycloak";
50
+ import { INFINITY_TIME } from "../tools/INFINITY_TIME";
50
51
 
51
52
  // NOTE: Replaced at build time
52
53
  const VERSION = "{{OIDC_SPA_VERSION}}";
@@ -55,6 +56,14 @@ export type ParamsOfCreateOidc<
55
56
  DecodedIdToken extends Record<string, unknown> = Oidc.Tokens.DecodedIdToken_base,
56
57
  AutoLogin extends boolean = false
57
58
  > = {
59
+ /**
60
+ * What should you put in this parameter?
61
+ * - Vite project: `BASE_URL: import.meta.env.BASE_URL`
62
+ * - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
63
+ * - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
64
+ */
65
+ homeUrl: string;
66
+
58
67
  issuerUri: string;
59
68
  clientId: string;
60
69
  /**
@@ -109,14 +118,6 @@ export type ParamsOfCreateOidc<
109
118
  */
110
119
  postLoginRedirectUrl?: string;
111
120
 
112
- /**
113
- * What should you put in this parameter?
114
- * - Vite project: `BASE_URL: import.meta.env.BASE_URL`
115
- * - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
116
- * - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
117
- */
118
- homeUrl: string;
119
-
120
121
  decodedIdTokenSchema?: {
121
122
  parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_base) => DecodedIdToken;
122
123
  };
@@ -1502,6 +1503,14 @@ export async function createOidc_nonMemoized<
1502
1503
  return;
1503
1504
  }
1504
1505
 
1506
+ if (
1507
+ currentTokens.refreshTokenExpirationTime !== undefined &&
1508
+ currentTokens.refreshTokenExpirationTime >= INFINITY_TIME
1509
+ ) {
1510
+ log?.("The refresh_token never expires, disabling auto-renewal mechanism");
1511
+ return;
1512
+ }
1513
+
1505
1514
  const msBeforeExpiration =
1506
1515
  (currentTokens.refreshTokenExpirationTime ?? currentTokens.accessTokenExpirationTime) -
1507
1516
  currentTokens.getServerDateNow();
@@ -1612,9 +1621,18 @@ export async function createOidc_nonMemoized<
1612
1621
  return undefined;
1613
1622
  }
1614
1623
 
1624
+ if (currentTokens.refreshTokenExpirationTime >= INFINITY_TIME) {
1625
+ return 0;
1626
+ }
1627
+
1615
1628
  return (currentTokens.refreshTokenExpirationTime - currentTokens.issuedAtTime) / 1000;
1616
1629
  };
1617
1630
 
1631
+ if (getCurrentRefreshTokenTtlInSeconds() === 0) {
1632
+ log?.("The refresh_token never expires, disabling auto logout mechanism");
1633
+ break auto_logout;
1634
+ }
1635
+
1618
1636
  if (getCurrentRefreshTokenTtlInSeconds() === undefined) {
1619
1637
  log?.(
1620
1638
  `${
@@ -1635,8 +1653,6 @@ export async function createOidc_nonMemoized<
1635
1653
  );
1636
1654
  };
1637
1655
 
1638
- invokeAllCallbacks({ secondsLeft });
1639
-
1640
1656
  if (secondsLeft === 0) {
1641
1657
  cancel_if_offline: {
1642
1658
  const { isOnline, prOnline } = getIsOnline();
@@ -1675,6 +1691,8 @@ export async function createOidc_nonMemoized<
1675
1691
 
1676
1692
  await oidc_loggedIn.logout(autoLogoutParams);
1677
1693
  }
1694
+
1695
+ invokeAllCallbacks({ secondsLeft });
1678
1696
  }
1679
1697
  });
1680
1698
 
@@ -3,6 +3,7 @@ import { assert, id } from "../vendor/frontend/tsafe";
3
3
  import { readExpirationTimeInJwt } from "../tools/readExpirationTimeInJwt";
4
4
  import { decodeJwt } from "../tools/decodeJwt";
5
5
  import type { Oidc } from "./Oidc";
6
+ import { INFINITY_TIME } from "../tools/INFINITY_TIME";
6
7
 
7
8
  export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, unknown>>(params: {
8
9
  oidcClientTsUser: OidcClientTsUser;
@@ -187,6 +188,10 @@ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, u
187
188
 
188
189
  assert(typeof refresh_expires_at === "number", "2033392");
189
190
 
191
+ if (refresh_expires_at === 0) {
192
+ return INFINITY_TIME;
193
+ }
194
+
190
195
  return refresh_expires_at * 1000;
191
196
  }
192
197
 
@@ -199,6 +204,10 @@ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, u
199
204
 
200
205
  assert(typeof refresh_expires_in === "number", "2033425330");
201
206
 
207
+ if (refresh_expires_in === 0) {
208
+ return INFINITY_TIME;
209
+ }
210
+
202
211
  return issuedAtTime + refresh_expires_in * 1000;
203
212
  }
204
213