Authentication flow
Prysm:ID speaks standard OIDC (the OpenID Connect profile of OAuth 2.0). If your favorite library speaks OIDC, it speaks to us with no proprietary glue.
The standard flow (Authorization Code + PKCE)
Section titled “The standard flow (Authorization Code + PKCE)”The recommended flow for web apps, mobile, and SPAs. For pure backend-to-backend you use client credentials (see end).
┌──────────┐ ┌────────────────────┐ │ Your app │ │ auth.acme. │ │ (browser │ │ prysmid.com │ │ or BE) │ │ (instance) │ └────┬─────┘ └─────────┬──────────┘ │ │ │ 1. user clicks "Login" │ │ → redirect to /authorize │ │ with client_id, redirect_uri, │ │ scope=openid profile email, │ │ state, code_challenge (PKCE) │ ├──────────────────────────────────────────────────▶│ │ │ │ 2. login UI: user enters email/pwd │ │ or picks a social IdP (Google/GitHub) │ │ │ │ 3. instance authenticates │ │ → redirect to redirect_uri │ │ with code + state │ │◀──────────────────────────────────────────────────┤ │ │ │ 4. POST /token │ │ code, code_verifier, client_id+secret │ ├──────────────────────────────────────────────────▶│ │ │ │ 5. returns id_token (JWT) │ │ + access_token + refresh_token │ │◀──────────────────────────────────────────────────┤ │ │ │ 6. your BE validates id_token via JWKS │ │ (verifies signature, aud, exp, iss) │ │ │What’s in the id_token
Section titled “What’s in the id_token”The id_token is a signed JWT (RS256 by default). Decoded, the typical payload:
{ "iss": "https://auth.acme.prysmid.com", "sub": "294857293874598734", "aud": "275893745987345987@my-saas-dev", "exp": 1738000000, "iat": 1737996400, "auth_time": 1737996398, "email_verified": true, "name": "Fernando", "preferred_username": "fernando"}sub: stable user identifier in your workspace. Persist it. Don’t useemailas the key — emails change.iss: the issuer. Must be exactlyhttps://auth.<slug>.prysmid.com. If you validate against another string, you’ll accept forged tokens.aud: yourclient_id. If you receive a token with a differentaud, reject it.
How to validate the JWT in your backend →
Scopes that matter
Section titled “Scopes that matter”| Scope | What you get |
|---|---|
openid | Required. Without it, no id_token. |
profile | name, preferred_username, picture. |
email | email, email_verified. |
offline_access | Returns a refresh_token for long-lived sessions. |
urn:zitadel:iam:org:project:roles | User roles in the project tied to your app. (Canonical claim URN; don’t rewrite it in your validator.) |
Refresh tokens
Section titled “Refresh tokens”If you requested offline_access, you receive a refresh_token next to the id_token. Exchange it for a new id_token when the current one expires:
curl -X POST https://auth.acme.prysmid.com/oauth/v2/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d "grant_type=refresh_token" \ -d "refresh_token=$REFRESH_TOKEN"The refresh token rotates on each use. Save the new one, discard the old.
For backend-to-backend apps (no user)
Section titled “For backend-to-backend apps (no user)”If your app calls another app with no human in the loop (machine-to-machine), use client_credentials:
curl -X POST https://auth.acme.prysmid.com/oauth/v2/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d "grant_type=client_credentials" \ -d "scope=openid"You won’t get an id_token (no user), just access_token. See also machine keys for agents if what you want is for an AI agent to operate the control plane.
Logout
Section titled “Logout”GET https://auth.acme.prysmid.com/oidc/v1/end_session ?id_token_hint=<id_token> &post_logout_redirect_uri=https://yourapp.com/logoutThe post_logout_redirect_uri must be registered for your app. The instance ends the session on its side, then bounces back to your URI.