How it works
- Your app sends the user to
https://failure.fail/oauth/authorizewith a PKCE challenge. - They sign in to Failure ID (password, passkey, authenticator or face check) and approve your app once.
- Failure ID redirects back with a one-time
code, which your app exchanges — with the PKCE verifier — for tokens. - You get an access token (1 hour), an ES256-signed ID token, and with
offline_accessa rotating refresh token (30 days).
S256 is required for every client, public or confidential. Redirect URIs must match exactly. Codes are single-use and expire in 10 minutes.1. Register your app
Open Console → OAuth apps and register the app with its exact redirect URIs (https://, or http://localhost for development; custom schemes work for native apps). Choose public for single-page and mobile apps — they never hold a secret — or confidential if a server makes the token call; the client secret is shown once.
2. Browser apps (SDK)
import { FailureID } from "https://failure.fail/sdk/failure-id.js";
const fid = new FailureID({
clientId: "fid_…",
redirectUri: location.origin + "/callback",
scope: "openid profile email offline_access",
});
// on the redirect page
const done = await fid.handleRedirect(); // { user, returnTo } or null
if (!fid.isSignedIn()) fid.renderButton("#login"); // or fid.login()
else console.log(await fid.getUser()); // { sub, email, name }
// call your own API with the token (refreshed automatically)
await fid.fetch("/api/me");
await fid.logout(); // revokes the session
The SDK stores tokens in localStorage by default; pass storage: "session" or "memory" to change that. It is plain ES modules with no dependencies — copy failure-id.js into your bundle if you prefer not to load it from our domain.
3. Server apps
The same file exports the low-level helpers and runs on Node 18+, Bun, Deno and Workers. A confidential web app:
import { createPkce, buildAuthorizeUrl, exchangeCode, verifyIdToken } from "./failure-id.js";
// GET /login
const pkce = await createPkce();
session.pkce = pkce.verifier; session.state = crypto.randomUUID();
res.redirect(buildAuthorizeUrl({ clientId, redirectUri, state: session.state, codeChallenge: pkce.challenge }));
// GET /callback?code&state
if (req.query.state !== session.state) throw new Error("state mismatch");
const t = await exchangeCode({ code: req.query.code, codeVerifier: session.pkce, clientId, clientSecret, redirectUri });
const claims = await verifyIdToken(t.id_token, { clientId }); // checks ES256 signature, iss, aud, exp
session.userId = claims.sub;
Any OIDC library
Failure ID publishes standard discovery, so libraries such as openid-client, Auth.js, oidc-client-ts or AppAuth configure themselves from the issuer:
| Setting | Value |
|---|---|
| Issuer | https://failure.fail |
| Discovery | /.well-known/openid-configuration |
| Authorize | /oauth/authorize |
| Token | /oauth/token (form or JSON; client_secret_post, client_secret_basic or none) |
| User info | /oauth/userinfo |
| Revocation | /oauth/revoke |
| JWKS | /.well-known/jwks.json (ES256) |
Scopes and claims
| Scope | Gives you |
|---|---|
openid | An ID token with sub — a stable user id unique to Failure ID |
profile | name |
email | email, with email_verified: false — accounts are invite-only but addresses aren't mailbox-verified yet, so don't treat email as proof of ownership |
offline_access | A refresh token. Each use returns a new one; reusing an old one revokes the whole session |
Authorize parameters
| Parameter | |
|---|---|
response_type | code |
client_id, redirect_uri | As registered |
code_challenge, code_challenge_method | Required, S256 |
scope, state, nonce | Recommended |
prompt | login forces re-authentication, consent shows the consent screen again, none fails with consent_required instead of prompting |
Errors
Authorization errors come back on your redirect URI as error (access_denied, invalid_request, consent_required, unsupported_response_type). Token errors are JSON: invalid_client (401), invalid_grant, unsupported_grant_type. The SDK throws FailureIDError with .code set to these.
For your users
People see your app's name and the scopes before they approve, and can disconnect it at any time in Security → Connected apps, which revokes every token you hold for them.