🔥 0
0
Lesson 3 of 6 20 min +200 XP

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();
Security Warning

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's Security Model

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.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What is the typical lifetime of an access token?

2

What are the three parts of a JWT?

3

When should you use the refresh token?

OAuth 2.0 Flows