React Integration Example

SSO Version: 5.24.0

Overview

This guide demonstrates OAuth 2.0 Authorization Code Flow integration in a React application. No special library is required—just standard OAuth 2.0 flow with your backend handling token exchange.

⚠️ Security Note: Never expose client_secret in your React app. All token exchange operations must happen on your backend server.

1. Environment Setup

Configure your environment variables (backend only):

# .env (Backend Only - NEVER commit this file)
SSO_CLIENT_ID=your_client_id_here
SSO_CLIENT_SECRET=your_client_secret_here
SSO_REDIRECT_URI=https://yourapp.com/api/auth/callback
SSO_BASE_URL=https://sso.doneisbetter.com
SESSION_SECRET=your_session_secret_here

2. AuthContext Setup

Create a React Context to manage authentication state:

// src/contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [permissionStatus, setPermissionStatus] = useState(null);

  // Check session on mount
  useEffect(() => {
    checkSession();
  }, []);

  // WHY: Verify if user has a valid session with your backend
  const checkSession = async () => {
    try {
      const response = await fetch('/api/auth/session', {
        credentials: 'include' // Include cookies
      });
      
      if (response.ok) {
        const data = await response.json();
        setUser(data.user);
        setPermissionStatus(data.permissionStatus); // 'approved', 'pending', 'revoked'
      }
    } catch (error) {
      console.error('Session check failed:', error);
    } finally {
      setLoading(false);
    }
  };

  // WHY: Redirect user to SSO authorization page
  const login = () => {
    // Generate random state for CSRF protection
    const state = Math.random().toString(36).substring(7);
    sessionStorage.setItem('oauth_state', state);

    const authUrl = new URL('https://sso.doneisbetter.com/api/oauth/authorize');
    authUrl.searchParams.append('client_id', process.env.NEXT_PUBLIC_SSO_CLIENT_ID);
    authUrl.searchParams.append('redirect_uri', window.location.origin + '/api/auth/callback');
    authUrl.searchParams.append('response_type', 'code');
    authUrl.searchParams.append('scope', 'openid profile email');
    authUrl.searchParams.append('state', state);

    window.location.href = authUrl.toString();
  };

  // WHY: Clear session and redirect to SSO logout
  const logout = async () => {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        credentials: 'include'
      });
      setUser(null);
      setPermissionStatus(null);
      
      // Optional: Also logout from SSO
      window.location.href = 'https://sso.doneisbetter.com/api/public/logout';
    } catch (error) {
      console.error('Logout failed:', error);
    }
  };

  return (
    <AuthContext.Provider value={{ 
      user, 
      loading, 
      permissionStatus,
      login, 
      logout,
      isApproved: permissionStatus === 'approved'
    }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

3. Backend OAuth Callback (Next.js API Route)

Handle the OAuth callback and exchange code for tokens:

// pages/api/auth/callback.js
import jwt from 'jsonwebtoken';

export default async function handler(req, res) {
  const { code, state } = req.query;

  // WHY: Validate state parameter to prevent CSRF attacks
  const expectedState = req.cookies.oauth_state;
  if (state !== expectedState) {
    return res.status(400).json({ error: 'Invalid state parameter' });
  }

  try {
    // WHY: Exchange authorization code for tokens (server-side only)
    const tokenResponse = await fetch(
      `${process.env.SSO_BASE_URL}/api/oauth/token`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          grant_type: 'authorization_code',
          code,
          redirect_uri: process.env.SSO_REDIRECT_URI,
          client_id: process.env.SSO_CLIENT_ID,
          client_secret: process.env.SSO_CLIENT_SECRET // NEVER expose this to frontend
        })
      }
    );

    if (!tokenResponse.ok) {
      const error = await tokenResponse.json();
      return res.status(tokenResponse.status).json(error);
    }

    const tokens = await tokenResponse.json();
    const { access_token, id_token, refresh_token } = tokens;

    // WHY: Decode ID token to get user info and app permissions
    const decoded = jwt.decode(id_token);
    const { sub: userId, email, name, role, permissionStatus } = decoded;

    // WHY: Store tokens securely in HTTP-only cookies
    res.setHeader('Set-Cookie', [
      `access_token=${access_token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600`,
      `refresh_token=${refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000`,
      `id_token=${id_token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600`
    ]);

    // WHY: Check app permission status and redirect accordingly
    if (permissionStatus === 'pending') {
      return res.redirect('/access-pending');
    } else if (permissionStatus === 'revoked') {
      return res.redirect('/access-denied');
    }

    // Success: Redirect to app
    res.redirect('/dashboard');
  } catch (error) {
    console.error('OAuth callback error:', error);
    res.status(500).json({ error: 'Authentication failed' });
  }
}

