Session Management

SSO Version: 5.24.0

Overview

The SSO service uses OAuth 2.0 tokens to manage user sessions. Understanding token types, lifetimes, and refresh mechanisms is essential for building secure, seamless applications.

📝 Note: This guide focuses on session management from the application's perspective. All token operations should happen on your backend server, not in the browser.

Token Types

The SSO service issues three types of tokens during OAuth 2.0 authentication:

1. Access Token

  • Purpose: Used to authenticate API requests
  • Lifetime: 1 hour (3600 seconds)
  • Format: JWT (JSON Web Token)
  • Usage: Include in Authorization: Bearer TOKEN header
  • Storage: HTTP-only cookie (backend) or secure storage (backend)
// Example access token payload (decoded)
{
  "sub": "user-uuid-123",
  "iss": "https://sso.doneisbetter.com",
  "aud": "your-client-id",
  "exp": 1710000000,
  "iat": 1709996400,
  "scope": "openid profile email"
}

2. ID Token

  • Purpose: Contains user identity and app permissions
  • Lifetime: 1 hour (3600 seconds)
  • Format: JWT (JSON Web Token)
  • Usage: Extract user info, role, and permission status
  • Storage: HTTP-only cookie (backend)
// Example ID token payload (decoded)
{
  "sub": "user-uuid-123",
  "email": "user@example.com",
  "name": "John Doe",
  "role": "admin",  // 'admin' or 'user'
  "permissionStatus": "approved",  // 'approved', 'pending', or 'revoked'
  "iss": "https://sso.doneisbetter.com",
  "aud": "your-client-id",
  "exp": 1710000000,
  "iat": 1709996400
}

3. Refresh Token

  • Purpose: Used to obtain new access and ID tokens
  • Lifetime: 30 days (2592000 seconds)
  • Format: Opaque string (not JWT)
  • Usage: Exchange for new tokens before access token expires
  • Storage: HTTP-only cookie (backend)
  • Security: Single-use (rotated on each refresh)

Session Lifecycle

Understanding the complete session lifecycle helps you implement reliable authentication:

Phase 1: Initial Authentication

  1. User clicks "Sign in with SSO"
  2. Your app redirects to SSO authorization page
  3. User authenticates with SSO
  4. SSO redirects back to your app with authorization code
  5. Your backend exchanges code for tokens (access, ID, refresh)
  6. Your backend stores tokens in HTTP-only cookies
  7. Your backend redirects user to app

Phase 2: Active Session

  • User makes requests to your app
  • Your backend validates ID token for each request
  • User info and permissions are extracted from ID token
  • Access token is used for SSO API calls (if needed)

Phase 3: Token Refresh (Automatic)

  • Access token expires after 1 hour
  • Your backend detects expiry (checks exp claim)
  • Your backend uses refresh token to get new tokens
  • Old refresh token is invalidated (single-use)
  • New tokens are stored in cookies

Phase 4: Session End

  • User clicks "Sign out"
  • Your backend revokes tokens with SSO
  • Your backend clears cookies
  • User is redirected to logout page

Validating Tokens

Your backend should validate tokens on every request to ensure session integrity:

Backend Session Validation

// Node.js/Express example
import jwt from 'jsonwebtoken';

