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.
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:
| Claim | Type | Meaning |
|---|---|---|
sub | string (UUID) | User ID — the authenticated subject |
org | string (UUID) | Organization ID this session is scoped to |
email | string | User email, when available |
aal | string | Authentication Assurance Level: aal1 (single factor) / aal2 (MFA-verified) |
exp | int (Unix ts) | Expiration — your library checks this for you |
iss | string | Issuer — matches IDENTSPHERE_ISSUER (default IdentSphere) |
aud | string | Audience — 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.
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:
- Pin the algorithm to
RS256. Never let the library acceptnoneor an HS256 token signed with a key an attacker controls. Always pass an explicit allowed-algorithms list. - Verify the signature against the JWKS public key matched by the token's
kidheader. - Check
exp. Reject expired tokens. (Most libraries do this by default — confirm it is on.) - Check
issequals your configured issuer. - Check
audif you configuredIDENTSPHERE_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(wrapsjose) - Python — PyJWT
PyJWKClient - Go —
IdentSphere/sdks/go/verify(wrapskeyfunc+golang-jwt) - Java / Kotlin — Nimbus JOSE+JWT or Spring Security
- PHP —
firebase/php-jwt - Ruby — the
jwtgem - Rust —
identsphere-axummiddleware, orjsonwebtoken+ 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 nolocalStorage. The SPA's origin must be inCORS_ALLOW_ORIGINS; CSRF stays covered by the double-submit token;Secureis forced automatically (browsers require it withSameSite=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.