@schematize/authorization-code
v0.6.7
Published
Schematize Authorization Code Auth Library
Readme
@schematize/authorization-code
OAuth 2.0 authorization code flow with PKCE for browser apps. The library exchanges codes for tokens, optionally validates OpenID Connect id_token claims and signatures (RS256 via JWKS), can call a userinfo endpoint, and schedules refresh_token exchanges before access token expiry.
Runtime requirements
- Browser only (uses
fetch,location,sessionStorage,localStorage,crypto.subtle). globalThis.isSecureContextmust be true (HTTPS orlocalhost); otherwise the constructor throws.
Features
- Authorization code + PKCE S256 (
code_verifierin localStorage so return flows work across tabs). statewraps currentsearch/hashplus optional app fields; restored after callback viahistory.replaceState.- Token endpoint supports
application/jsonorapplication/x-www-form-urlencoded(including optionalresourceparameters fromconfig.resources). - Stored session: access (and refresh) credentials in sessionStorage; PKCE verifier, OAuth state, and redirect_uri in localStorage.
refresh_tokengrant with a timer to refresh roughly 10 minutes before access token expiry.revoke: optional RFC 7009-style POST toendpoints.revokebefore clearing local state (used fromsignoutwhen revoke is configured and the access token is not yet expired).getUserInfo: GET toendpoints.userInfowith bearer header when configured; used as fallback ifid_tokenvalidation fails.
Dependencies
@schematize/refs@schematize/metamodel@schematize/util-common(base64 / binary helpers for PKCE and JWT handling)
Installation
npm install @schematize/authorization-codeUsage
import AuthorizationCode from '@schematize/authorization-code';
const auth = AuthorizationCode({
endpoints: {
authorize: 'https://auth.example.com/authorize',
token: 'https://auth.example.com/token',
revoke: 'https://auth.example.com/revoke',
userInfo: 'https://auth.example.com/userinfo',
wellKnownJwks: 'https://auth.example.com/.well-known/jwks.json',
signout: 'https://auth.example.com/signout',
},
clientId: 'your-client-id',
scope: 'openid profile email',
responseType: 'code',
responseMode: 'fragment',
issuer: 'https://auth.example.com',
});
// Initialize and check authorization status
await auth.initialized;
if (!auth.authorized) {
// Redirect to authorization server
await auth.authorize();
} else {
// Use the access token
console.log('Access token:', auth.accessToken);
console.log('User info:', auth.userInfo);
}AuthorizationCode is invoked as AuthorizationCode(config) or new AuthorizationCode(config). Identical (authorize URL, clientId, scope, responseType, resources fingerprint) configurations return the same instance (singleton per key).
Configuration
endpoints (required shape)
| Key | Required | Description |
| --- | --- | --- |
| authorize | yes | Authorization server URL (query string is appended). |
| token | yes | Token URL string, or { url, contentType } with contentType 'application/json' or 'application/x-www-form-urlencoded'. |
| revoke | no | Same string or { url, contentType } pattern as token. Used by signout → revoke when present. |
| userInfo | no | UserInfo URL for getUserInfo and id_token fallback. |
| wellKnownJwks | needed for id_token path | JWKS document URL. Required when the token response includes id_token and you rely on local signature verification. |
| signout | required to call signout() | SSO sign-out URL; signout appends ?redirect_uri=…. |
Other fields
clientId(required)scope(required for restore logic: stored token scopes must cover every scope inconfig.scope)responseType: use'code'for the code flow (default in examples).responseMode:'query'readslocation.search;'fragment'readslocation.hashforcode,state, and errors.issuer: optional; when set, validated againstid_tokeniss.resources: optionalstring[]; sent as repeatedresourceparameters (form) orresource: [...](JSON) on token and refresh requests.
API
auth.initialized
Promise started in the constructor; await auth.initialized waits for initialize() (code exchange from URL, or load from storage, or OAuth error params).
await auth.initialized;
if (auth.authorized) {
console.log(`Signed in`);
} else if (auth.error) {
console.error(auth.error, auth.errorDescription);
}initialize()
Same work as initialized; you normally do not need to call it explicitly.
await auth.initialize();authorize(options?)
- Awaits
initializedfirst. - If already
authorizedandoptions.forceis not true, returnsthis. - If
erroris set andforceis not true, throwsErrorwith message built fromerror/error_description/error_uri. - Otherwise clears token storage, stores PKCE verifier and state, sets
location.hrefto the authorize URL, and returnsfalse(pending navigation).
Options:
force: re-run authorize even if authorized or in error state.redirectUri: override redirect URI (defaultlocation.origin + location.pathname).state: shallow-merged into the wrapped state object (must be a plain object if provided).
// First visit: redirects the browser; execution may continue briefly until unload.
const redirecting = await auth.authorize();
if (redirecting === false) {
return;
}
// Already authorized: same instance returned
const same = await auth.authorize();
console.log(same === auth);
// Force a new login (e.g. “switch account”)
await auth.authorize({ force: true });
// Fixed callback URL
await auth.authorize({
redirectUri: `https://app.example.com/oauth/callback`,
});
// Extra fields stored in wrapped state (merged into OAuth state payload)
await auth.authorize({
state: { returnTo: `/dashboard` },
});signout(options?)
- Awaits
initialized, callsrevoke()(best-effortfetch), clears timers and storage, resets auth fields, then setslocation.hreftoendpoints.signout?redirect_uri=…(default redirect islocation.href). - Returns
falsewhen navigation is pending.
options.redirectUri overrides the post-sign-out redirect query value.
const leaving = await auth.signout();
if (leaving === false) {
return;
}
await auth.signout({
redirectUri: `https://app.example.com/`,
});refresh()
Uses grant_type=refresh_token with the same token endpoint encoding rules and optional resource list. Guarded by isRefreshingToken so overlapping calls are skipped. On failure after init, callers may see thrown errors from refresh (unlike the initial code exchange, which only sets error on the instance).
try {
await auth.refresh();
} catch (e) {
console.error(`Refresh failed`, e);
}revoke()
If endpoints.revoke exists and the access token is not considered expired (expires is in seconds on the instance from stored creds), POSTs client_id, token, token_type_hint (access_token or refresh_token). Errors are logged, not thrown.
await auth.initialized;
await auth.revoke();getUserInfo()
If endpoints.userInfo is missing or there is no bearer header, returns undefined. Otherwise GETs JSON and assigns userInfo. Returns cached userInfo when already set.
await auth.initialized;
const profile = await auth.getUserInfo();
if (profile === undefined) {
console.log(`No userInfo endpoint or not yet authorized`);
} else {
console.log(profile);
}Properties (after init / token handling)
| Property | Notes |
| --- | --- |
| authorized | Boolean |
| accessToken | String |
| refreshToken | String when issued |
| idToken | Raw id_token string when present |
| expires | Access token expiry as seconds since epoch (internal scheduling uses this) |
| headers | e.g. { Authorization: 'Bearer …' } |
| userInfo | OIDC claims object from validated id_token payload, storage replay, or UserInfo endpoint |
| error, errorDescription, errorUri | From token error JSON or authorize redirect error query |
OpenID / id_token behavior
- Validates RS256 JWS using
wellKnownJwks, checksexp/iat, optionalissuermatch, andaudequalsclientId(single-audience case). - Nonce (
noncein auth request) is not fully wired end-to-end yet (see code TODO). - On validation failure, falls back to
getUserInfo()when possible.
Security notes
- Prefer sessionStorage for tokens (current default); PKCE/state/redirect use localStorage for cross-tab flows (see OWASP notes in source).
- Token and revoke responses are read with
response.json(); endpoints must return JSON bodies for those requests.
License
MIT
Author
Benjamin Bytheway
