SenseCrypt Docs
Reference

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_token and 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" }
  • alg is always ES256 (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"]. Pin ES256 in your verifier and reject anything else — never trust the token's own alg to tell you how to verify it.
  • kid is the RFC 7638 thumbprint of the signing public key. You use it to pick the right key out of the JWKS.

The steps

  1. Resolve the tenant's jwks_uri from discovery. Fetch {issuer}/.well-known/openid-configuration and read jwks_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.)

  2. Fetch the JWKS. GET the jwks_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": "…"
        }
      ]
    }
  3. Select the key by kid. Match the token header's kid against the kid of 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).

  4. Verify the ES256 signature against the selected key's public coordinates.

  5. 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:

ClaimCheck
issMust 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.
expMust be in the future. Allow only a small clock-skew leeway (a minute or two), not more.
audMust contain the audience you expect (see below).
iatPresent on every token; some libraries let you require it.

What aud should be depends on which token and how it was requested:

  • id_tokenaud is your client_id. An id_token authenticates the user to your app; check aud == your_client_id.
  • Access token, no API audience requestedaud is also your client_id (the default binding).
  • Access token with an API audience (requested via ?audience= or ?resource=) — aud is the resource server's audience (e.g. https://api.your-app.example). An API validating an incoming access token must check aud equals 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).

sub is stable, but not the database id. Once validated, sub is 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 a sub from 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:

  1. Select by kid, tolerate multiple keys. Never assume the JWKS has exactly one key, and never pin a single key. Pick the one whose kid matches the token.
  2. Re-fetch on an unknown kid. If a token's kid is 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 bogus kids 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.

Python (PyJWT)
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")
Node / TypeScript (jose)
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 pin ES256 in the verifier. Accepting whatever alg the header claims is the classic algorithm-confusion vulnerability.
  • Pinning one JWKS key. Rotation publishes multiple keys; select by kid and 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 check aud.
  • Accepting an id_token at an API. Require token_use == "access" on API credentials; the id_token has no token_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.

On this page