Auth & Security/
Lesson

Getting tokens from an OAuthWhat is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password. providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually. is only the beginning. The harder problem is what you do with them afterward. Access tokens expire, refresh tokens can be revoked, and storing credentials insecurely can hand attackers long-term access to your users' accounts. This lesson covers the full lifecycle: secure storage, transparent renewal, and clean revocation.

Access tokens vs refresh tokens

These two tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. types serve different purposes and have very different security requirements:

PropertyAccess tokenRefresh token
Lifetime15 minutes to 1 hourDays, months, or until revoked
PurposeAuthorize API callsObtain new access tokens
FormatUsually a JWTOpaque string
Where to storeServer-side session or httpOnly cookieEncrypted in your database
Exposure riskLow (short-lived)High (must be protected carefully)

An access token is like a day-pass wristband at a concert, it works for the day, then it is worthless. A refresh tokenWhat is refresh token?A long-lived credential used solely to obtain new short-lived access tokens without requiring the user to log in again. is like the receipt you can exchange for a new wristband, you need to keep it safe.

Access token JWTs can be decoded without a key (they are base64-encoded). Never put sensitive data in them that you do not want users to see.
02

TokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. storage security

The most common mistake developers make is storing tokens in localStorage. It is convenient, it survives page refreshes, and every OAuthWhat is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password. tutorial shows it. It is also a serious security hole because any JavaScript on your page, including injected XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. scripts, can read it.

// Never do this - localStorage is readable by any script on the page
localStorage.setItem('access_token', token);
localStorage.setItem('refresh_token', token);

// Never return tokens raw to the client
res.json({ accessToken, refreshToken });  // Wrong!

Instead, keep tokens on the server and use an opaque sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions. to identify the user:

// Server-side: store the refresh token encrypted in your database
await db.updateUser(userId, {
  oauth_refresh_token: encrypt(refreshToken),
  token_expires_at: new Date(Date.now() + expiresIn * 1000)
});

// Set an httpOnly session cookie - JavaScript cannot read this
res.cookie('session', sessionId, {
  httpOnly: true,      // Not accessible via JavaScript
  secure: true,        // HTTPS only
  sameSite: 'strict',  // CSRF protection
  maxAge: 3600000      // 1 hour
});
03

Refreshing access tokens

When an access tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. expires, you use the refresh tokenWhat is refresh token?A long-lived credential used solely to obtain new short-lived access tokens without requiring the user to log in again. to get a new one without asking the user to log in again. Here is a complete refresh function:

async function refreshAccessToken(userId) {
  const user = await db.getUser(userId);
  const refreshToken = decrypt(user.oauth_refresh_token);

  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      refresh_token: refreshToken,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      grant_type: 'refresh_token'
    })
  });

  if (!response.ok) {
    if (response.status === 400) {
      // The refresh token has been revoked or expired
      throw new Error('REFRESH_TOKEN_INVALID');
    }
    throw new Error('Token refresh failed');
  }

  const tokens = await response.json();

  // Google sometimes returns a new refresh token (token rotation)
  if (tokens.refresh_token) {
    await db.updateUser(userId, {
      oauth_refresh_token: encrypt(tokens.refresh_token)
    });
  }

  // Always update the expiration timestamp
  await db.updateUser(userId, {
    access_token_expires_at: new Date(Date.now() + tokens.expires_in * 1000)
  });

  return tokens.access_token;
}
Google does not always return a new refresh token on every refresh. It only does so when you used prompt: 'consent' during authorization or when token rotation is triggered. Store the new one when it arrives; keep the old one otherwise.
04

Silent refresh middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.

You want users to stay logged in seamlessly. The trick is to refresh tokens proactively, before they expire, rather than reactively after a failed APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. call. A middleware function that runs on every request is a clean place to do this:

