Skip to main content

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:

  1. Session not being set after authentication
  2. Auth check runs before session is loaded
  3. 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:

  1. State not stored correctly before redirect
  2. Session storage cleared between request and callback
  3. Using different session stores
  4. 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:

  1. URI doesn't match registered value exactly
  2. Protocol mismatch (http vs https)
  3. Port number different or missing
  4. 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:

  1. Access token invalid or expired
  2. Insufficient scopes requested
  3. 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:

  1. Code verifier not stored correctly
  2. Different verifier used in exchange
  3. 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:

  1. Session middleware not configured
  2. Session secret not set
  3. Cookies not being set/sent
  4. 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:

  1. ✅ Enable debug logging at each step
  2. ✅ Test with cURL to isolate the issue
  3. ✅ Check browser DevTools (Console, Network, Application)
  4. ✅ Verify environment variables are set
  5. ✅ Compare your code with the integration examples
  6. ✅ Check the FAQ
  7. ✅ Review error codes

Get help:

  • Open an issue on GitHub with debug logs
  • Include: browser/language, error messages, relevant code
  • Email: support@consentkeys.com