React Integration
Integrate ConsentKeys authentication into your React application using standard OAuth 2.0 / OIDC protocols.
Prerequisites
- A ConsentKeys Client ID and Secret from the Developer Portal
- React 18+
- Node.js 18+
- A backend API to proxy token exchange (required for security)
When you register your app in the Developer Portal, configure your redirect URI to point to your application (e.g., http://localhost:3001/callback for local dev or https://yourapp.com/callback for production). This is where ConsentKeys will send users after they authenticate.
Architecture Overview
Why a backend? Your client secret must never be exposed in frontend code. The backend securely exchanges the authorization code for tokens.
Step 1: Generate PKCE Challenge
PKCE (Proof Key for Code Exchange) secures the OAuth flow for public clients.
// Generate a random code verifier
export function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
// Generate code challenge from verifier
export async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
// Base64 URL encoding
function base64UrlEncode(array: Uint8Array): string {
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Generate random state for CSRF protection
export function generateState(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
Step 2: Create Auth Context
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User {
sub: string;
email: string;
name?: string;
picture?: string;
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: () => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const CONSENTKEYS_AUTH_URL = 'https://pseudoidc.consentkeys.com/auth';
const CLIENT_ID = 'ck_your_client_id';
const REDIRECT_URI = 'http://localhost:3001/callback';
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Check for existing session on mount
useEffect(() => {
checkSession();
}, []);
async function checkSession() {
try {
const response = await fetch('/api/auth/me', {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setUser(data.user);
}
} catch (error) {
console.error('Session check failed:', error);
} finally {
setIsLoading(false);
}
}
async function login() {
// Generate PKCE parameters
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateState();
// Store in sessionStorage for callback
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
// Build authorization URL
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
// Redirect to ConsentKeys
window.location.href = `${CONSENTKEYS_AUTH_URL}?${params}`;
}
async function logout() {
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
setUser(null);
} catch (error) {
console.error('Logout failed:', error);
}
}
return (
<AuthContext.Provider
value={{
user,
isLoading,
isAuthenticated: !!user,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Step 3: Handle OAuth Callback
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export default function Callback() {
const navigate = useNavigate();
useEffect(() => {
handleCallback();
}, []);
async function handleCallback() {
try {
// Parse URL parameters
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const error = params.get('error');
// Handle OAuth errors
if (error) {
throw new Error(params.get('error_description') || error);
}
// Verify state (CSRF protection)
const storedState = sessionStorage.getItem('oauth_state');
if (!state || state !== storedState) {
throw new Error('Invalid state parameter');
}
// Get code verifier
const codeVerifier = sessionStorage.getItem('code_verifier');
if (!code || !codeVerifier) {
throw new Error('Missing code or verifier');
}
// Exchange code for tokens via backend
const response = await fetch('/api/auth/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
code,
codeVerifier,
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Token exchange failed');
}
// Clean up
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('oauth_state');
// Redirect to dashboard
navigate('/dashboard');
} catch (error) {
console.error('Callback error:', error);
navigate('/login?error=' + encodeURIComponent(error.message));
}
}
return (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<h2>Completing sign in...</h2>
<p>Please wait while we log you in.</p>
</div>
);
}
Step 4: Backend API Routes
Your backend must handle token exchange securely.
import express from 'express';
const router = express.Router();
const CONSENTKEYS_TOKEN_URL = 'https://pseudoidc.consentkeys.com/token';
const CONSENTKEYS_USERINFO_URL = 'https://pseudoidc.consentkeys.com/userinfo';
const CLIENT_ID = process.env.CONSENTKEYS_CLIENT_ID;
const CLIENT_SECRET = process.env.CONSENTKEYS_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3001/callback';
// Exchange code for tokens
router.post('/callback', async (req, res) => {
try {
const { code, codeVerifier } = req.body;
// Exchange authorization code for tokens
const tokenResponse = await fetch(CONSENTKEYS_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code_verifier: codeVerifier,
}),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.json();
return res.status(400).json({ error: error.error_description });
}
const tokens = await tokenResponse.json();
// Get user info
const userResponse = await fetch(CONSENTKEYS_USERINFO_URL, {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
});
const user = await userResponse.json();
// Store in session (using express-session or similar)
req.session.accessToken = tokens.access_token;
req.session.user = user;
res.json({ success: true });
} catch (error) {
console.error('Token exchange error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
// Get current user
router.get('/me', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({ user: req.session.user });
});
// Logout
router.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ success: true });
});
});
export default router;
Step 5: Protected Routes
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}
Step 6: UI Components
import { useAuth } from '../contexts/AuthContext';
export function LoginButton() {
const { login } = useAuth();
return (
<button
onClick={login}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#2493FB',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
fontSize: '1rem',
cursor: 'pointer',
}}
>
Sign in with ConsentKeys
</button>
);
}
import { useAuth } from '../contexts/AuthContext';
export function UserProfile() {
const { user, logout } = useAuth();
if (!user) return null;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
{user.picture && (
<img
src={user.picture}
alt={user.name}
style={{ width: '40px', height: '40px', borderRadius: '50%' }}
/>
)}
<div>
<div style={{ fontWeight: 'bold' }}>{user.name || user.email}</div>
<div style={{ fontSize: '0.875rem', color: '#666' }}>
{user.email}
</div>
</div>
<button onClick={logout}>Logout</button>
</div>
);
}
Complete Example
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { LoginButton } from './components/LoginButton';
import { UserProfile } from './components/UserProfile';
import Callback from './pages/Callback';
function LoginPage() {
const { isAuthenticated } = useAuth();
if (isAuthenticated) {
return <Navigate to="/dashboard" replace />;
}
return (
<div style={{ textAlign: 'center', padding: '4rem' }}>
<h1>Welcome to My App</h1>
<p>Sign in to get started</p>
<LoginButton />
</div>
);
}
function Dashboard() {
return (
<div style={{ padding: '2rem' }}>
<h1>Dashboard</h1>
<UserProfile />
<p>Welcome to your dashboard!</p>
</div>
);
}
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
export default App;
Environment Variables
# React app
REACT_APP_CLIENT_ID=ck_your_client_id
REACT_APP_REDIRECT_URI=http://localhost:3001/callback
# Backend (keep secret!)
CONSENTKEYS_CLIENT_ID=ck_your_client_id
CONSENTKEYS_CLIENT_SECRET=your_client_secret
SESSION_SECRET=your-session-secret-min-32-chars
Testing Locally
-
Start ConsentKeys backend:
cd backend
npm run dev -
Start your backend:
cd your-backend
npm start -
Start React app:
cd your-react-app
npm start -
Visit
http://localhost:3001and click "Sign in"
Security Checklist
- ✅ Client secret stored on backend only
- ✅ PKCE implemented for authorization code flow
- ✅ State parameter verified for CSRF protection
- ✅ Tokens never stored in localStorage
- ✅ Session cookies are httpOnly
- ✅ HTTPS in production
Troubleshooting
"Invalid redirect_uri"
- Ensure redirect URI matches exactly in Developer Portal
"Invalid state"
- Clear sessionStorage and try again
"CORS error"
- Backend must proxy requests to ConsentKeys
- Don't call token endpoint from frontend
"Code verifier mismatch"
- Ensure PKCE functions match the spec
Next Steps
- Learn about OAuth concepts
- Explore available scopes
- Check the full API reference