async function tokenRefreshMiddleware(req, res, next) {
  const userId = req.session.userId;
  if (!userId) return next();  // Not logged in, skip

  const user = await db.getUser(userId);
  const expiresAt = user.access_token_expires_at;
  const fiveMinutes = 5 * 60 * 1000;

  // Refresh if the token will expire within 5 minutes
  if (expiresAt && Date.now() > expiresAt - fiveMinutes) {
    try {
      await refreshAccessToken(userId);
    } catch (error) {
      if (error.message === 'REFRESH_TOKEN_INVALID') {
        // Token revoked - clear session and ask user to log in again
        req.session.destroy();
        return res.status(401).json({ error: 'Session expired' });
      }
    }
  }

  next();
}

// Apply to all protected routes
app.use('/api', tokenRefreshMiddleware);

The 5-minute buffer handles the case where a tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. expires during a slow API call. Without it, you might start a request with a valid token that expires before the response arrives.

05

TokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. rotation

Some providers (and you can implement this yourself too) use token rotation: every time you exchange a refresh tokenWhat is refresh token?A long-lived credential used solely to obtain new short-lived access tokens without requiring the user to log in again., the old one is immediately invalidated and a new one is issued. This limits the damage if an old refresh token leaks, an attacker cannot use it because it has already been replaced.

If your database still has the old refresh token when a new one arrives, something is wrong. You can detect token reuse attacks this way:

async function storeRotatedRefreshToken(userId, newRefreshToken, oldRefreshToken) {
  const user = await db.getUser(userId);
  const storedToken = decrypt(user.oauth_refresh_token);

  if (storedToken !== oldRefreshToken) {
    // Token reuse detected - someone may have used a stolen refresh token
    // Clear all tokens for this user and force re-authentication
    await db.clearTokens(userId);
    throw new Error('TOKEN_REUSE_DETECTED');
  }

  await db.updateUser(userId, {
    oauth_refresh_token: encrypt(newRefreshToken)
  });
}
06

TokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. revocation

When a user logs out, you should do three things: revoke the refresh tokenWhat is refresh token?A long-lived credential used solely to obtain new short-lived access tokens without requiring the user to log in again. with the providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually. so it cannot be used again, clear it from your database, and destroy the sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request..

async function revokeToken(token) {
  // Google's revocation endpoint
  await fetch('https://oauth2.googleapis.com/revoke', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ token })
  });
}

app.post('/logout', async (req, res) => {
  const userId = req.session.userId;

  if (userId) {
    const user = await db.getUser(userId);

    if (user.oauth_refresh_token) {
      try {
        await revokeToken(decrypt(user.oauth_refresh_token));
      } catch (e) {
        // Log but do not block logout if revocation fails
        console.error('Failed to revoke token:', e);
      }
    }

    // Always clear from the database regardless of revocation outcome
    await db.updateUser(userId, {
      oauth_refresh_token: null,
      access_token_expires_at: null
    });
  }

  req.session.destroy();
  res.clearCookie('session');
  res.json({ success: true });
});
Do not block the logout flow if token revocation fails. The provider's server might be temporarily unavailable. Clear the local session and tokens regardless, and log the revocation failure for monitoring.
07

Making authenticated APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. calls

When you need to call a providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually. API (like GitHub's API to list a user's repositories), wrap the fetch call in a helper that handles tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. refresh transparently:

class OAuthApiClient {
  constructor(userId) {
    this.userId = userId;
  }

