Skip to main content
GET
/
callback
# This request is typically made by the OAuth provider
curl -X GET "http://localhost:8080/callback?code=auth_code_here&state=csrf_state&provider=google"
HTTP/1.1 302 Found
Location: https://yourapp.com/dashboard?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&expires_in=3600&token_type=bearer
Set-Cookie: sb-access-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Lax
Set-Cookie: sb-refresh-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Lax
Handle OAuth provider callbacks and complete the authentication flow. This endpoint receives the authorization code from OAuth providers and exchanges it for access tokens to authenticate users.
This endpoint is automatically called by OAuth providers after user authorization. You typically don’t call this endpoint directly.
# This request is typically made by the OAuth provider
curl -X GET "http://localhost:8080/callback?code=auth_code_here&state=csrf_state&provider=google"

Query Parameters

code
string
required
Authorization code returned by the OAuth provider.
state
string
State parameter for CSRF protection (if provided in the authorization request).
provider
string
OAuth provider identifier (may be included in some implementations).
error
string
Error code if the OAuth authorization failed.
error_description
string
Human-readable error description if the OAuth authorization failed.

Response

This endpoint typically returns a 302 Found redirect response to the application’s redirect URL with authentication tokens.
Location
string
Redirect URL with authentication tokens or session information
HTTP/1.1 302 Found
Location: https://yourapp.com/dashboard?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&expires_in=3600&token_type=bearer
Set-Cookie: sb-access-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Lax
Set-Cookie: sb-refresh-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Lax

Error Responses

HTTP/1.1 302 Found
Location: https://yourapp.com/login?error=access_denied&error_description=User%20denied%20access

OAuth Callback Flow

Successful Authentication

  1. Authorization Code Exchange: The auth service exchanges the authorization code for access tokens
  2. User Information Retrieval: Fetch user profile information from the OAuth provider
  3. User Account Management: Create or update user account in the database
  4. Session Creation: Generate authentication tokens for the user
  5. Redirect: Redirect user to the specified redirect URL with tokens

Error Handling

Common OAuth errors and their handling:
  • access_denied: User denied authorization
  • invalid_request: Malformed authorization request
  • unauthorized_client: Client not authorized for this grant type
  • unsupported_response_type: Authorization server doesn’t support the response type
  • invalid_scope: Requested scope is invalid or unknown
  • server_error: Authorization server encountered an error
  • temporarily_unavailable: Authorization server is temporarily unavailable

Implementation Examples

React OAuth Callback Handler

import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

function OAuthCallback() {
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  const [status, setStatus] = useState('processing');
  const [error, setError] = useState(null);

  useEffect(() => {
    handleOAuthCallback();
  }, []);

  const handleOAuthCallback = async () => {
    const code = searchParams.get('code');
    const state = searchParams.get('state');
    const error = searchParams.get('error');
    const errorDescription = searchParams.get('error_description');

    // Handle OAuth errors
    if (error) {
      setStatus('error');
      setError({
        code: error,
        description: errorDescription || 'OAuth authentication failed'
      });
      return;
    }

    // Validate required parameters
    if (!code) {
      setStatus('error');
      setError({
        code: 'missing_code',
        description: 'Authorization code is missing'
      });
      return;
    }

    // Validate state parameter (CSRF protection)
    const storedState = sessionStorage.getItem('oauth_state');
    if (state && storedState && state !== storedState) {
      setStatus('error');
      setError({
        code: 'state_mismatch',
        description: 'State parameter mismatch - possible CSRF attack'
      });
      return;
    }

    try {
      // Extract tokens from URL or cookies
      const accessToken = searchParams.get('access_token');
      const refreshToken = searchParams.get('refresh_token');
      const expiresIn = searchParams.get('expires_in');

      if (accessToken) {
        // Store tokens
        localStorage.setItem('access_token', accessToken);
        if (refreshToken) {
          localStorage.setItem('refresh_token', refreshToken);
        }
        if (expiresIn) {
          const expiresAt = Date.now() + parseInt(expiresIn) * 1000;
          localStorage.setItem('token_expires_at', expiresAt.toString());
        }

        // Clear OAuth state
        sessionStorage.removeItem('oauth_state');

        setStatus('success');
        
        // Redirect to dashboard or intended page
        const redirectTo = sessionStorage.getItem('oauth_redirect_to') || '/dashboard';
        sessionStorage.removeItem('oauth_redirect_to');
        
        setTimeout(() => {
          navigate(redirectTo, { replace: true });
        }, 1000);
      } else {
        // Tokens might be in cookies, check if user is authenticated
        const response = await fetch('/api/user/profile', {
          credentials: 'include'
        });

        if (response.ok) {
          setStatus('success');
          const redirectTo = sessionStorage.getItem('oauth_redirect_to') || '/dashboard';
          sessionStorage.removeItem('oauth_redirect_to');
          navigate(redirectTo, { replace: true });
        } else {
          throw new Error('Authentication failed');
        }
      }
    } catch (err) {
      setStatus('error');
      setError({
        code: 'auth_failed',
        description: err.message || 'Authentication failed'
      });
    }
  };

  if (status === 'processing') {
    return (
      <div className="oauth-callback-processing">
        <div className="spinner"></div>
        <h2>Completing authentication...</h2>
        <p>Please wait while we finish setting up your account.</p>
      </div>
    );
  }

  if (status === 'success') {
    return (
      <div className="oauth-callback-success">
        <div className="success-icon"></div>
        <h2>Authentication successful!</h2>
        <p>Redirecting you to your dashboard...</p>
      </div>
    );
  }

  if (status === 'error') {
    return (
      <div className="oauth-callback-error">
        <div className="error-icon"></div>
        <h2>Authentication failed</h2>
        <p>{error.description}</p>
        <div className="error-actions">
          <button onClick={() => navigate('/login')}>
            Try Again
          </button>
          <button onClick={() => navigate('/support')}>
            Get Help
          </button>
        </div>
      </div>
    );
  }

  return null;
}

