Access Token Decoder
Decode API access tokens in JWT format to understand their permissions, scope, and validity. Access tokens authorize requests to protected resources.
Access Token Purpose
| Function | Description |
|---|---|
| Authorization | Proves the bearer can access resources |
| Scope | Defines what actions are permitted |
| Identity | Often contains user/client information |
| Expiration | Limits how long access is granted |
Access Token Claims
| Claim | Purpose | Example |
|---|---|---|
| sub | Resource owner | "user_123" |
| client_id | Application | "my_app" |
| scope | Permissions | "read write delete" |
| aud | API identifier | "https://api.example.com" |
| exp | Expiration | 1699900800 |
Access Token Decoder
``javascript
function decodeAccessToken(token) {
const parts = token.split('.');
if (parts.length !== 3) {
return {
format: 'opaque',
note: 'Opaque tokens must be validated via introspection endpoint',
hint: 'POST to /oauth/introspect with token parameter'
};
}
const decode = (s) => JSON.parse(atob(s.replace(/-/g, '+').replace(/_/g, '/')));
const payload = decode(parts[1]);
// Parse scopes
const scopes = payload.scope
? payload.scope.split(' ')
: payload.scp || [];
// Calculate remaining validity
const now = Math.floor(Date.now() / 1000);
const remainingSeconds = payload.exp ? payload.exp - now : null;
return {
format: 'jwt',
subject: payload.sub,
clientId: payload.client_id || payload.azp,
audience: payload.aud,
scopes,
permissions: payload.permissions || [],
issuedAt: payload.iat ? new Date(payload.iat * 1000) : null,
expiresAt: payload.exp ? new Date(payload.exp * 1000) : null,
remainingTime: remainingSeconds > 0
? ${Math.floor(remainingSeconds / 60)} minutes
: 'EXPIRED',
isExpired: remainingSeconds <= 0
};
}
`
Common Scopes by Provider
| Provider | Scopes | Purpose |
|---|---|---|
| gmail.readonly, drive.file | Google API access | |
| GitHub | repo, user:email | Repository and user access |
| Microsoft | User.Read, Mail.Send | Microsoft Graph access |
| Custom API | read:users, write:posts | Your API permissions |
Access Token Lifecycle
`
1. Client requests token (authorization code, client credentials, etc.)
2. Auth server issues access token (+ optional refresh token)
3. Client sends token in Authorization header
4. Resource server validates token
5. Token expires → use refresh token for new access token
`
The Attacks a Verifier Must Stop
alg: none. A token declaring no algorithm with an empty signature. A library that
trusts the header's alg will accept it as valid. Always specify the expected algorithm
when verifying rather than reading it from the token.
Algorithm confusion. A token signed with HMAC using the server's *public* RSA key as the
secret. If the verifier picks the algorithm from the header, an RS256 verifier can be tricked
into running HS256 with a key the attacker already has.
Weak secrets. HS256 with a short or dictionary secret is brute-forceable offline from a
single captured token. Use at least 256 bits of real entropy.
No expiry check. exp is a claim, not an enforcement. A library that decodes without
verifying, or code that reads claims from a decoded-but-unverified token, accepts expired and
forged tokens alike.
`javascript
// Specify the algorithm; never trust the header's
jwt.verify(token, secret, { algorithms: ['HS256'] });
`
Anyone Can Read the Payload
A JWT is signed, not encrypted. The payload is base64url — readable by anyone holding the
token, including the browser it is stored in. Never put anything in it you would not print
on a postcard: no passwords, no PII beyond an identifier, no internal keys.
Revocation Is the Hard Part
A signed token is valid until it expires, and there is no way to withdraw one. Options:
| Approach | Cost |
|---|---|
| Short expiry (5–15 min) plus refresh tokens | Standard; adds a refresh endpoint |
| A denylist of revoked JTIs | Reintroduces the state JWTs were meant to avoid |
| Rotate the signing key | Revokes every token at once |
| Version claim checked against the user record | A database read per request |
If you need immediate revocation for every session, a server-side session is a simpler and
more honest choice than a JWT.Where to Store One
| Location | XSS-safe | CSRF-safe |
|---|---|---|
localStorage | No | Yes |
| sessionStorage` | No | Yes |
| Cookie (HttpOnly, Secure, SameSite) | Yes | Yes, with SameSite |
| In-memory | Yes | Yes |
An HttpOnly cookie is the safest default. Any token reachable from JavaScript is reachable by
any script that gets injected.