@svadmin/sso 0.11.0 → 0.11.2

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.
@@ -17,6 +17,8 @@ export interface SSOConfig {
17
17
  scopes?: string[];
18
18
  /** Where to redirect after logout */
19
19
  postLogoutRedirectUri?: string;
20
+ /** Optional public RP-initiated logout endpoint override. */
21
+ endSessionEndpoint?: string;
20
22
  /** Token storage backend. Default: 'local' (localStorage) */
21
23
  storage?: 'local' | 'session' | TokenStorage;
22
24
  /** Storage namespace. Defaults to an issuer/client-specific key. */
@@ -8,6 +8,35 @@ import { createAuthenticatedFetch as buildAuthenticatedFetch } from './authentic
8
8
  import { createSSOAuthNetworkError, createSSOAuthResponseError, isTerminalSessionError, SSOAuthError, } from './errors.js';
9
9
  import { createSessionManager, } from './session-manager.js';
10
10
  const DEFAULT_STORAGE_KEY = 'svadmin_sso';
11
+ const REQUIRED_OIDC_ENDPOINTS = [
12
+ 'authorization_endpoint',
13
+ 'token_endpoint',
14
+ 'userinfo_endpoint',
15
+ ];
16
+ function readOIDCConfig(body) {
17
+ const discovery = typeof body === 'object' && body !== null && !Array.isArray(body)
18
+ ? body
19
+ : {};
20
+ const missingEndpoints = REQUIRED_OIDC_ENDPOINTS.filter((endpoint) => {
21
+ const value = discovery[endpoint];
22
+ return typeof value !== 'string' || value.length === 0;
23
+ });
24
+ if (missingEndpoints.length > 0) {
25
+ throw new SSOAuthError(`OIDC discovery returned incomplete endpoints: missing ${missingEndpoints.join(', ')}`, 502, {
26
+ code: 'invalid_discovery_document',
27
+ body,
28
+ });
29
+ }
30
+ return {
31
+ authorization_endpoint: discovery.authorization_endpoint,
32
+ token_endpoint: discovery.token_endpoint,
33
+ userinfo_endpoint: discovery.userinfo_endpoint,
34
+ ...(typeof discovery.end_session_endpoint === 'string'
35
+ && discovery.end_session_endpoint.length > 0
36
+ ? { end_session_endpoint: discovery.end_session_endpoint }
37
+ : {}),
38
+ };
39
+ }
11
40
  function decodeJwtPayload(token) {
12
41
  if (!token)
13
42
  return null;
@@ -181,7 +210,10 @@ export function createSSOAuthProvider(config) {
181
210
  if (oidcConfig)
182
211
  return oidcConfig;
183
212
  if (config.manualEndpoints) {
184
- oidcConfig = config.manualEndpoints;
213
+ oidcConfig = {
214
+ ...config.manualEndpoints,
215
+ ...(config.endSessionEndpoint ? { end_session_endpoint: config.endSessionEndpoint } : {}),
216
+ };
185
217
  return oidcConfig;
186
218
  }
187
219
  const url = `${config.issuer.replace(/\/$/, '')}/.well-known/openid-configuration`;
@@ -205,15 +237,10 @@ export function createSSOAuthProvider(config) {
205
237
  cause: error,
206
238
  });
207
239
  }
208
- if (typeof discovered.authorization_endpoint !== 'string'
209
- || typeof discovered.token_endpoint !== 'string'
210
- || typeof discovered.userinfo_endpoint !== 'string') {
211
- throw new SSOAuthError('OIDC discovery returned incomplete endpoints', 502, {
212
- code: 'invalid_discovery_document',
213
- body: discovered,
214
- });
215
- }
216
- oidcConfig = discovered;
240
+ const discoveredConfig = readOIDCConfig(discovered);
241
+ oidcConfig = config.endSessionEndpoint
242
+ ? { ...discoveredConfig, end_session_endpoint: config.endSessionEndpoint }
243
+ : discoveredConfig;
217
244
  return oidcConfig;
218
245
  }
