Troubleshooting
Debug common issues with ConsentKeys authentication.
Quick Debug Checklist
Before diving deep, check these common issues:
- Backend is running (
https://pseudoidc.consentkeys.com) - Client ID and secret are correct
- Redirect URI matches exactly (including protocol and trailing slash)
- PKCE code verifier is stored and retrieved correctly
- State parameter matches between request and callback
- Tokens haven't expired
- CORS is configured if calling from a different origin
Authentication Issues
Redirect Loop
Symptom: Browser keeps redirecting between your app and ConsentKeys.
Causes:
- Session not being set after authentication
- Auth check runs before session is loaded
- Callback handler not properly exchanging code
Solutions:
// ❌ Bad: Checking auth before session loads
function App() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
window.location.href = '/api/auth/login'; // Causes loop!
}
return <Dashboard />;
}
// ✅ Good: Wait for loading state
function App() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <Loading />;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <Dashboard />;
}
Debug:
// Add logging to identify the loop
console.log('Current path:', window.location.pathname);
console.log('Is authenticated:', isAuthenticated);
console.log('Is loading:', isLoading);
console.log('Session:', session);
"Invalid state" Error
Symptom: Callback fails with "Invalid state parameter".
Causes:
- State not stored correctly before redirect
- Session storage cleared between request and callback
- Using different session stores
- State parameter manipulated
Solutions:
// ❌ Bad: State not persisted
function login() {
const state = generateState();
// Not stored anywhere!
window.location.href = authUrl;
}
// ✅ Good: State stored in session
function login() {
const state = generateState();
sessionStorage.setItem('oauth_state', state); // Browser
// OR
req.session.state = state; // Server-side
await req.session.save();
window.location.href = authUrl;
}
// ✅ Verify in callback
function callback() {
const returnedState = params.get('state');
const storedState = sessionStorage.getItem('oauth_state');
if (returnedState !== storedState) {
console.error('State mismatch:', {
returned: returnedState,
stored: storedState,
});
throw new Error('Invalid state');
}
}
"Invalid redirect_uri" Error
Symptom: Authorization fails with redirect URI mismatch.
Causes:
- URI doesn't match registered value exactly
- Protocol mismatch (
httpvshttps) - Port number different or missing
- Trailing slash inconsistency
Solutions:
# ❌ These are all DIFFERENT URIs:
https://myapp.com/callback
https://myapp.com/callback/
http://myapp.com/callback
https://www.myapp.com/callback
https://myapp.com/auth/callback
# ✅ Must match EXACTLY what's registered:
https://myapp.com/callback
Check registration:
# Verify registered URIs
curl https://pseudoidc.consentkeys.com/api/clients/your-client-id \
-H "Authorization: Bearer $ADMIN_TOKEN"
Token Exchange Failing
Symptom: POST to /token returns 400 or 401.
Common errors:
"invalid_grant: Authorization code has expired"
// Code expired (10 minute lifetime)
// Solution: Complete the flow faster, or restart from authorization
// Debug: Log timestamp
console.log('Code received at:', new Date().toISOString());
console.log('Exchanging code at:', new Date().toISOString());
// If >10 minutes apart, code expired
"invalid_grant: Code has already been used"
// Each code is single-use
// Solution: Don't retry token exchange, restart from authorization
// ❌ Bad: Retrying with same code
async function exchangeCode(code) {
try {
return await fetch('/token', { body: { code } });
} catch (error) {
return await fetch('/token', { body: { code } }); // Already used!
}
}
// ✅ Good: Handle errors without retry
async function exchangeCode(code) {
try {
return await fetch('/token', { body: { code } });
} catch (error) {
// Restart auth flow
window.location.href = '/login';
}
}
"invalid_client: Client authentication failed"
// Wrong credentials
// Debug: Verify client_id and client_secret
console.log('CLIENT_ID:', process.env.CONSENTKEYS_CLIENT_ID);
console.log('CLIENT_SECRET:', process.env.CONSENTKEYS_CLIENT_SECRET?.substring(0, 10) + '...');
// Check they match what's registered
User Info Returns Null/Empty
Symptom: /userinfo returns no data or limited data.
Causes:
- Access token invalid or expired
- Insufficient scopes requested
- Token not included in request
Solutions:
// ❌ Bad: Missing token
fetch('/userinfo');
// ✅ Good: Include Bearer token
fetch('/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
// Check token contents
import { decodeJwt } from 'jose';
const decoded = decodeJwt(accessToken);
console.log('Token scopes:', decoded.scope);
console.log('Token expiry:', new Date(decoded.exp * 1000));
// Verify scopes requested
// If you only requested "openid", you won't get profile/email
CORS Issues
"CORS policy: No 'Access-Control-Allow-Origin' header"
Symptom: Browser blocks requests to ConsentKeys.
Cause: Calling ConsentKeys directly from frontend.
Solution:
// ❌ Bad: Frontend calling token endpoint directly
fetch('https://pseudoidc.consentkeys.com/token', {
method: 'POST',
body: tokenData,
}); // CORS error!
// ✅ Good: Backend proxies the request
fetch('/api/auth/callback', {
method: 'POST',
body: { code },
credentials: 'include',
});
// Backend then calls ConsentKeys
Why: Token exchange requires client secret, which can't be exposed to frontend. Always proxy through your backend.
PKCE Issues
"Code verifier mismatch"
Symptom: Token exchange fails with PKCE error.
Causes:
- Code verifier not stored correctly
- Different verifier used in exchange
- PKCE implementation incorrect
Debug:
// Log verifier at each step
console.log('1. Generated verifier:', codeVerifier);
sessionStorage.setItem('code_verifier', codeVerifier);
console.log('2. Retrieved verifier:', sessionStorage.getItem('code_verifier'));
// In callback
const verifier = sessionStorage.getItem('code_verifier');
console.log('3. Using verifier:', verifier);
// Verify they're the same string
Verify PKCE implementation:
# Test PKCE generation
CODE_VERIFIER="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
echo -n "$CODE_VERIFIER" | openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '/+' '_-'
# Should produce: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
Session Issues
Sessions Not Persisting
Symptom: User logged out after page refresh.
Causes:
- Session middleware not configured
- Session secret not set
- Cookies not being set/sent
- SameSite cookie issues
Solutions:
Next.js:
// Check session config
export const sessionOptions = {
password: process.env.SESSION_SECRET!, // Must be 32+ chars
cookieName: 'ck_session',
cookieOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', // Important!
maxAge: 60 * 60 * 24 * 7,
},
};
// Verify secret
console.log('Session secret length:', process.env.SESSION_SECRET.length);
// Must be at least 32 characters
Flask:
# Check session config
app.secret_key = os.environ.get('SESSION_SECRET')
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Verify secret is set
print(f"Secret key set: {bool(app.secret_key)}")
Debug cookies:
// Check if cookies are being set
console.log('All cookies:', document.cookie);
// In browser DevTools
// Application > Cookies > pseudoidc.consentkeys.com
// Verify ck_session cookie exists
"Session expired" Too Quickly
Symptom: Users logged out after short time.
Check session maxAge:
// Too short (5 minutes)
cookieOptions: {
maxAge: 60 * 5, // ❌
}
// Better (7 days)
cookieOptions: {
maxAge: 60 * 60 * 24 * 7, // ✅
}
Rate Limiting
"Too Many Requests" (429)
Symptom: Requests blocked with 429 status.
Response headers:
RateLimit-Limit: 20
RateLimit-Remaining: 0
RateLimit-Reset: 3600
Solution:
async function handleRateLimit(response) {
if (response.status === 429) {
const resetTime = response.headers.get('RateLimit-Reset');
const waitSeconds = parseInt(resetTime);
console.warn(`Rate limited. Retry in ${waitSeconds}s`);
// Wait and retry
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
return retryRequest();
}
}
Rate limits:
- Magic links: 20/hour per email
- API calls: 100/minute general
- Session endpoints: 20/minute
Token Issues
Access Token Expired
Symptom: API calls fail with 401 after some time.
Solution:
async function callAPI(endpoint) {
let response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// Token expired
if (response.status === 401) {
// Re-authenticate
window.location.href = '/login';
return;
}
return response.json();
}
Check token expiry:
import { decodeJwt } from 'jose';
const payload = decodeJwt(accessToken);
const expiryDate = new Date(payload.exp * 1000);
const isExpired = Date.now() > expiryDate.getTime();
console.log('Token expires:', expiryDate.toISOString());
console.log('Is expired:', isExpired);
Developer Tools
Browser DevTools Checklist
Console:
- Check for JavaScript errors
- Look for failed network requests
- Verify PKCE/state values
Network Tab:
- Inspect authorization redirect (302)
- Check token exchange request/response
- Verify Authorization headers
- Look for CORS errors
Application Tab:
- Check cookies (HttpOnly, Secure, SameSite)
- Inspect sessionStorage for PKCE values
- Verify localStorage doesn't contain tokens (security issue!)
Logging Best Practices
// Development logging
const DEBUG = process.env.NODE_ENV === 'development';
function logAuth(step, data) {
if (!DEBUG) return;
console.log(`[Auth] ${step}:`, {
timestamp: new Date().toISOString(),
...data,
});
}
// Use throughout flow
logAuth('Login initiated', { clientId, redirectUri });
logAuth('Code received', { code: code.substring(0, 10) + '...' });
logAuth('Tokens received', { hasAccessToken: !!tokens.access_token });
logAuth('User loaded', { userId: user.sub });
cURL Testing
Isolate issues by testing with cURL:
# Test discovery endpoint
curl https://pseudoidc.consentkeys.com/.well-known/openid-configuration
# Test token exchange
curl -X POST https://pseudoidc.consentkeys.com/token \
-d "grant_type=authorization_code" \
-d "code=YOUR_CODE" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "redirect_uri=$REDIRECT_URI" \
-d "code_verifier=$CODE_VERIFIER" \
-v # Verbose output
# Test userinfo
curl https://pseudoidc.consentkeys.com/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-v
Still Stuck?
If you've tried everything:
- ✅ Enable debug logging at each step
- ✅ Test with cURL to isolate the issue
- ✅ Check browser DevTools (Console, Network, Application)
- ✅ Verify environment variables are set
- ✅ Compare your code with the integration examples
- ✅ Check the FAQ
- ✅ Review error codes
Get help:
- Open an issue on GitHub with debug logs
- Include: browser/language, error messages, relevant code
- Email: support@consentkeys.com