Examples
React Integration Example
Reference React integration using OAuth Authorization Code flow with backend token exchange.
5.29.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.
Current contract note
When these examples talk about app approval state, your backend should derive it from the permission APIs and expose it through your own session endpoint. Do not assume raw id_token claims already contain canonical app-permission status.
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 [permission, setPermission] = 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);
setPermission(data.permission ?? null); // e.g. { status: 'approved', role: 'member' }
}
} 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);
setPermission(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,
permission,
login,
logout,
isApproved: permission?.status === '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 identity claims
const decoded = jwt.decode(id_token);
const { sub: userId, email, name, role } = decoded;
// WHY: Ask your backend permission layer for canonical app access state
const permission = await getPermissionForUserAndClient({
userId,
clientId: process.env.SSO_CLIENT_ID
});
// 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 backend-derived app permission status and redirect accordingly
if (permission?.status === 'pending') {
return res.redirect('/access-pending');
} else if (permission?.status === '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 identity claims
const decoded = jwt.decode(id_token);
const { sub: userId, email, name, role } = decoded;
// WHY: Check token expiration
if (decoded.exp * 1000 < Date.now()) {
return res.status(401).json({ error: 'Token expired' });
}
const permission = await getPermissionForUserAndClient({
userId,
clientId: process.env.SSO_CLIENT_ID
});
res.json({
user: { userId, email, name, role },
permission
});
} 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, permission, 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 (permission?.status === 'pending') {
router.push('/access-pending');
} else if (permission?.status === 'revoked') {
router.push('/access-denied');
}
}
}, [user, loading, permission, 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>
<Title order={1} mb="xs">Sign In</Title>
<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>
<Title order={1} mb="xs">Welcome, {user.name}</Title>
<Text size="sm">Email: {user.email}</Text>
<Text size="sm">Role: {user.role}</Text>
<button onClick={logout}>Sign Out</button>
</div>
</ProtectedRoute>
);
}
// pages/access-pending.js
export default function AccessPending() {
return (
<div>
<Title order={1} mb="xs">Access Pending</Title>
<Text size="sm">
Your access to this application is pending approval.
An administrator will review your request shortly.
</Text>
<Text size="sm">You will receive an email notification once approved.</Text>
</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
- Review Authentication Flow for detailed OAuth 2.0 explanation
- Check App Permissions to understand permission lifecycle
- See API Reference for complete endpoint documentation