SSO Provider
The @svadmin/sso package provides an AuthProvider that implements OIDC Authorization Code Flow with PKCE — no client secret needed in the browser.
Installation
Section titled “Installation”bun add @svadmin/ssoSupported Identity Providers
Section titled “Supported Identity Providers”Any OIDC-compliant provider works out of the box:
| Provider | Issuer URL Example |
|---|---|
| Okta | https://your-tenant.okta.com |
| Azure AD / Entra ID | https://login.microsoftonline.com/{tenant}/v2.0 |
| Amazon Cognito | https://cognito-idp.{region}.amazonaws.com/{userPoolId} |
| Keycloak | https://your-keycloak.com/realms/{realm} |
https://accounts.google.com | |
| Auth0 | https://your-tenant.auth0.com |
Basic Usage
Section titled “Basic Usage”import { createSSOAuthProvider } from '@svadmin/sso';import { setAuthProvider } from '@svadmin/core';
const authProvider = createSSOAuthProvider({ issuer: 'https://your-tenant.okta.com', clientId: 'your-client-id', redirectUri: window.location.origin + '/callback',});
setAuthProvider(authProvider);Configuration
Section titled “Configuration”interface SSOConfig { /** OIDC Issuer URL */ issuer: string; /** OAuth2 Client ID */ clientId: string; /** Redirect URI after login */ redirectUri: string; /** OAuth2 scopes. Default: ['openid', 'profile', 'email'] */ scopes?: string[]; /** Redirect after logout */ postLogoutRedirectUri?: string; /** Token storage. Default: 'local' (localStorage) */ storage?: 'local' | 'session' | TokenStorage; /** Storage namespace. Defaults to an issuer/client-specific key. */ storageKey?: string; /** Explicit legacy namespace to migrate after confirming provider ownership. */ legacyStorageKey?: string; /** Custom identity mapper */ mapIdentity?: (userinfo: Record<string, unknown>) => Identity; /** Auto-refresh tokens. Default: true */ autoRefresh?: boolean; /** Seconds before expiry to refresh. Default: 60 */ refreshBuffer?: number; /** Extra authorization request params, e.g. audience or prompt */ authorizationParams?: Record<string, string>; /** Atomic authentication coordination lock override for runtimes without Web Locks. */ refreshLock?: RefreshLock; /** Injectable fetch implementation for tests and SSR runtimes. */ fetcher?: typeof fetch;}Features
Section titled “Features”PKCE Flow
Section titled “PKCE Flow”All auth flows use the Authorization Code Flow with PKCE (Proof Key for Code Exchange) — the recommended approach for browser-based apps. No client secret is stored in the frontend.
Auto Discovery
Section titled “Auto Discovery”The provider automatically fetches your IdP’s configuration from /.well-known/openid-configuration. No need to manually specify endpoints.
Token Refresh
Section titled “Token Refresh”When autoRefresh: true (default), the provider schedules refresh before the access token expires. Browser refreshes and authentication commits use the atomic Web Locks API, and concurrent refresh calls in one provider share a single result. A browser without Web Locks fails refresh and token commits closed unless an atomic cross-context refreshLock is configured; it never falls back to a non-atomic local-storage lease. Local logout still clears the current tab.
Retryable failures such as network errors, 503, or lock acquisition failures retain the current session for a later retry. Terminal OAuth errors such as invalid_grant, refresh_token_not_found, or refresh-token reuse clear the session and require a new login.
The default storage key is isolated by issuer and client ID. Ambiguous svadmin_sso_* sessions are not claimed automatically because they contain no issuer/client ownership metadata. For a verified single-provider upgrade, set legacyStorageKey: 'svadmin_sso'; alternatively, keep using the legacy namespace with storageKey: 'svadmin_sso'.
Cross-tab rotation coordination requires both Web Locks (or a custom atomic refreshLock) and a shared storage backend. sessionStorage remains tab-isolated; use the default local storage or provide a shared TokenStorage when tabs must converge on the same rotated session.
If local storage is unavailable, the default browser adapter falls back to sessionStorage; that fallback is intentionally tab-isolated.
Custom Identity Mapping
Section titled “Custom Identity Mapping”const authProvider = createSSOAuthProvider({ issuer: 'https://your-tenant.okta.com', clientId: 'abc', redirectUri: '/callback', mapIdentity: (userinfo) => ({ id: userinfo.sub as string, name: userinfo.preferred_username as string, avatar: userinfo.picture as string, email: userinfo.email as string, }),});Permissions from ID Token
Section titled “Permissions from ID Token”The provider automatically extracts roles, groups, or permissions claims from the ID token via getPermissions().
const permissions = await authProvider.getPermissions();// → ['admin', 'editor'] (from ID token claims)Calling Protected APIs
Section titled “Calling Protected APIs”Use createAuthenticatedFetch() for protected API calls. It adds the current access token, refreshes after one 401, and safely replays the request once. A 403 is returned unchanged and does not refresh or sign out the user.
const authProvider = createSSOAuthProvider({ issuer: 'https://your-tenant.okta.com', clientId: 'abc', redirectUri: '/callback', authorizationParams: { audience: 'admin-api' },});
const authFetch = authProvider.createAuthenticatedFetch();const response = await authFetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ range: '30d' }),});The original request must be replayable. A consumed Request body fails explicitly with request_not_replayable instead of sending a partial retry.
For non-idempotent operations, use an idempotency key accepted by the API before enabling automatic replay. A transport-level retry cannot prove whether an upstream processed a request before returning 401.
Use getAccessToken() only when a library requires the raw token and owns its own retry behavior:
const token = await authProvider.getAccessToken({ minValiditySeconds: 60 });Session Events
Section titled “Session Events”Subscribe to rotation and logout changes when application state must follow the auth session:
const unsubscribe = authProvider.onAuthStateChange((event, session) => { if (event === 'TOKEN_REFRESHED') updateApiSession(session); if (event === 'SIGNED_OUT') clearPrivateState();});
// Call during application teardown.unsubscribe();authProvider.destroy();Callback Page
Section titled “Callback Page”Create a callback page at your redirectUri path:
<script> import { useIsAuthenticated } from '@svadmin/core'; const { isLoading } = useIsAuthenticated();</script>
{#if isLoading} <p>Authenticating...</p>{/if}The check() method automatically detects the callback URL, exchanges the authorization code for tokens, and cleans up the URL.