@axa-fr/oidc-client 7.27.16 → 7.27.18

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.
package/src/oidc.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  import { tryKeepSessionAsync } from './keepSession';
13
13
  import { ILOidcLocation, OidcLocation } from './location';
14
14
  import { defaultLoginAsync, loginCallbackAsync } from './login.js';
15
- import { destroyAsync, logoutAsync } from './logout.js';
15
+ import { clearSessionAsync, destroyAsync, logoutAsync } from './logout.js';
16
16
  import { TokenRenewMode, Tokens } from './parseTokens.js';
17
17
  import { autoRenewTokens, renewTokensAndStartTimerAsync } from './renewTokens.js';
18
18
  import { fetchFromIssuer } from './requests.js';
@@ -99,6 +99,14 @@ export class Oidc {
99
99
  public checkSessionIFrame: CheckSessionIFrame;
100
100
  public getFetch: () => Fetch;
101
101
  public location: ILOidcLocation;
102
+ /**
103
+ * `true` while {@link logoutAsync} is executing or has scheduled a
104
+ * navigation to the identity provider's end-session endpoint that has not
105
+ * yet committed. Consumers (UI guards, silent-renew handlers, 401 retry
106
+ * interceptors, …) should check this flag and skip starting a new auth
107
+ * flow when it is set, even if `tokens` is null.
108
+ */
109
+ public isLoggingOut = false;
102
110
  constructor(
103
111
  configuration: OidcConfiguration,
104
112
  configurationName = 'default',
@@ -477,6 +485,27 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
477
485
  return await destroyAsync(this)(status);
478
486
  }
479
487
 
488
+ /**
489
+ * Drops the local OIDC session (tokens, user info, service-worker storage)
490
+ * and broadcasts `logout_from_same_tab`, without contacting the identity
491
+ * provider's `end_session_endpoint` and without revoking tokens.
492
+ *
493
+ * Use this for SPA-only logouts, service-worker-only flows, or
494
+ * error-recovery paths where a full IdP logout is not needed or not
495
+ * desirable. For a standard OIDC RP-initiated logout use
496
+ * {@link logoutAsync} instead.
497
+ */
498
+ clearSessionPromise: Promise<void> = null;
499
+ async clearSessionAsync(): Promise<void> {
500
+ if (this.clearSessionPromise) {
501
+ return this.clearSessionPromise;
502
+ }
503
+ this.clearSessionPromise = clearSessionAsync(this, oidcDatabase)();
504
+ return this.clearSessionPromise.finally(() => {
505
+ this.clearSessionPromise = null;
506
+ });
507
+ }
508
+
480
509
  async logoutSameTabAsync(clientId: string, sub: any) {
481
510
  // @ts-ignore
482
511
  if (
package/src/oidcClient.ts CHANGED
@@ -84,6 +84,32 @@ export class OidcClient {
84
84
  return this._oidc.logoutAsync(callbackPathOrUrl, extras);
85
85
  }
86
86
 
87
+ /**
88
+ * Drops the local OIDC session (tokens, user info, service-worker storage)
89
+ * and notifies same-tab listeners via the `logout_from_same_tab` event,
90
+ * without contacting the identity provider's `end_session_endpoint` and
91
+ * without revoking tokens.
92
+ *
93
+ * Use this for SPA-only logouts, service-worker-only flows, or
94
+ * error-recovery paths. For a standard OIDC RP-initiated logout (with
95
+ * token revocation and navigation to the IdP's end-session endpoint) use
96
+ * {@link logoutAsync} instead.
97
+ */
98
+ clearSessionAsync(): Promise<void> {
99
+ return this._oidc.clearSessionAsync();
100
+ }
101
+
102
+ /**
103
+ * `true` while a logout flow is in progress: between the moment
104
+ * {@link logoutAsync} starts and the moment the browser navigates away to
105
+ * the identity provider's end-session endpoint. UI guards and silent-renew
106
+ * handlers should check this flag to avoid kicking off a new auth flow
107
+ * during that window.
108
+ */
109
+ get isLoggingOut(): boolean {
110
+ return this._oidc.isLoggingOut === true;
111
+ }
112
+
87
113
  silentLoginCallbackAsync(): Promise<void> {
88
114
  return this._oidc.silentLoginCallbackAsync();
89
115
  }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { isOidcStateError, OidcStateError, OidcStateErrorCode } from './oidcStateError';
4
+
5
+ describe('OidcStateError', () => {
6
+ it('exposes the well-known state error codes', () => {
7
+ expect(OidcStateErrorCode.STATE_MISSING).toBe('STATE_MISSING');
8
+ expect(OidcStateErrorCode.STATE_MISMATCH).toBe('STATE_MISMATCH');
9
+ expect(OidcStateErrorCode.NONCE_MISSING).toBe('NONCE_MISSING');
10
+ });
11
+
12
+ it('is an Error subclass with name "OidcStateError"', () => {
13
+ const err = new OidcStateError(OidcStateErrorCode.STATE_MISSING, 'missing');
14
+ expect(err).toBeInstanceOf(Error);
15
+ expect(err).toBeInstanceOf(OidcStateError);
16
+ expect(err.name).toBe('OidcStateError');
17
+ });
18
+
19
+ it('preserves the code and message passed to the constructor', () => {
20
+ const err = new OidcStateError(OidcStateErrorCode.STATE_MISMATCH, 'mismatch happened');
21
+ expect(err.code).toBe('STATE_MISMATCH');
22
+ expect(err.message).toBe('mismatch happened');
23
+ });
24
+
25
+ it('is detectable via isOidcStateError', () => {
26
+ const err = new OidcStateError(OidcStateErrorCode.NONCE_MISSING, 'nonce missing');
27
+ expect(isOidcStateError(err)).toBe(true);
28
+ expect(isOidcStateError(new Error('boom'))).toBe(false);
29
+ expect(isOidcStateError(null)).toBe(false);
30
+ expect(isOidcStateError(undefined)).toBe(false);
31
+ expect(isOidcStateError({ code: 'STATE_MISSING' })).toBe(false);
32
+ });
33
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Stable, machine-readable codes for OIDC state / nonce failures occurring
3
+ * between the authorization redirect and the callback handling.
4
+ *
5
+ * These codes let consumers react to specific failure modes without having to
6
+ * pattern-match against error message strings.
7
+ */
8
+ export const OidcStateErrorCode = {
9
+ /** No state was found in storage when handling the callback. */
10
+ STATE_MISSING: 'STATE_MISSING',
11
+ /** The state returned by the server does not match the stored one. */
12
+ STATE_MISMATCH: 'STATE_MISMATCH',
13
+ /** No nonce was found in storage when handling the callback / renewal. */
14
+ NONCE_MISSING: 'NONCE_MISSING',
15
+ } as const;
16
+
17
+ // Companion type that mirrors the const above. This is the standard TS
18
+ // "string-enum-like" pattern; we intentionally reuse the same name so that
19
+ // `OidcStateErrorCode` works as both a value namespace and a type for
20
+ // consumers.
21
+ // eslint-disable-next-line @typescript-eslint/no-redeclare
22
+ export type OidcStateErrorCode = (typeof OidcStateErrorCode)[keyof typeof OidcStateErrorCode];
23
+
24
+ /**
25
+ * Typed error thrown when the OIDC login state or nonce is missing,
26
+ * corrupted, or does not match the value returned by the authorization server.
27
+ *
28
+ * Consumers can use `instanceof OidcStateError` and inspect `code` instead of
29
+ * relying on the (unstable) error message text.
30
+ */
31
+ export class OidcStateError extends Error {
32
+ readonly code: OidcStateErrorCode;
33
+
34
+ constructor(code: OidcStateErrorCode, message: string) {
35
+ super(message);
36
+ this.name = 'OidcStateError';
37
+ this.code = code;
38
+
39
+ // Keep prototype chain intact when transpiled to ES5.
40
+ Object.setPrototypeOf(this, OidcStateError.prototype);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Type guard for {@link OidcStateError}. Useful in callers that want to react
46
+ * specifically to state/nonce failures.
47
+ */
48
+ export const isOidcStateError = (value: unknown): value is OidcStateError => {
49
+ return value instanceof OidcStateError;
50
+ };
@@ -446,6 +446,19 @@ const synchroniseTokensAsync =
446
446
  );
447
447
 
448
448
  if (tokenResponse.success) {
449
+ // Guard against a missing/corrupted nonce reaching id_token validation.
450
+ // Without this guard, accessing `nonce.nonce` would throw a TypeError
451
+ // when the underlying storage has been cleared (private mode, manual
452
+ // clearing, browser eviction). We prefer a defined SESSION_LOST result
453
+ // so silent renew stays non-throwing for consumers.
454
+ // See https://github.com/AxaFrance/oidc-client/issues/1678
455
+ if (!nonce || !nonce.nonce) {
456
+ updateTokens(null);
457
+ oidc.publishEvent(eventNames.refreshTokensAsync_error, {
458
+ message: 'refresh token: nonce missing from storage',
459
+ });
460
+ return { tokens: null, status: 'SESSION_LOST' };
461
+ }
449
462
  const { isValid, reason } = isTokensOidcValid(
450
463
  tokenResponse.data,
451
464
  nonce.nonce,
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export default '7.27.16';
1
+ export default '7.27.18';