Skip to main content

Validating tokens in your backend

IdentSphere is a service. You run one identsphere-server binary (or, if you write Rust, embed it in your Axum app), and a backend in any language integrates the same way it would with Auth0, Clerk, Keycloak, Supabase GoTrue, Ory, or SuperTokens. There is nothing IdentSphere-specific to learn beyond the HTTP contract and the token format — both are standard.

There are exactly two things your backend does, and they live on two different paths.

The two paths

Path 1 — Auth flows (login, register, refresh, MFA)

Login, registration, token refresh, MFA enrollment, passkeys, OAuth, password reset — every flow that mutates auth state — happens by calling the IdentSphere HTTP API. Your frontend or backend POSTs to identsphere-server; IdentSphere does the work and hands back a token pair.

Use the per-language SDK or the raw REST endpoints for these. They are not on the hot path — they happen once per session, not once per request.

Path 2 — Per-request authorization (the hot path)

Every subsequent request carries the access token IdentSphere issued. Your backend authorizes that request by validating the token's RS256 signature locally, against the public keys it cached from /.well-known/jwks.json.

once, then cached
┌─────────────────────┐ ◄── JWKS fetch ──── ┌─────────────────────┐
│ Your backend │ │ identsphere-server │
│ (any language) │ ── login/refresh ──► │ publishes │
│ │ ◄── token pair ──── │ /.well-known/ │
│ validates the JWT │ │ jwks.json │
│ LOCALLY per req │ └─────────────────────┘
└─────────────────────┘

│ zero network hop per request

[your business logic]

This is the part people overthink. There is no network hop per request, no FFI, and no per-language IdentSphere binary. Your backend uses its language's standard JWT library plus the JWKS URL — about ten lines of glue — and validation runs entirely in-process. A request that arrives with a valid, unexpired, correctly-signed token is authorized without ever touching identsphere-server.

This is the industry-standard model

Local JWKS validation is how every major auth provider expects a backend to authorize requests. Auth0, Clerk, Keycloak, Supabase GoTrue, Ory, and SuperTokens all issue RS256 JWTs and publish a JWKS; backends validate locally against it. IdentSphere works the same way on purpose — your existing JWT middleware, your team's existing knowledge, and the standard libraries all transfer directly.

note

The Rust in-process embed (depending on identsphere-axum as a crate so the auth routes and middleware live inside your Axum process) is an optional extra for Rust shops who want zero deployment surface. It is not the model. The model is: run the service, validate tokens locally. See Rust (Axum) if you want the embed.

What a verified token carries

Once your library has verified the signature, you read the claims. An IdentSphere access token carries:

ClaimTypeMeaning
substring (UUID)User ID — the authenticated subject
orgstring (UUID)Organization ID this session is scoped to
emailstringUser email, when available
aalstringAuthentication Assurance Level: aal1 (single factor) / aal2 (MFA-verified)
expint (Unix ts)Expiration — your library checks this for you
issstringIssuer — matches IDENTSPHERE_ISSUER (default IdentSphere)
audstringAudience — present only when the server sets a token audience (see below)

The full claim set (iat, typ, sid, jti, auth_time, auth_method, picture, name, azp) is documented in the REST guide's claims table. Treat every claim except sub, exp, iat, and typ as optional in your language's type.

note

The aud check is opt-in and pairs with the server's IDENTSPHERE_TOKEN_AUDIENCE. If you set IDENTSPHERE_TOKEN_AUDIENCE on identsphere-server, every access token gets that aud claim and your backend should pass the matching expected audience to its verifier. If you leave it unset (the default), the claim is omitted and you skip the audience check.

What every backend must enforce

These snippets are security-relevant. Whatever language you use, the verifier must:

  1. Pin the algorithm to RS256. Never let the library accept none or an HS256 token signed with a key an attacker controls. Always pass an explicit allowed-algorithms list.
  2. Verify the signature against the JWKS public key matched by the token's kid header.
  3. Check exp. Reject expired tokens. (Most libraries do this by default — confirm it is on.)
  4. Check iss equals your configured issuer.
  5. Check aud if you configured IDENTSPHERE_TOKEN_AUDIENCE.

Per-language snippets

Each language guide has a Verify tokens in your backend section with a copy-pasteable verifier built on that language's standard JWT library:

  • Node.js@identsphere/verify (wraps jose)
  • Python — PyJWT PyJWKClient
  • GoIdentSphere/sdks/go/verify (wraps keyfunc + golang-jwt)
  • Java / Kotlin — Nimbus JOSE+JWT or Spring Security
  • PHPfirebase/php-jwt
  • Ruby — the jwt gem
  • Rustidentsphere-axum middleware, or jsonwebtoken + JWKS

Where the browser stores the refresh token

For a same-origin web app (SPA served from the same origin as IdentSphere, or behind a same-origin reverse proxy / BFF), the refresh token lives in the httpOnly identsphere_rt cookie — JS never touches it. Most secure, no config (the default SameSite=Lax).

For a cross-origin SPA (different origins), a SameSite=Lax cookie isn't sent on the SPA's cross-origin fetch, so there are two clean options:

  • Set IDENTSPHERE_COOKIE_SAMESITE=None — the httpOnly refresh cookie is then sent on a cross-origin credentialed fetch (credentials: "include"), so the SPA gets httpOnly refresh with no BFF and no localStorage. The SPA's origin must be in CORS_ALLOW_ORIGINS; CSRF stays covered by the double-submit token; Secure is forced automatically (browsers require it with SameSite=None).
  • Put a same-origin BFF / reverse proxy in front of IdentSphere — then it's the same-origin case above.

Either keeps the refresh token out of JS. Native apps (iOS/Android) instead store tokens in the platform keychain (e.g. expo-secure-store).

A note on revocation

Local validation means a token stays valid until it expires, even if the session was revoked server-side in the meantime. The revocation lag is bounded by (JWKS cache TTL + access-token TTL). Shorten IDENTSPHERE_ACCESS_EXPIRY_SECS (default 900) to tighten it — see choosing an integration pattern.