Skip to Content
API ReferenceCore
View .md

Alien SSO Core API Reference: AlienSsoClient Methods

Low-level client for any JavaScript or TypeScript project. For React apps, prefer the hooks from @alien-id/sso-react. For the flow in context, see the Core Integration guide.

AlienSsoClient

AlienSsoClient drives the whole sign-in flow: deep link generation, polling, token exchange, verification and refresh.

new AlienSsoClient(config: AlienSsoClientConfig)
OptionTypeDefaultDescription
ssoBaseUrlstring— (required)Base URL of the SSO service. HTTPS is enforced for non-loopback hosts
providerAddressstring— (required)Your provider address
pollingIntervalnumber5000Polling interval in ms
redirectUristring (URL)OAuth2 redirect_uri; when set, it is sent on both the authorize and token requests (RFC 6749 §4.1.3). Not needed for the deeplink + poll flow
tokenStorageTokenStorageenv-awareToken persistence override (see Token Storage). Default: LocalStorageTokenStorage in browsers, MemoryTokenStorage in Node/SSR
allowInsecureSsoBaseUrlbooleanfalseDev opt-in: accept http:// for a non-loopback ssoBaseUrl. localhost / 127.0.0.1 / [::1] are always allowed
dpopobjectOpt into RFC 9449 DPoP sender-constrained tokens (see DPoP)
const client = new AlienSsoClient({ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'your-provider-address', });

Authentication Methods

Starts the OAuth2 authorization flow with response_mode=json and returns the deep link to show as a QR code plus the polling code. Throws in environments without a CSPRNG (crypto.getRandomValues) or SubtleCrypto — the SDK refuses to fall back to a non-cryptographic random source.

async generateDeeplink(): Promise<AuthorizeResponse>
GET /oauth/authorize?response_type=code&response_mode=json&client_id={providerAddress}&scope=openid&code_challenge={challenge}&code_challenge_method=S256&state={state}&nonce={nonce}

Each call generates a PKCE code verifier and challenge (S256), mints fresh state and nonce values, and stores all three in sessionStorage. The state echo and the RFC 9207 iss parameter are verified on pollAuth(), and the nonce is verified against the id_token on exchangeToken() — protecting against CSRF, token replay and issuer mix-up attacks.