export default OAuthCallback;

OAuth Token Extraction Hook

import { useEffect, useState } from 'react';

function useOAuthTokens() {
  const [tokens, setTokens] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    extractTokensFromCallback();
  }, []);

  const extractTokensFromCallback = () => {
    try {
      // Check URL parameters first
      const urlParams = new URLSearchParams(window.location.search);
      const accessToken = urlParams.get('access_token');
      const refreshToken = urlParams.get('refresh_token');
      const expiresIn = urlParams.get('expires_in');

      if (accessToken) {
        const tokenData = {
          access_token: accessToken,
          refresh_token: refreshToken,
          expires_in: expiresIn ? parseInt(expiresIn) : null,
          expires_at: expiresIn ? Date.now() + parseInt(expiresIn) * 1000 : null
        };

        setTokens(tokenData);
        
        // Store tokens securely
        localStorage.setItem('auth_tokens', JSON.stringify(tokenData));
        
        // Clean URL
        window.history.replaceState({}, document.title, window.location.pathname);
      } else {
        // Check if tokens are in localStorage
        const storedTokens = localStorage.getItem('auth_tokens');
        if (storedTokens) {
          setTokens(JSON.parse(storedTokens));
        }
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const clearTokens = () => {
    setTokens(null);
    localStorage.removeItem('auth_tokens');
  };

  const isTokenExpired = () => {
    if (!tokens?.expires_at) return false;
    return Date.now() >= tokens.expires_at;
  };

  return {
    tokens,
    loading,
    error,
    clearTokens,
    isTokenExpired
  };
}

export default useOAuthTokens;

Node.js OAuth Callback Handler

const express = require('express');
const { query, validationResult } = require('express-validator');

const router = express.Router();

router.get('/callback', [
  query('code').optional().isString(),
  query('state').optional().isString(),
  query('error').optional().isString(),
  query('provider').optional().isString()
], async (req, res) => {
  try {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        msg: 'Invalid callback parameters',
        details: errors.array()
      });
    }

    const { code, state, error, error_description, provider } = req.query;

    // Handle OAuth errors
    if (error) {
      const errorUrl = `${process.env.APP_URL}/login?error=${error}&error_description=${encodeURIComponent(error_description || '')}`;
      return res.redirect(errorUrl);
    }

    // Validate authorization code
    if (!code) {
      return res.status(400).json({
        code: 400,
        msg: 'Missing authorization code',
        details: 'Authorization code is required for OAuth callback'
      });
    }

    // Validate state parameter (if provided)
    if (state) {
      const storedState = req.session?.oauth_state;
      if (!storedState || storedState !== state) {
        return res.status(400).json({
          code: 400,
          msg: 'State parameter mismatch',
          details: 'Invalid state parameter - possible CSRF attack'
        });
      }
    }

    // Forward to Strike Auth Service for token exchange
    const callbackUrl = `${process.env.AUTH_SERVICE_URL}/callback?${req.url.split('?')[1]}`;
    
    const response = await fetch(callbackUrl, {
      method: 'GET',
      headers: {
        'User-Agent': req.headers['user-agent'],
        'X-Forwarded-For': req.ip,
        'X-Real-IP': req.ip
      },
      redirect: 'manual'
    });

    // Handle redirect response
    if (response.status === 302) {
      const location = response.headers.get('location');
      
      // Extract and set cookies if present
      const cookies = response.headers.get('set-cookie');
      if (cookies) {
        res.set('Set-Cookie', cookies);
      }
      
      return res.redirect(location);
    }

    // Handle error response
    if (!response.ok) {
      const errorData = await response.json();
      return res.status(response.status).json(errorData);
    }

    // This shouldn't happen in normal flow
    const data = await response.json();
    res.json(data);

  } catch (error) {
    console.error('OAuth callback error:', error);
    
    // Redirect to error page
    const errorUrl = `${process.env.APP_URL}/login?error=server_error&error_description=${encodeURIComponent('Authentication failed')}`;
    res.redirect(errorUrl);
  }
});

