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)| Option | Type | Default | Description |
|---|---|---|---|
ssoBaseUrl | string | — (required) | Base URL of the SSO service. HTTPS is enforced for non-loopback hosts |
providerAddress | string | — (required) | Your provider address |
pollingInterval | number | 5000 | Polling interval in ms |
redirectUri | string (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 |
tokenStorage | TokenStorage | env-aware | Token persistence override (see Token Storage). Default: LocalStorageTokenStorage in browsers, MemoryTokenStorage in Node/SSR |
allowInsecureSsoBaseUrl | boolean | false | Dev opt-in: accept http:// for a non-loopback ssoBaseUrl. localhost / 127.0.0.1 / [::1] are always allowed |
dpop | object | — | Opt 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
generateDeeplink()
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)
}| Status | Meaning |
|---|---|
pending | User hasn’t completed authentication yet |
authorized | User approved, authorization_code available |
rejected | User denied authentication |
expired | Session 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>| Parameter | Type | Required | Description |
|---|---|---|---|
requestFn | function | Yes | Async function to execute |
maxRetries | number | No | Max 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(): voidSession 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 | nullgetAuthData()
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 | nullSee TokenInfo for the full claim list.
getSubject()
Shorthand for getAuthData()?.sub — the subject (user identifier)
from the verified claims, or null.
getSubject(): string | nullisTokenExpired()
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(): booleanisAccessTokenExpired()
Checks whether the access token is expired or will expire within 5 minutes, using the stored expiry timestamp for efficiency.
isAccessTokenExpired(): booleanhasRefreshToken()
Checks whether a refresh token is available.
hasRefreshToken(): booleanTypes
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 reloadThe 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"— aBearerresponse means the server did not honor the binding, and the SDK throws rather than silently downgrade - Issued tokens carry the
cnf.jktconfirmation 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 theAuthorization: Bearerscheme and no DPoP proof. Withdpopconfigured, tokens are DPoP-bound and userinfo requires theDPoPscheme plus a valid proof — soverifyAuth()fails for DPoP clients. Rely on the locally verified claims viagetAuthData(), or call/oauth/userinfowith 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.
| Key | Description |
|---|---|
alien-sso_access_token | JWT access token |
alien-sso_id_token | JWT ID token |
alien-sso_id_token_claims | Verified ID token claims (JSON) |
alien-sso_refresh_token | Refresh token |
alien-sso_token_expiry | Expiry timestamp (ms) |
In sessionStorage:
| Key | Description |
|---|---|
alien-sso_code_verifier | PKCE code verifier |
alien-sso_state | OAuth2 state (CSRF protection) |
alien-sso_nonce | OIDC nonce (ID token replay protection) |
HTTP Endpoints Summary
| Method | Endpoint | Description |
|---|---|---|
| GET | /oauth/authorize | Start authorization flow |
| POST | /oauth/poll | Poll for authorization status |
| POST | /oauth/token | Exchange code or refresh token |
| GET, POST | /oauth/userinfo | Get user info |
| GET | /oauth/jwks | Get public keys for JWT verification |
| GET | /.well-known/openid-configuration | OIDC discovery document |
| GET | /.well-known/oauth-authorization-server | OAuth 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
- React API Reference — hooks and components
- Core Integration guide — the full flow in context
- OAuth2 Clients — NextAuth, Python and Go recipes
- Demo App — example implementation