The Seven Standard Claims (RFC 7519)
RFC 7519 §4.1 defines a set of "Registered Claim Names" — short, interoperable keys that every JWT library recognises. They are three-letter abbreviations by design (JSON Web Tokens are transmitted on every HTTP request; every byte saved counts).
| Claim | Full name | Type | Purpose |
|---|---|---|---|
iss | Issuer | string / URI | Who created the token (auth server URL, service name) |
sub | Subject | string | The entity the token is about (user ID, machine ID) |
aud | Audience | string or string[] | Which service(s) should accept this token |
exp | Expiration Time | number (Unix s) | Token is invalid after this timestamp |
nbf | Not Before | number (Unix s) | Token is invalid before this timestamp |
iat | Issued At | number (Unix s) | When the token was created |
jti | JWT ID | string (UUID) | Unique token identifier for revocation / one-time use |
A Full Real-World Example
// Access token for a multi-tenant SaaS application
{
// Standard claims
"iss": "https://auth.myapp.com", // issuer — your auth server
"sub": "usr_9f2c5b3a", // user ID in your DB
"aud": ["https://api.myapp.com", // audience — two services accept this
"https://billing.myapp.com"],
"exp": 1760486400, // expires 2025-10-14 22:40:00 UTC
"iat": 1760482800, // issued 2025-10-14 21:40:00 UTC (1 h token)
"jti": "a8b3c4d5-e6f7-8901-bcde-f2345678abcd",
// Custom claims — namespaced to avoid collisions
"https://myapp.com/roles": ["admin", "editor"],
"https://myapp.com/tenant": "org_42abc",
"https://myapp.com/plan": "pro",
// Un-namespaced custom claims (fine for internal systems)
"email": "[email protected]",
"email_verified": true,
"name": "Alice Nguyen"
}Real-World Decoded Examples
Firebase ID Token
{
"iss": "https://securetoken.google.com/my-firebase-project",
"aud": "my-firebase-project",
"auth_time": 1700000000,
"user_id": "9Xzk7LQmA2TuB8nOdP1fR",
"sub": "9Xzk7LQmA2TuB8nOdP1fR", // same as user_id in Firebase
"iat": 1760400000,
"exp": 1760403600, // 1-hour lifetime
"email": "[email protected]",
"email_verified": true,
"firebase": {
"identities": { "email": ["[email protected]"] },
"sign_in_provider": "password"
}
}Auth0 Access Token
{
"iss": "https://myapp.auth0.com/",
"sub": "auth0|5f7c8a3b2d9e1c4a6b0d2e5f",
"aud": ["https://api.myapp.com", "https://myapp.auth0.com/userinfo"],
"azp": "Yxk2QmTpLnABC", // authorised party (client_id)
"exp": 1760486400,
"iat": 1760400000,
"scope": "openid profile email read:orders",
"permissions": ["read:orders", "write:cart"],
"https://myapp.com/roles": ["customer"]
}AWS Cognito User Pool Token
{
"sub": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XYZ",
"aud": "1abc23defg45678hijklmno", // Cognito App Client ID
"token_use": "id", // "id" or "access"
"cognito:username": "alice",
"cognito:groups": ["admin-group"],
"email_verified": true,
"email": "[email protected]",
"exp": 1760489999,
"iat": 1760486399
}GitHub OAuth Access Token (Note: not a JWT — GitHub uses opaque tokens)
GitHub's OAuth user access tokens (ghu_...) are opaque bearer tokens, not JWTs — you cannot decode them. GitHub's GitHub Apps use JWT for app authentication (not the access token), with a payload like:
{
"iat": 1760486340, // 60 seconds ago (GitHub rejects tokens issued in the future)
"exp": 1760486640, // 5 minutes from iat (GitHub max: 10 minutes)
"iss": "123456" // your GitHub App ID
}Custom Claims — Best Practices
- Namespace with a URI.
"https://myapp.com/roles"avoids collisions with other systems or future RFC additions. - Use arrays for multi-value claims.
roles: ["admin", "editor"]is safer than a comma-separated string — no parser confusion. - Prefer IDs over labels.
tenant_id: "org_42abc"outlives rebrands;tenant_name: "ACME Inc."breaks when the tenant renames. - Do not duplicate the database. A JWT claiming 20 custom fields is probably storing things that belong in a profile lookup. Keep it under 10 claims.
- Version your claim schema. Add a
cv(claim version) claim —1for the first schema,2after a breaking change — so old tokens degrade gracefully.
Decoding the Payload Manually (Any Language)
# The payload is the SECOND dot-segment of the JWT.
# Add back any missing Base64URL padding, then decode.
# JavaScript (browser + Node)
const [, payload] = jwt.split('.');
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
# Python
import base64, json
parts = jwt.split('.')
decoded = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
# Go
import (
"encoding/base64"
"encoding/json"
)
parts := strings.Split(jwt, ".")
b, _ := base64.RawURLEncoding.DecodeString(parts[1])
var payload map[string]interface{}
json.Unmarshal(b, &payload)
# curl + jq
echo $JWT | cut -d. -f2 | base64 -d 2>/dev/null | jq .Related JWT Tools
- JWT Decoder Online — paste any JWT and see all claims instantly
- Verify JWT Signature — confirm a token is authentic
- JWT Expiration Checker — is the
expclaim in the future? - JWT Generator Online — build a JWT with your own claims
- JWT Secret Key Generator — create a signing secret
- JWT Debugger — full decode + verify tool