// WHY: Middleware to validate session and extract user info
export function validateSession(req, res, next) {
  const idToken = req.cookies.id_token;
  const accessToken = req.cookies.access_token;

  // Check if tokens exist
  if (!idToken || !accessToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  try {
    // Decode ID token (contains user info)
    const decoded = jwt.decode(idToken);

    // WHY: Check token expiration
    if (decoded.exp * 1000 < Date.now()) {
      // Token expired, attempt refresh
      return refreshAndRetry(req, res, next);
    }

    // WHY: Check app permission status
    if (decoded.permissionStatus !== 'approved') {
      return res.status(403).json({
        error: 'APP_ACCESS_DENIED',
        permissionStatus: decoded.permissionStatus
      });
    }

    // Attach user info to request
    req.user = {
      userId: decoded.sub,
      email: decoded.email,
      name: decoded.name,
      role: decoded.role,
      permissionStatus: decoded.permissionStatus
    };

    next();
  } catch (error) {
    console.error('Token validation error:', error);
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Token Refresh Implementation

Implement automatic token refresh to maintain seamless user sessions:

Proactive Refresh (Recommended)

// Refresh tokens 5 minutes before expiry
const REFRESH_BUFFER = 5 * 60 * 1000; // 5 minutes

async function ensureValidToken(req, res, next) {
  const idToken = req.cookies.id_token;
  
  if (!idToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  const decoded = jwt.decode(idToken);
  const expiresAt = decoded.exp * 1000;
  const now = Date.now();

  // WHY: Refresh proactively before expiry
  if (expiresAt - now < REFRESH_BUFFER) {
    await refreshTokens(req, res);
  }

  next();
}

async function refreshTokens(req, res) {
  const refreshToken = req.cookies.refresh_token;

  if (!refreshToken) {
    throw new Error('No refresh token available');
  }

  try {
    // WHY: Exchange refresh token for new access and ID tokens
    const response = await fetch(
      'https://sso.doneisbetter.com/api/oauth/token',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
          client_id: process.env.SSO_CLIENT_ID,
          client_secret: process.env.SSO_CLIENT_SECRET
        })
      }
    );

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    const tokens = await response.json();
    const { access_token, id_token, refresh_token: newRefreshToken } = tokens;

    // WHY: Update cookies with new tokens
    res.cookie('access_token', access_token, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 3600000 // 1 hour
    });
    res.cookie('id_token', id_token, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 3600000
    });
    res.cookie('refresh_token', newRefreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 2592000000 // 30 days
    });

    console.log('Tokens refreshed successfully');
  } catch (error) {
    console.error('Token refresh error:', error);
    // Clear invalid tokens
    res.clearCookie('access_token');
    res.clearCookie('id_token');
    res.clearCookie('refresh_token');
    throw error;
  }
}

Session Termination

Properly terminate sessions to ensure security:

Logout Implementation

// Backend logout endpoint
export async function logout(req, res) {
  const accessToken = req.cookies.access_token;

  try {
    // WHY: Revoke tokens with SSO server
    if (accessToken) {
      await fetch('https://sso.doneisbetter.com/api/oauth/revoke', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          token: accessToken,
          client_id: process.env.SSO_CLIENT_ID,
          client_secret: process.env.SSO_CLIENT_SECRET
        })
      });
    }
  } catch (error) {
    console.error('Token revocation failed:', error);
    // Continue with logout even if revocation fails
  }

  // WHY: Clear all session cookies
  res.clearCookie('access_token');
  res.clearCookie('id_token');
  res.clearCookie('refresh_token');

  res.json({ success: true });
}

Best Practices

  • Store tokens in HTTP-only cookies (never in localStorage/sessionStorage)
  • Implement proactive token refresh (5 minutes before expiry)
  • Validate tokens on every request (check expiry and permission status)
  • Use refresh tokens correctly (they're single-use and rotate on refresh)
  • Revoke tokens on logout (prevent reuse even if intercepted)
  • Handle token expiry gracefully (redirect to login or show message)
  • Monitor token operations (log refresh failures for debugging)
  • ⚠️ Never expose tokens in URLs (use cookies or headers only)
  • ⚠️ Never decode tokens in frontend (keep user info extraction server-side)

Troubleshooting

Token expired

Symptom: 401 errors, user logged out unexpectedly

Solution: Implement proactive token refresh (see above)

Refresh token invalid

Symptom: Token refresh fails with 401 error

Causes:

  • Refresh token already used (they're single-use)
  • Refresh token expired (30 day lifetime)
  • User's access was revoked

Solution: Redirect user to login page

Permission status changed

Symptom: User was approved but now gets 403 errors

Cause: SSO admin changed user's permission status

Solution: Check permissionStatus in ID token and redirect accordingly

Session not persisting

Symptom: User logged out on page refresh

Solutions:

  • Verify cookies are set with correct domain and path
  • Ensure SameSite and Secure flags are set correctly
  • Check CORS configuration (credentials must be included)

Summary

  • ☑️ Understand three token types: access, ID, and refresh
  • ☑️ Implement token validation middleware
  • ☑️ Implement proactive token refresh (5 min before expiry)
  • ☑️ Store tokens in HTTP-only cookies
  • ☑️ Revoke tokens on logout
  • ☑️ Check permissionStatus on every request