module.exports = router;

OAuth Error Handler Component

import { useSearchParams, useNavigate } from 'react-router-dom';

function OAuthError() {
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  
  const error = searchParams.get('error');
  const errorDescription = searchParams.get('error_description');

  const getErrorMessage = (errorCode) => {
    const errorMessages = {
      'access_denied': 'You denied access to your account. Please try again if you want to sign in.',
      'invalid_request': 'The authentication request was invalid. Please try again.',
      'unauthorized_client': 'This application is not authorized to use this authentication method.',
      'unsupported_response_type': 'The authentication method is not supported.',
      'invalid_scope': 'The requested permissions are invalid.',
      'server_error': 'The authentication server encountered an error. Please try again.',
      'temporarily_unavailable': 'The authentication service is temporarily unavailable. Please try again later.',
      'state_mismatch': 'Security validation failed. Please try again.',
      'auth_failed': 'Authentication failed. Please try again.'
    };

    return errorMessages[errorCode] || 'An unknown error occurred during authentication.';
  };

  const getErrorSolution = (errorCode) => {
    const solutions = {
      'access_denied': 'Click "Try Again" to restart the authentication process.',
      'server_error': 'Wait a moment and try again. If the problem persists, contact support.',
      'temporarily_unavailable': 'Please wait a few minutes and try again.',
      'state_mismatch': 'Clear your browser cache and try again.',
      'auth_failed': 'Check your internet connection and try again.'
    };

    return solutions[errorCode] || 'Please try again or contact support if the problem persists.';
  };

  return (
    <div className="oauth-error-page">
      <div className="error-container">
        <div className="error-icon">⚠️</div>
        
        <h1>Authentication Failed</h1>
        
        <div className="error-details">
          <p className="error-message">
            {getErrorMessage(error)}
          </p>
          
          {errorDescription && (
            <p className="error-description">
              {decodeURIComponent(errorDescription)}
            </p>
          )}
          
          <p className="error-solution">
            {getErrorSolution(error)}
          </p>
        </div>

        <div className="error-actions">
          <button 
            onClick={() => navigate('/login')}
            className="primary-button"
          >
            Try Again
          </button>
          
          <button 
            onClick={() => navigate('/signup')}
            className="secondary-button"
          >
            Create Account
          </button>
          
          <button 
            onClick={() => navigate('/support')}
            className="text-button"
          >
            Contact Support
          </button>
        </div>

        <div className="error-code">
          Error Code: {error || 'unknown'}
        </div>
      </div>
    </div>
  );
}

export default OAuthError;

Security Considerations

  • State Parameter Validation: Always validate the state parameter to prevent CSRF attacks
  • Authorization Code Expiration: Authorization codes are short-lived (typically 10 minutes)
  • Token Security: Store tokens securely and use HttpOnly cookies when possible
  • Redirect URL Validation: Ensure redirect URLs are whitelisted
  • Error Handling: Don’t expose sensitive error information to users

Callback URL Configuration

Development

http://localhost:8080/callback

Production

https://yourapp.com/auth/callback

Multiple Environments

const getCallbackURL = () => {
  const baseURL = process.env.NODE_ENV === 'production' 
    ? 'https://yourapp.com' 
    : 'http://localhost:3000';
  
  return `${baseURL}/auth/callback`;
};

Best Practices

  • Always validate state parameters
  • Use HTTPS in production
  • Implement proper error handling
  • Store tokens securely
  • Set appropriate cookie flags
  • Show loading states during processing
  • Provide clear error messages
  • Offer alternative authentication methods
  • Handle network failures gracefully
  • Clean URLs after token extraction
  • Log OAuth errors for debugging
  • Provide user-friendly error messages
  • Implement retry mechanisms
  • Handle expired authorization codes
  • Gracefully handle provider downtime

Testing

Unit Tests

describe('GET /callback', () => {
  test('should handle successful OAuth callback', async () => {
    const response = await request(app)
      .get('/callback')
      .query({
        code: 'valid_auth_code',
        state: 'valid_state'
      })
      .expect(302);

    expect(response.headers.location).toContain('/dashboard');
  });

  test('should handle OAuth error', async () => {
    const response = await request(app)
      .get('/callback')
      .query({
        error: 'access_denied',
        error_description: 'User denied access'
      })
      .expect(302);

    expect(response.headers.location).toContain('error=access_denied');
  });

  test('should reject missing authorization code', async () => {
    await request(app)
      .get('/callback')
      .query({
        state: 'valid_state'
      })
      .expect(400);
  });
});
I