4. Session Validation Endpoint

Create an endpoint to check current session status:

// pages/api/auth/session.js
import jwt from 'jsonwebtoken';

export default async function handler(req, res) {
  const { id_token, access_token } = req.cookies;

  if (!id_token || !access_token) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  try {
    // WHY: Decode ID token to get user info and permission status
    const decoded = jwt.decode(id_token);
    const { sub: userId, email, name, role, permissionStatus } = decoded;

    // WHY: Check token expiration
    if (decoded.exp * 1000 < Date.now()) {
      return res.status(401).json({ error: 'Token expired' });
    }

    res.json({
      user: { userId, email, name, role },
      permissionStatus
    });
  } catch (error) {
    console.error('Session validation error:', error);
    res.status(500).json({ error: 'Session validation failed' });
  }
}

5. Protected Route Component

Create a component to protect routes requiring authentication:

// src/components/ProtectedRoute.jsx
import { useAuth } from '../contexts/AuthContext';
import { useRouter } from 'next/router';
import { useEffect } from 'react';

export function ProtectedRoute({ children, requireApproved = true }) {
  const { user, loading, permissionStatus, isApproved } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (loading) return;

    // WHY: Redirect to login if not authenticated
    if (!user) {
      router.push('/login');
      return;
    }

    // WHY: Check app permission status
    if (requireApproved && !isApproved) {
      if (permissionStatus === 'pending') {
        router.push('/access-pending');
      } else if (permissionStatus === 'revoked') {
        router.push('/access-denied');
      }
    }
  }, [user, loading, permissionStatus, isApproved, requireApproved]);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user || (requireApproved && !isApproved)) {
    return null;
  }

  return <>{children}</>;
}

6. Usage in Your App

// pages/_app.js
import { AuthProvider } from '../contexts/AuthContext';

export default function App({ Component, pageProps }) {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
}

// pages/login.js
import { useAuth } from '../contexts/AuthContext';

export default function LoginPage() {
  const { login } = useAuth();

  return (
    <div>
      <h1>Sign In</h1>
      <button onClick={login}>Sign in with SSO</button>
    </div>
  );
}

// pages/dashboard.js
import { ProtectedRoute } from '../components/ProtectedRoute';
import { useAuth } from '../contexts/AuthContext';

export default function Dashboard() {
  const { user, logout } = useAuth();

  return (
    <ProtectedRoute>
      <div>
        <h1>Welcome, {user.name}</h1>
        <p>Email: {user.email}</p>
        <p>Role: {user.role}</p>
        <button onClick={logout}>Sign Out</button>
      </div>
    </ProtectedRoute>
  );
}

// pages/access-pending.js
export default function AccessPending() {
  return (
    <div>
      <h1>Access Pending</h1>
      <p>
        Your access to this application is pending approval.
        An administrator will review your request shortly.
      </p>
      <p>You will receive an email notification once approved.</p>
    </div>
  );
}

7. Token Refresh (Optional)

Implement automatic token refresh before expiry:

// pages/api/auth/refresh.js
export default async function handler(req, res) {
  const { refresh_token } = req.cookies;

  if (!refresh_token) {
    return res.status(401).json({ error: 'No refresh token' });
  }

  try {
    const tokenResponse = await fetch(
      `${process.env.SSO_BASE_URL}/api/oauth/token`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token,
          client_id: process.env.SSO_CLIENT_ID,
          client_secret: process.env.SSO_CLIENT_SECRET
        })
      }
    );

    if (!tokenResponse.ok) {
      return res.status(401).json({ error: 'Token refresh failed' });
    }

    const tokens = await tokenResponse.json();
    const { access_token, id_token } = tokens;

    // WHY: Update cookies with new tokens
    res.setHeader('Set-Cookie', [
      `access_token=${access_token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600`,
      `id_token=${id_token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600`
    ]);

    res.json({ success: true });
  } catch (error) {
    console.error('Token refresh error:', error);
    res.status(500).json({ error: 'Token refresh failed' });
  }
}

Summary

  • ✅ OAuth 2.0 Authorization Code Flow implemented
  • ✅ Secure token handling with HTTP-only cookies
  • ✅ CSRF protection with state parameter
  • ✅ App permission status handling (pending/approved/revoked)
  • ✅ Protected routes with automatic redirects
  • ✅ Token refresh capability
🔗 Next Steps: