Skip to main content

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)
Redirect URI Configuration

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.

src/utils/pkce.ts
// 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

src/contexts/AuthContext.tsx
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

src/pages/Callback.tsx
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.

backend/routes/auth.ts
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

src/components/ProtectedRoute.tsx
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

src/components/LoginButton.tsx
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>
);
}
src/components/UserProfile.tsx
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

src/App.tsx
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

.env
# 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

  1. Start ConsentKeys backend:

    cd backend
    npm run dev
  2. Start your backend:

    cd your-backend
    npm start
  3. Start React app:

    cd your-react-app
    npm start
  4. Visit http://localhost:3001 and 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