{ deep_link: string; // Deep link for QR code or mobile redirect polling_code: string; // Code for polling authentication status expired_at: number; // Unix timestamp when the session expires }
const { deep_link, polling_code, expired_at } = await client.generateDeeplink(); console.log('Deep link:', deep_link); console.log('Expires:', new Date(expired_at * 1000));

The authorization session behind the deep link is short-lived — the server caps it at 10 minutes (RFC 6749 §4.1.2); expired_at carries the exact expiry. Deep links are also single-use on the server side: a previously seen code_challenge is rejected with invalid_request: code challenge already used, so always mint a fresh deep link per sign-in attempt.

pollAuth()

Polls the authorization status for the polling_code returned by generateDeeplink(). When status is authorized, the SDK verifies that state matches the value minted by generateDeeplink(). An iss present on any poll response must equal ssoBaseUrl (RFC 9207) — a mismatch throws.

async pollAuth(pollingCode: string): Promise<PollResponse>
POST /oauth/poll Body: { "polling_code": "..." }
{ status: 'pending' | 'authorized' | 'rejected' | 'expired'; authorization_code?: string; // Only present when status is 'authorized' state?: string; // Echo of the state sent on authorize (RFC 6749 §10.12) iss?: string; // Issuer identifier (RFC 9207) }
StatusMeaning
pendingUser hasn’t completed authentication yet
authorizedUser approved, authorization_code available
rejectedUser denied authentication
expiredSession expired

The server also responds 404 for an unknown polling_code and 409 (invalid_grant) once the code has been redeemed — e.g. when polling continues after exchangeToken(). Treat either as a signal to stop polling.

exchangeToken()

Exchanges the authorization code for tokens at the OAuth2 token endpoint. The SDK retrieves the code verifier from sessionStorage, verifies the id_token (signature, iss, aud, nonce) before persisting, stores the tokens and the verified claims via the configured token storage (localStorage by default in browsers), then clears the code verifier and nonce from sessionStorage.

async exchangeToken(authorizationCode: string): Promise<TokenResponse>
POST /oauth/token Body: grant_type=authorization_code&code={code}&client_id={providerAddress}&code_verifier={verifier}
{ access_token: string; // JWT access token id_token?: string; // JWT ID token with user claims refresh_token?: string; // Opaque refresh token token_type: string; // "Bearer", or "DPoP" when DPoP-bound expires_in: number; // Token lifetime in seconds }

verifyAuth()

Calls the userinfo endpoint to verify the current access token, automatically refreshing it on a 401 when a refresh token is stored. Returns null if not authenticated or the token is invalid. If userinfo.sub does not match the verified id_token sub, the SDK treats it as a token-substitution attack (OIDC Core §5.3.2): it calls logout() and throws. Not DPoP-aware — see DPoP.

async verifyAuth(): Promise<UserInfoResponse | null>
GET /oauth/userinfo Authorization: Bearer {access_token}
{ sub: string; // User identifier (session address) aud?: string; // client_id the access token was issued for }

refreshAccessToken()

Refreshes the access token using the stored refresh token and updates the stored tokens. If the response omits refresh_token, the existing refresh token is kept (RFC 6749 §6). Throws when no refresh token is available; on a failed refresh it calls logout() to clear the invalid tokens, then throws. Concurrent calls are deduplicated per providerAddress — they share a single in-flight request.

async refreshAccessToken(): Promise<TokenResponse>
POST /oauth/token Body: grant_type=refresh_token&refresh_token={token}&client_id={providerAddress}

Resolves to the same TokenResponse shape as exchangeToken().

withAutoRefresh()

Executes a function and, when it fails with a 401 and a refresh token is stored, refreshes the token and retries it.

async withAutoRefresh<T>( requestFn: () => Promise<T>, maxRetries?: number ): Promise<T>
ParameterTypeRequiredDescription
requestFnfunctionYesAsync function to execute
maxRetriesnumberNoMax retry attempts (default 1)
const data = await client.withAutoRefresh(async () => { const res = await fetch('/api/data', { headers: { Authorization: `Bearer ${client.getAccessToken()}` } }); if (res.status === 401) { throw Object.assign(new Error('Unauthorized'), { response: { status: 401 } }); } return res.json(); });

logout()

Clears all stored authentication data: removes access_token, id_token, id_token_claims, refresh_token and token_expiry from the configured token storage, and code_verifier, state and nonce from sessionStorage.

logout(): void

Session Methods

getAccessToken() / getIdToken() / getRefreshToken()

Return the stored access token, ID token or refresh token, or null when none is stored.

getAccessToken(): string | null getIdToken(): string | null getRefreshToken(): string | null

getAuthData()

Returns the id_token claims that were verified and persisted at exchange/refresh time, re-checking only exp against the current clock. It does not re-parse or re-verify the JWT — signature, iss, aud and nonce verification already ran before the claims were persisted. Returns null if no persisted claims exist, the claims fail schema validation, or the token is expired.

getAuthData(): TokenInfo | null

See TokenInfo for the full claim list.

getSubject()

Shorthand for getAuthData()?.sub — the subject (user identifier) from the verified claims, or null.

getSubject(): string | null

isTokenExpired()

Checks whether the stored token is expired, using the alien-sso_token_expiry timestamp persisted from the token response’s expires_in. It never parses the JWT’s exp claim (RFC 9068 §6) and returns true when no expiry is stored.

isTokenExpired(): boolean

isAccessTokenExpired()

Checks whether the access token is expired or will expire within 5 minutes, using the stored expiry timestamp for efficiency.

isAccessTokenExpired(): boolean

hasRefreshToken()

Checks whether a refresh token is available.

hasRefreshToken(): boolean

Types

AlienSsoClientConfig

interface AlienSsoClientConfig { ssoBaseUrl: string; providerAddress: string; pollingInterval?: number; redirectUri?: string; tokenStorage?: TokenStorage; allowInsecureSsoBaseUrl?: boolean; dpop?: { keypair: { privateKey: CryptoKey; publicJwk: { kty: 'OKP'; crv: 'Ed25519'; x: string }; }; }; }

ssoBaseUrl must use https://; plain http:// is rejected for non-loopback hosts (localhost, 127.0.0.1, [::1] are allowed) unless allowInsecureSsoBaseUrl: true is set for development.

Token Storage

Token persistence is pluggable via the tokenStorage config field. The SDK exports the interface and two implementations:

interface TokenStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; } class LocalStorageTokenStorage implements TokenStorage {} // backed by localStorage class MemoryTokenStorage implements TokenStorage {} // in-memory, cleared on reload

The default is environment-aware: LocalStorageTokenStorage in browsers (sessions survive a reload), MemoryTokenStorage in Node/SSR where no localStorage global exists. Pass tokenStorage: new MemoryTokenStorage() to opt out of persistence in the browser, e.g. to reduce XSS exposure of stored tokens.

DPoP (sender-constrained tokens)

Passing dpop: { keypair } opts the client into RFC 9449 DPoP:

  • dpop_jkt (the keypair’s JWK thumbprint) is sent on /oauth/authorize
  • A DPoP proof header signed by the keypair is sent on /oauth/token (code exchange and refresh)
  • The response must have token_type: "DPoP" — a Bearer response means the server did not honor the binding, and the SDK throws rather than silently downgrade
  • Issued tokens carry the cnf.jkt confirmation claim binding them to your key

The keypair is an Ed25519 Web Crypto pair ({ privateKey: CryptoKey, publicJwk } — the inline shape in AlienSsoClientConfig) and is reused across exchange and refresh so the binding survives refresh-token rotation. When dpop is omitted, the client is a regular Bearer-token OIDC consumer.

The SDK does not currently export a keypair helper, so generate one with Web Crypto directly (requires a runtime with Ed25519 support — current browsers and modern Node):

const kp = (await crypto.subtle.generateKey( { name: 'Ed25519' }, false, // non-extractable — the private key never leaves Web Crypto ['sign', 'verify'], )) as CryptoKeyPair; const { kty, crv, x } = (await crypto.subtle.exportKey( 'jwk', kp.publicKey, )) as { kty: 'OKP'; crv: 'Ed25519'; x: string }; const client = new AlienSsoClient({ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'your-provider-address', dpop: { keypair: { privateKey: kp.privateKey, publicJwk: { kty, crv, x } }, }, });

DPoP and verifyAuth(). verifyAuth() always calls userinfo with the Authorization: Bearer scheme and no DPoP proof. With dpop configured, tokens are DPoP-bound and userinfo requires the DPoP scheme plus a valid proof — so verifyAuth() fails for DPoP clients. Rely on the locally verified claims via getAuthData(), or call /oauth/userinfo with your own RFC 9449 proof.

AuthorizeResponse

interface AuthorizeResponse { deep_link: string; polling_code: string; expired_at: number; }

PollResponse

interface PollResponse { status: 'pending' | 'authorized' | 'rejected' | 'expired'; authorization_code?: string; state?: string; iss?: string; }

TokenResponse

interface TokenResponse { access_token: string; id_token?: string; refresh_token?: string; token_type: string; expires_in: number; }

UserInfoResponse

interface UserInfoResponse { sub: string; aud?: string; }

TokenInfo

interface TokenInfo { iss: string; // Issuer URL sub: string; // Subject (user identifier) aud: string | string[]; // Audience (your provider address) exp: number; // Expiration timestamp iat?: number; // Issued at timestamp (optional per RFC 7519) client_id?: string; // Present on access tokens (RFC 9068) jti?: string; // JWT ID, present on access tokens nonce?: string; // Nonce (if provided in authorize) auth_time?: number; // Authentication time (always emitted on ID tokens) azp?: string; // Authorized party (OIDC §3.1.3.7) }

auth_time is always emitted on ID tokens. client_id and jti appear on access tokens (RFC 9068). DPoP-bound tokens additionally carry a cnf confirmation claim (RFC 7800) — see DPoP.

Local ID Token Verification

For backends that receive an id_token and need to validate it without calling the SSO server on every request, the package exports a standalone verifier. It is Web Crypto based, so it works in browsers and modern Node:

function parseJwt(token: string): ParsedJwt async function verifyIdToken( token: string, opts: VerifyIdTokenOptions ): Promise<VerifiedIdToken | null> async function fetchJwks(url: string): Promise<JWKS> class JwksCache { constructor(url: string, opts?: { ttlMs?: number; fetcher?: JwksFetcher }); get(forceRefresh?: boolean): Promise<JWKS>; inject(jwks: JWKS): void; }

verifyIdToken runs the full OIDC Core §3.1.3.7 validation — RS256 signature against the JWKS, iss, aud (including azp and the trusted-audience rule), exp/nbf/iat with a 30-second default clock skew, and nonce when expected — and returns null on any failure. parseJwt only splits and decodes the JWT (it does not verify); JwksCache caches the JWKS for 24 hours by default.

import { JwksCache, verifyIdToken } from '@alien-id/sso'; const jwks = new JwksCache('https://sso.alien-api.com/oauth/jwks'); export async function requireUser(idToken: string) { const result = await verifyIdToken(idToken, { jwks: await jwks.get(), expectedIssuer: 'https://sso.alien-api.com', expectedAudience: 'your-provider-address', }); if (!result) throw new Error('Invalid id_token'); return result.payload.sub as string; // the user's session address }

Options: expectedNonce?, clockSkewSec? (default 30) and trustedAudiences? (OIDC §3.1.3.7 step 3 — extra audiences are rejected unless listed). The related types JWK, JWKS, ParsedJwt, VerifyIdTokenOptions, VerifiedIdToken, JwksFetcher and JwksCacheOptions are all exported. client.injectJwks(jwks) exists as a test/dev seam that pre-loads the client’s own JWKS cache to skip the HTTP fetch during exchange and refresh.

Storage Keys

Token keys live in the configured token storage (localStorage by default in browsers); flow-scoped values live in sessionStorage.

KeyDescription
alien-sso_access_tokenJWT access token
alien-sso_id_tokenJWT ID token
alien-sso_id_token_claimsVerified ID token claims (JSON)
alien-sso_refresh_tokenRefresh token
alien-sso_token_expiryExpiry timestamp (ms)

In sessionStorage:

KeyDescription
alien-sso_code_verifierPKCE code verifier
alien-sso_stateOAuth2 state (CSRF protection)
alien-sso_nonceOIDC nonce (ID token replay protection)

HTTP Endpoints Summary

MethodEndpointDescription
GET/oauth/authorizeStart authorization flow
POST/oauth/pollPoll for authorization status
POST/oauth/tokenExchange code or refresh token
GET, POST/oauth/userinfoGet user info
GET/oauth/jwksGet public keys for JWT verification
GET/.well-known/openid-configurationOIDC discovery document
GET/.well-known/oauth-authorization-serverOAuth 2.0 authorization server metadata (RFC 8414, same document)

Error Handling

Per-method throw behavior is documented at each entry above. All async methods can also fail on network errors:

try { await client.generateDeeplink(); } catch (error) { // Network error, invalid provider, or server error } try { await client.exchangeToken(code); } catch (error) { // Invalid code, expired code, or PKCE mismatch } try { await client.refreshAccessToken(); } catch (error) { // Refresh token expired or revoked // Client automatically calls logout() }

Next Steps

Last updated on