Validate a SenseCrypt token
The canonical recipe for validating a SenseCrypt JWT — fetch the tenant JWKS from jwks_uri, select the key by kid, verify the ES256 signature, and check iss, aud, exp, and token_use, with JWKS caching and key-rotation handling.
Every token SenseCrypt issues — the id_token and the access token — is a JWT signed with ES256 using the tenant's own key. Anyone can hand you one of these tokens; validating it is what turns "a string the caller sent" into "a claim you can trust." This page is the canonical, end-to-end recipe. The quickstarts and other guides point here rather than repeating it.
If you use a mature OIDC or JWT library, it does all of this for you — you mostly just supply the issuer and your expected audience. The steps below are what a correct library does under the hood, so you can configure one properly or implement validation yourself when you have to.
Refresh tokens are not JWTs. They are opaque strings — do not try to parse or validate them. This page is about the
id_tokenand the access token only. See Tokens & sessions.
What you're validating
A decoded SenseCrypt JWT header always looks like this:
{ "alg": "ES256", "kid": "<jwk-thumbprint>", "typ": "JWT" }algis alwaysES256(ECDSA on the NIST P-256 curve). This is the only signing algorithm SenseCrypt uses for tokens, and discovery advertises exactly"id_token_signing_alg_values_supported": ["ES256"]. PinES256in your verifier and reject anything else — never trust the token's ownalgto tell you how to verify it.kidis the RFC 7638 thumbprint of the signing public key. You use it to pick the right key out of the JWKS.
The steps
-
Resolve the tenant's
jwks_urifrom discovery. Fetch{issuer}/.well-known/openid-configurationand readjwks_uri. Never hard-code the JWKS path — resolve it from discovery so it survives any future change. (The current value is{issuer}/.well-known/jwks.json, but discovery is the contract.) -
Fetch the JWKS.
GETthejwks_uri. It returns a JSON Web Key Set — one or more EC P-256 public keys:{ "keys": [ { "kty": "EC", "crv": "P-256", "x": "…", "y": "…", "use": "sig", "alg": "ES256", "kid": "…" } ] } -
Select the key by
kid. Match the token header'skidagainst thekidof each key in the set. Expect more than one key — during a signing-key rotation the JWKS publishes both the new and the outgoing key for an overlap window (see Handle key rotation). -
Verify the ES256 signature against the selected key's public coordinates.
-
Check the claims (below). A valid signature only proves SenseCrypt minted the token — the claims prove it was minted for you and is still current.
The claims to check
Check these on every token, after the signature verifies:
| Claim | Check |
|---|---|
iss | Must equal the tenant issuer you expect (the same issuer whose JWKS you verified against). This is your defence against a token from another tenant or another IdP. |
exp | Must be in the future. Allow only a small clock-skew leeway (a minute or two), not more. |
aud | Must contain the audience you expect (see below). |
iat | Present on every token; some libraries let you require it. |
What aud should be depends on which token and how it was requested:
id_token—audis yourclient_id. An id_token authenticates the user to your app; checkaud == your_client_id.- Access token, no API audience requested —
audis also yourclient_id(the default binding). - Access token with an API audience (requested via
?audience=or?resource=) —audis the resource server's audience (e.g.https://api.your-app.example). An API validating an incoming access token must checkaudequals its own audience, and reject tokens minted for anything else. See Tokens & sessions for the full access-token shape.
Check token_use to tell the two apart
An access token carries "token_use": "access"; the id_token does not carry token_use at all. Use this to enforce the right token in the right place:
- Your API should require
token_use == "access"and reject an id_token presented as a bearer token. An id_token is proof of a login event, not an API credential — do not accept it at an API. - Do not treat an access token as proof of login. For "who is signed in," use the id_token (or call UserInfo with the access token).
subis stable, but not the database id. Once validated,subis a stable, per-tenant pseudonymous identifier — safe to use as your application's primary key for the user. It is not SenseCrypt's internal row id, and asubfrom one tenant is meaningless in another.
Cache the JWKS
Fetching the JWKS on every token check is unnecessary and slow. Cache it — the keys change only when a tenant rotates its signing key, which is rare. Most JWKS clients cache for you (for example, jwks_uri-aware clients in the common libraries). If you cache yourself, a short TTL (minutes to an hour) is fine, provided you also handle the unknown-kid case below so a rotation is never blocked by a stale cache.
Handle key rotation
Signing keys rotate. SenseCrypt uses make-before-break rotation: the new key is published in the JWKS before it starts signing, so both keys are present during an overlap window and tokens signed on either side verify. Two rules make your verifier rotation-proof:
- Select by
kid, tolerate multiple keys. Never assume the JWKS has exactly one key, and never pin a single key. Pick the one whosekidmatches the token. - Re-fetch on an unknown
kid. If a token'skidis not in your cached JWKS, a rotation likely just happened — re-fetch the JWKS once and retry the lookup before rejecting the token. (Guard this so a flood of boguskids can't make you hammer the endpoint — for example, cap the re-fetch rate.) Most JWKS-caching clients do this automatically.
Operators can rotate a tenant's OIDC signing key on demand via the signing-key rotation endpoint; the outgoing key stays published through the grace window so in-flight tokens keep verifying.
Worked examples
These use the tenant's discovery document to find the JWKS, cache it, select by kid, and pin ES256. Both libraries also re-fetch on an unknown kid for you.
import jwt
from jwt import PyJWKClient
# disco = the discovery document, fetched once from
# {issuer}/.well-known/openid-configuration
jwks_client = PyJWKClient(disco["jwks_uri"]) # caches + refetches on unknown kid
def validate(token: str, *, expected_aud: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token) # selects by kid
return jwt.decode(
token,
signing_key.key,
algorithms=["ES256"], # pin ES256 — do not trust the token's alg
issuer=disco["issuer"], # iss must be the tenant issuer
audience=expected_aud, # your client_id, or your API's audience
options={"require": ["exp", "iat", "iss", "aud", "sub"]},
)For an access token at an API, additionally assert token_use:
claims = validate(access_token, expected_aud="https://api.your-app.example")
if claims.get("token_use") != "access":
raise ValueError("expected an access token")import { createRemoteJWKSet, jwtVerify } from "jose";
// disco = the discovery document, fetched once from
// {issuer}/.well-known/openid-configuration
const JWKS = createRemoteJWKSet(new URL(disco.jwks_uri)); // caches + refetches on unknown kid
export async function validate(token: string, expectedAud: string) {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ["ES256"], // pin ES256
issuer: disco.issuer, // iss must be the tenant issuer
audience: expectedAud, // your client_id, or your API's audience
});
return payload;
}Common mistakes
- Trusting the token's
alg. Always pinES256in the verifier. Accepting whateveralgthe header claims is the classic algorithm-confusion vulnerability. - Pinning one JWKS key. Rotation publishes multiple keys; select by
kidand re-fetch on an unknown one. - Skipping
aud. A signature-valid token minted for a different client or API is still not for you. Always checkaud. - Accepting an id_token at an API. Require
token_use == "access"on API credentials; the id_token has notoken_use. - Verifying against the wrong issuer. The JWKS you verify against must belong to the token's
iss. In a multi-tenant deployment each tenant is its own issuer with its own keys — see Multi-tenancy.
Related
- Tokens & sessions — the full claim-by-claim shape of the id_token and access token.
- OIDC & OAuth 2.0 — discovery and the flows that mint these tokens.
- Multi-tenancy — per-tenant issuers, per-tenant keys, and the
kidselection rule. - JWKS endpoint — the endpoint reference.
CORS
How cross-origin browser access to SenseCrypt works — the backend registers no CORS middleware of its own, so CORS is terminated at the operator's reverse proxy, and confidential apps should prefer server-side token exchange.
Glossary
Definitions of the terms used across the SenseCrypt documentation — protocol, authorization, key-custody, and device concepts, in the precise sense SenseCrypt uses them.