docs›Guides›Verify a user's JWT Verify a user's JWT in your backend
Your own backend can check that a request comes from a signed-in user, with any JOSE library, using only the project URL.
6 min read·Level intermediate·Revised on Oct 25, 2026
This English text was generated automatically from the French original and has not been reviewed yet.
#Tokens
1. How tokens are signed
Each project has its own ES256 (ECDSA P-256) key. The access token header carries the kid of that key, and the claims carry:
iss: <project URL>/v1/auth (for example https://my-app-x1y2z3.aurabase.cloud/v1/auth),
aud: the project identifier,
sub: the user id, plus role, email, app_metadata and user_metadata.
The public keys are served, without any API key, at <project URL>/.well-known/jwks.json. No secret is shared with your backend, and a token of one project is rejected by the keys of another.
Studio › Settings › API shows the JWKS URL, the issuer and the current kid.
verify.tstypescript
import { createRemoteJWKSet, jwtVerify } from 'jose'
const PROJECT_URL = process.env.AURABASE_URL! // https://<slug>.aurabase.cloud
const PROJECT_ID = process.env.AURABASE_PROJECT_ID!
// jose caches the keys and refetches when it meets an unknown kid (rotation).
const jwks = createRemoteJWKSet(new URL(`${PROJECT_URL}/.well-known/jwks.json`))
export async function verifyAccessToken(token: string) {
const { payload } = await jwtVerify(token, jwks, {
issuer: `${PROJECT_URL}/v1/auth`,
audience: PROJECT_ID,
algorithms: ['ES256'],
})
return payload // payload.sub is the user id
}
verify.pypython
import os
import jwt # pip install "pyjwt[crypto]"
PROJECT_URL = os.environ["AURABASE_URL"]
PROJECT_ID = os.environ["AURABASE_PROJECT_ID"]
jwks = jwt.PyJWKClient(f"{PROJECT_URL}/.well-known/jwks.json")
def verify_access_token(token: str) -> dict:
key = jwks.get_signing_key_from_jwt(token).key
return jwt.decode(
token,
key,
algorithms=["ES256"],
issuer=f"{PROJECT_URL}/v1/auth",
audience=PROJECT_ID,
)
verify.gogo
package main
import (
"fmt"
"os"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
func verify(token string) (jwt.MapClaims, error) {
projectURL := os.Getenv("AURABASE_URL")
jwks, err := keyfunc.NewDefault([]string{projectURL + "/.well-known/jwks.json"})
if err != nil {
return nil, err
}
claims := jwt.MapClaims{}
_, err = jwt.ParseWithClaims(token, claims, jwks.Keyfunc,
jwt.WithValidMethods([]string{"ES256"}),
jwt.WithIssuer(projectURL+"/v1/auth"),
jwt.WithAudience(os.Getenv("AURABASE_PROJECT_ID")),
)
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
return claims, nil
}
Rotating the key in Studio (or with POST /v1/auth/admin/signing-keys/rotate and the service_role key) makes a new key sign every new token. The previous key stays in the JWKS for 24 hours, so tokens signed before the rotation keep verifying; a key that leaked can be revoked with DELETE /v1/auth/admin/signing-keys/<kid> once it is no longer the active one, and is then removed from the JWKS and refused.
Info
In the JavaScript SDK, aura.auth.getClaims() already verifies the session token locally with this JWKS and returns its real claims.
Last updated · Oct 25, 2026