  async request(url, options = {}) {
    const user = await db.getUser(this.userId);

    // Refresh proactively if token expires within 60 seconds
    if (Date.now() > user.access_token_expires_at - 60000) {
      await refreshAccessToken(this.userId);
    }

    const freshUser = await db.getUser(this.userId);
    const accessToken = decrypt(freshUser.oauth_access_token);

    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      }
    });

    // 401 means the token was rejected - try one refresh and retry
    if (response.status === 401) {
      const newToken = await refreshAccessToken(this.userId);
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${newToken}`
        }
      });
    }

    return response;
  }
}

// Usage
const client = new OAuthApiClient(userId);
const response = await client.request('https://api.github.com/user/repos');
08

Quick reference

ScenarioWhat to do
Access token expiredUse refresh token to get a new one
Refresh token invalid (400)Clear all tokens, return 401, require re-login
User logs outRevoke refresh token with provider, clear database, destroy session
Provider sends new refresh tokenStore it immediately, discard the old one
Token reuse detectedClear all tokens for the user, log the incident
API call returns 401Refresh once and retry; if it fails again, force re-login
javascript
// Complete token management system
import crypto from 'crypto';

class TokenManager {
  constructor(db, encryptionKey) {
    this.db = db;
    this.encryptionKey = encryptionKey;
  }

  encrypt(text) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher('aes-256-gcm', this.encryptionKey);
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    const authTag = cipher.getAuthTag();
    return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
  }

  decrypt(encryptedData) {
    const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
    const decipher = crypto.createDecipher('aes-256-gcm', this.encryptionKey);
    decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }

  async storeTokens(userId, tokens) {
    const updates = {
      oauth_access_token: tokens.access_token ? this.encrypt(tokens.access_token) : undefined,
      oauth_refresh_token: tokens.refresh_token ? this.encrypt(tokens.refresh_token) : undefined,
      token_expires_at: tokens.expires_in
        ? new Date(Date.now() + tokens.expires_in * 1000)
        : undefined
    };

    await this.db.updateUser(userId, updates);
  }

  async getValidAccessToken(userId) {
    const user = await this.db.getUser(userId);

    if (!user.oauth_access_token) {
      throw new Error('No access token found');
    }

    const expiresAt = new Date(user.token_expires_at).getTime();
    const fiveMinutes = 5 * 60 * 1000;

    // Refresh if expiring soon
    if (Date.now() > expiresAt, fiveMinutes) {
      return await this.refreshToken(userId);
    }

    return this.decrypt(user.oauth_access_token);
  }

  async refreshToken(userId, provider = 'google') {
    const user = await this.db.getUser(userId);

    if (!user.oauth_refresh_token) {
      throw new Error('REFRESH_TOKEN_INVALID');
    }

    const refreshToken = this.decrypt(user.oauth_refresh_token);
    const urls = {
      google: 'https://oauth2.googleapis.com/token',
      github: 'https://github.com/login/oauth/access_token',
      discord: 'https://discord.com/api/oauth2/token'
    };

    const response = await fetch(urls[provider], {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        refresh_token: refreshToken,
        client_id: process.env[`${provider.toUpperCase()}_CLIENT_ID`],
        client_secret: process.env[`${provider.toUpperCase()}_CLIENT_SECRET`],
        grant_type: 'refresh_token'
      })
    });

    if (!response.ok) {
      if (response.status === 400) {
        // Clear invalid tokens
        await this.clearTokens(userId);
        throw new Error('REFRESH_TOKEN_INVALID');
      }
      throw new Error('Token refresh failed');
    }

    const newTokens = await response.json();
    await this.storeTokens(userId, newTokens);

    return newTokens.access_token;
  }

  async clearTokens(userId) {
    await this.db.updateUser(userId, {
      oauth_access_token: null,
      oauth_refresh_token: null,
      token_expires_at: null
    });
  }
}

// Express middleware for automatic token refresh
export function createTokenRefreshMiddleware(tokenManager) {
  return async (req, res, next) => {
    if (!req.session?.userId) return next();

    try {
      await tokenManager.getValidAccessToken(req.session.userId);
      next();
    } catch (error) {
      if (error.message === 'REFRESH_TOKEN_INVALID') {
        req.session.destroy();
        res.clearCookie('session');
        return res.status(401).json({
          error: 'Session expired',
          code: 'REAUTHENTICATION_REQUIRED'
        });
      }
      next(error);
    }
  };
}