OAuth Quickstart
Register an OAuth app and run the full Login with Zafeguard flow end-to-end.
OAuth Quickstart
End-to-end setup for "Login with Zafeguard". The example uses openid-client on the Node.js side; any OIDC-compliant library works the same way.
1. Register your app
-
Sign in to the Zafeguard dashboard.
-
Open the Developer section → OAuth apps → New app.
-
Fill in:
- Name — shown on the consent screen. Users will see this; choose a name that matches your product.
- Description — short summary, shown on the consent screen.
- Homepage URL, Privacy Policy URL, Terms of Service URL — required for review; users see them on the consent screen.
- Redirect URIs — exact-match list. The OAuth flow refuses to redirect to anything not in this list. Use HTTPS in production.
- Allowed scopes — the maximum set your app can request. Users can grant a subset but never more.
- Token endpoint auth method:
client_secret_basic— confidential server-side apps (most integrations).client_secret_post— same, but credentials in the form body instead of the Basic header.none— public clients (SPAs, mobile apps) using PKCE without a client secret.
-
Click Submit for review. Zafeguard staff approve the metadata.
-
Once approved, the detail page shows an Issue client secret button. Click it — the secret is shown exactly once. Copy it into your app's secret store before closing the dialog.
You now have:
client_id— azfg_…string, visible on the detail page.client_secret— the plaintext you copied. Treat it like any production credential.- A list of approved redirect URIs and allowed scopes.
2. Discover the endpoints
Every OIDC library can self-configure from the discovery document:
curl https://api.zafeguard.com/.well-known/openid-configuration{
"issuer": "https://api.zafeguard.com",
"authorization_endpoint": "https://api.zafeguard.com/oauth/authorize",
"token_endpoint": "https://api.zafeguard.com/oauth/token",
"userinfo_endpoint": "https://api.zafeguard.com/oauth/userinfo",
"revocation_endpoint": "https://api.zafeguard.com/oauth/revoke",
"jwks_uri": "https://api.zafeguard.com/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "email", "profile", "offline_access", "workspace:read", "workspace:write", "workspace:api_keys", "workspace:admin"],
"code_challenge_methods_supported": ["S256"]
}3. Run the authorization code + PKCE flow
Server-side (Node.js + openid-client)
import { Issuer, generators } from "openid-client";
// One-time at boot
const issuer = await Issuer.discover("https://api.zafeguard.com");
const client = new issuer.Client({
client_id: process.env.ZAFEGUARD_CLIENT_ID!,
client_secret: process.env.ZAFEGUARD_CLIENT_SECRET!,
redirect_uris: ["https://app.example.com/oauth/callback"],
response_types: ["code"],
token_endpoint_auth_method: "client_secret_basic",
});
// Per sign-in attempt
function buildSignInUrl() {
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
const state = generators.state();
const nonce = generators.nonce();
// Store { code_verifier, state, nonce } in a session-scoped store keyed by state
// so the callback can look them up.
return client.authorizationUrl({
scope: "openid email profile workspace:read",
code_challenge,
code_challenge_method: "S256",
state,
nonce,
});
}
// In your /oauth/callback handler
async function handleCallback(req, res) {
const params = client.callbackParams(req);
const stored = await sessionStore.get(params.state); // { code_verifier, nonce }
const tokenSet = await client.callback(
"https://app.example.com/oauth/callback",
params,
{ code_verifier: stored.code_verifier, state: params.state, nonce: stored.nonce },
);
// tokenSet.access_token — JWT, RS256-signed, carries workspace_id claim
// tokenSet.id_token — JWT, RS256-signed, identity claims
// tokenSet.refresh_token — opaque string, only if you requested `offline_access`
// tokenSet.expires_in — seconds until access_token expires (1 hour)
const userinfo = await client.userinfo(tokenSet.access_token);
// { sub, email, email_verified, name } — filtered to the granted scopes
return res.json({ user: userinfo });
}Public clients (SPA / mobile)
Set token_endpoint_auth_method: "none" when registering the app — Zafeguard will not require a client secret and PKCE alone proves possession of the original request. Everything else is identical.
4. Verify tokens offline
Your app can verify both the ID token and any access token against the JWKS without contacting Zafeguard:
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://api.zafeguard.com/.well-known/jwks.json"),
);
const { payload } = await jwtVerify(accessToken, JWKS, {
issuer: "https://api.zafeguard.com",
audience: process.env.ZAFEGUARD_CLIENT_ID,
});
// payload.sub — Zafeguard user id (stable, opaque)
// payload.workspace_id — the workspace this token can act on
// payload.scope — space-separated granted scopes
// payload.exp — unix expiryCache the JWKS response and respect its Cache-Control header. Retired signing keys remain in the JWKS until every token they signed has expired — verification never breaks on a key rotation.
5. Refresh access tokens
If you requested offline_access, the token response includes a refresh_token. Exchange it for a fresh access token before expiry:
const refreshed = await client.refresh(currentRefreshToken);
// refreshed.access_token — new access token
// refreshed.refresh_token — ALWAYS a new value; the old one is now invalidThe refresh token rotates on every use. If you ever submit a previously-rotated refresh token (replay), Zafeguard revokes the entire token chain and the user must sign in again — store only the most recent refresh token.
6. Revoke tokens
When the user signs out of your app, revoke the refresh token so it cannot be used to mint more access tokens:
await client.revoke(refreshToken);The user can also revoke your app's access independently from their workspace's Connected apps page in the Zafeguard dashboard. Either side of the revocation cuts off access immediately; existing access tokens become invalid at their natural expiry (1 hour).
Next: pick the right scope
The scopes you request determine what your app can read and do on the user's behalf. See Scopes → for the full list with concrete examples.