Understanding Tokens
Tokens are the currency of OAuth. Understanding how they work is crucial for building secure applications.
Token Types
ACCESS TOKEN
The "key card" to access resources
- Short-lived (minutes to hours)
- Sent with every API request
- Contains scopes (permissions)
- If leaked, limited damage window
REFRESH TOKEN
Gets you new access tokens
- Long-lived (days to months)
- Stored securely, used rarely
- Never sent to resource servers
- Can be revoked to end sessions
The Token Lifecycle
┌─────────────────────────────────────────────────────────────────┐
│ TOKEN LIFECYCLE │
└─────────────────────────────────────────────────────────────────┘
User Login API Calls Token Refresh
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ OAuth │ │ Access │ │ Refresh │
│ Flow │───────>│ Token │───exp──>│ Token │
└─────────┘ │ (1 hour) │ │ (30 days) │
└─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ API │ │ New │
│ Request │ │ Access │
└─────────────┘ │ Token │
└─────────────┘
Access Token Usage
// Making API requests with access token
const response = await fetch('https://api.github.com/user', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json'
}
});
// The server validates the token and returns data
const user = await response.json();
Token Refresh Flow
// When access token expires, use refresh token to get a new one
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken,
grant_type: 'refresh_token'
})
});
const tokens = await response.json();
return {
accessToken: tokens.access_token,
// Some providers rotate refresh tokens
refreshToken: tokens.refresh_token || refreshToken
};
}
// Example: Auto-refresh before expiration
class TokenManager {
constructor(tokens) {
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token;
this.expiresAt = Date.now() + (tokens.expires_in * 1000);
}
async getValidToken() {
// Refresh 5 minutes before expiration
if (Date.now() > this.expiresAt - 300000) {
const newTokens = await refreshAccessToken(this.refreshToken);
this.accessToken = newTokens.accessToken;
this.refreshToken = newTokens.refreshToken;
this.expiresAt = Date.now() + 3600000; // 1 hour
}
return this.accessToken;
}
}
JWT: JSON Web Tokens
Most modern OAuth implementations use JWTs (pronounced "jots") for access tokens.
JWT Structure
A JWT has three parts separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA
HEADER
{
"alg": "RS256",
"typ": "JWT"
}
Algorithm + token type
PAYLOAD (Claims)
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
User data + metadata
SIGNATURE
RSASHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
privateKey
)
Proves token wasn't tampered
Common JWT Claims
| Claim | Name | Description |
|---|---|---|
| iss | Issuer | Who created the token |
| sub | Subject | User ID |
| aud | Audience | Intended recipient |
| exp | Expiration | When token expires (Unix timestamp) |
| iat | Issued At | When token was created |
| scope | Scope | Permissions granted |
Decoding JWTs
// JWTs can be decoded (but not verified) client-side
function decodeJWT(token) {
const [header, payload, signature] = token.split('.');
return {
header: JSON.parse(atob(header)),
payload: JSON.parse(atob(payload)),
signature: signature
};
}
// Example
const decoded = decodeJWT(accessToken);
console.log(decoded.payload);
// { sub: "user123", name: "John", exp: 1699999999, scope: "read write" }
// Check if expired
const isExpired = decoded.payload.exp * 1000 < Date.now();
JWT payload is only base64-encoded, NOT encrypted. Anyone can decode it. Never put sensitive data in JWTs. The signature only proves the token wasn't modified - it doesn't hide the contents.
ID Tokens vs Access Tokens
ACCESS TOKEN
- Used to access APIs
- Sent to resource servers
- Audience: the API
- May or may not be a JWT
ID TOKEN (OIDC)
- Contains user identity info
- Used by your app only
- Audience: your app
- Always a JWT
Opaque vs Self-Contained Tokens
| Type | Example | Validation |
|---|---|---|
| Opaque | gho_16C7e42F292c6912E7710c838347Ae178B4a | Must call auth server |
| Self-contained (JWT) | eyJhbGciOiJSUzI1NiIs... | Verify signature locally |
Real-World: Stripe's Token Strategy
Stripe uses different token patterns for different purposes:
// API Key (long-lived, opaque) - for server-to-server
const stripe = require('stripe')('sk_live_...');
// Publishable Key (restricted) - for client-side
// pk_live_... can only create tokens, not read data
// OAuth Access Token - for connected accounts
const response = await stripe.oauth.token({
grant_type: 'authorization_code',
code: 'ac_...'
});
// Returns access_token (for API) and refresh_token
Stripe uses restricted API keys (pk_) for client-side code that can only create tokens. The secret key (sk_) stays on your server. This limits exposure if client-side code is compromised.
Token Storage Best Practices
DO
- Store refresh tokens server-side
- Use httpOnly cookies for web apps
- Encrypt tokens at rest
- Use secure/encrypted storage on mobile
DON'T
- Store tokens in localStorage (XSS vulnerable)
- Log tokens in plaintext
- Include tokens in URLs
- Share tokens between devices
Key Takeaways
- Access tokens are short-lived - Minutes to hours, limits damage if leaked
- Refresh tokens get new access tokens - Without re-authentication
- JWTs are self-contained - Header + Payload + Signature
- JWTs are encoded, not encrypted - Don't put secrets in them
Next up: OpenID Connect - Building identity on top of OAuth 2.0.