219
246
  async function requestToken(endpoint, body, fallbackMessage) {
@@ -387,14 +414,16 @@ export function createSSOAuthProvider(config) {
387
414
  }
388
415
  try {
389
416
  const endpoints = await discover();
390
- if (typeof window !== 'undefined' && endpoints.end_session_endpoint && session?.id_token) {
391
- const params = new URLSearchParams({
392
- id_token_hint: session.id_token,
393
- ...(config.postLogoutRedirectUri
394
- ? { post_logout_redirect_uri: config.postLogoutRedirectUri }
395
- : {}),
396
- });
397
- window.location.href = `${endpoints.end_session_endpoint}?${params}`;
417
+ if (typeof window !== 'undefined' && endpoints.end_session_endpoint) {
418
+ const logoutUrl = new URL(endpoints.end_session_endpoint, window.location.href);
419
+ logoutUrl.searchParams.set('client_id', config.clientId);
420
+ logoutUrl.searchParams.delete('id_token_hint');
421
+ if (session?.id_token)
422
+ logoutUrl.searchParams.set('id_token_hint', session.id_token);
423
+ if (config.postLogoutRedirectUri) {
424
+ logoutUrl.searchParams.set('post_logout_redirect_uri', config.postLogoutRedirectUri);
425
+ }
426
+ window.location.href = logoutUrl.href;
398
427
  return { success: true };
399
428
  }
400
429
  }
@@ -407,7 +436,16 @@ export function createSSOAuthProvider(config) {
407
436
  };
408
437
  },
409
438
  async check() {
410
- const existingSession = sessions.getSession();
439
+ let existingSession = sessions.getSession();
440
+ if (!existingSession) {
441
+ try {
442
+ await sessions.clearInvalidSession();
443
+ existingSession = sessions.getSession();
444
+ }
445
+ catch (error) {
446
+ return { authenticated: false, error: getErrorResult(error) };
447
+ }
448
+ }
411
449
  if (typeof window === 'undefined') {
412
450
  const now = Math.floor(Date.now() / 1000);
413
451
  return existingSession && (existingSession.expires_at === undefined || existingSession.expires_at > now)
@@ -31,6 +31,7 @@ export interface TokenStorageLike {
31
31
  }
32
32
  export interface SessionManager {
33
33
  getSession: () => SSOSession | null;
34
+ clearInvalidSession: () => Promise<void>;
34
35
  getAuthGeneration: () => number;
35
36
  saveSession: (session: SSOSession, event?: AuthStateChangeEvent) => void;
36
37
  clearSession: () => void;
@@ -306,6 +306,17 @@ export function createSessionManager(options) {
306
306
  });
307
307
  }
308
308
  }
309
+ function clearInvalidSession() {
310
+ const initialSnapshot = getSessionSnapshot();
311
+ if (initialSnapshot.raw === null || initialSnapshot.session)
312
+ return Promise.resolve();
313
+ return runAuthMutation(async () => {
314
+ const currentRaw = locallySignedOut ? null : options.storage.getItem(keys.tokens);
315
+ if (currentRaw === null || parseSession(currentRaw))
316
+ return;
317
+ clearTokenSession();
318
+ });
319
+ }
309
320
  function resolveRefreshRace(expectedRaw) {
310
321
  const currentRaw = locallySignedOut ? null : options.storage.getItem(keys.tokens);
311
322
  if (currentRaw === expectedRaw)
@@ -432,8 +443,11 @@ export function createSessionManager(options) {
432
443
  }
433
444
  async function getAccessToken(accessOptions = {}) {
434
445
  const snapshot = getSessionSnapshot();
435
- if (!snapshot.raw || !snapshot.session)
446
+ if (!snapshot.raw || !snapshot.session) {
447
+ if (snapshot.raw)
448
+ await clearInvalidSession();
436
449
  return null;
450
+ }
437
451
  const { raw: initialRaw, session } = snapshot;
438
452
  const minValiditySeconds = Math.max(0, accessOptions.minValiditySeconds ?? 0);
439
453
  const now = Math.floor(Date.now() / 1000);
@@ -532,6 +546,7 @@ export function createSessionManager(options) {
532
546
  scheduleRefresh(parseSession(lastObservedRaw));
533
547
  return {
534
548
  getSession,
549
+ clearInvalidSession,
535
550
  getAuthGeneration: () => authGeneration,
536
551
  saveSession,
537
552
  clearSession,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/sso",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "type": "module",
5
5
  "description": "OIDC/OAuth2 SSO AuthProvider plugin for svadmin — supports Okta, Azure AD, Cognito, Keycloak, Google",
6
6
  "license": "MIT",
@@ -32,10 +32,10 @@
32
32
  "dist"
33
33
  ],
34
34
  "peerDependencies": {
35
- "@svadmin/core": "^0.32.0"
35
+ "@svadmin/core": "^0.32.2"
36
36
  },
37
37
  "devDependencies": {
38
- "typescript": "^5.7.0"
38
+ "typescript": "^6.0.3"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "bun ../../scripts/build-node-package.ts"