Troubleshooting & Production Checklist
Common Issues
401 Unauthorized
Symptom: API calls return 401, authenticate() returns { valid: false }.
| Cause | Fix |
|---|---|
| Expired access token | Call refresh() then retry |
| Invalid/wrong API key | Check REAUTH_API_KEY matches dashboard |
| Wrong domain | Verify REAUTH_DOMAIN matches your verified domain exactly |
| Missing cookie | Ensure credentials: 'include' on fetch calls |
| CORS blocking cookies | Check your API and auth are on the same parent domain |
Debug token issues server-side:
const result = await reauth.authenticate(request);
if (!result.valid) {
console.log('Auth error:', result.error);
// Common errors:
// "No token provided" — no Authorization header or cookie
// "Domain mismatch" — token was issued for a different domain
// "Missing domain_id in token" — malformed JWT
// "\"exp\" claim timestamp check failed" — token expired
}
OAuth Redirect Issues
Symptom: OAuth flow fails or redirects to wrong page.
| Cause | Fix |
|---|---|
| Provider callback URL wrong | Add https://reauth.yourdomain.com/callback/google (or /callback/twitter) to your OAuth provider's allowed redirect URIs |
| Dashboard redirect URL wrong | Check redirect URL in reauth dashboard matches your app URL |
| Missing OAuth credentials | Check Client ID/Secret in reauth dashboard |
OAUTH_RETRY_EXPIRED error | OAuth retry window expired — restart the flow |
import { requiresOAuthRestart } from '@reauth-dev/sdk';
if (session.error_code && requiresOAuthRestart({ code: session.error_code })) {
// Headless: restart OAuth and redirect
const { authUrl } = await reauth.startGoogleOAuth();
window.location.href = authUrl;
// Or if using hosted UI, just redirect to login
// reauth.login();
}
Cookie Issues
Symptom: Session works in dev but not production, or cross-domain requests fail.
| Cause | Fix |
|---|---|
| Not HTTPS | Cookies are Secure — must use HTTPS in production |
| Different domain | Cookies scoped to your domain; API must be same-origin or subdomain |
| SameSite blocking | Cookies are SameSite=Lax — works for top-level navigation but not cross-origin XHR without same-site |
| Browser privacy mode | Some browsers block third-party cookies; use Bearer tokens instead |
Alternative: Use Bearer tokens instead of cookies:
// Client: get a token
const tokenResponse = await reauth.getToken();
if (!tokenResponse) {
// User is not authenticated — redirect to login
reauth.login();
return;
}
// Send as Authorization header
fetch('https://api.yourdomain.com/data', {
headers: { Authorization: `Bearer ${tokenResponse.accessToken}` },
});
// Server: verify from header (no cookie needed)
const result = await reauth.authenticate({
headers: { authorization: req.headers.authorization },
});
Webhook Signature Failures
Symptom: WebhookVerificationError thrown.
| Error | Cause | Fix |
|---|---|---|
| "Missing webhook signature header" | Header not forwarded | Ensure proxy/load balancer forwards reauth-webhook-signature |
| "Invalid signature header format" | Malformed header | Check header value matches t=<ts>,v1=<sig> format |
| "Webhook timestamp too old" | Clock drift or delayed delivery | Increase tolerance or check server clock sync |
| "Webhook signature verification failed" | Wrong secret or body modified | Verify webhook secret, use raw body (not parsed JSON) |
Common middleware mistake:
// WRONG — express.json() parses the body, changing it
app.post('/webhooks', express.json(), handler);
// CORRECT — express.raw() preserves the raw body for verification
app.post('/webhooks', express.raw({ type: 'application/json' }), handler);
DNS Verification Stuck
Symptom: Domain verification doesn't complete.
| Cause | Fix |
|---|---|
| Wrong DNS record type | CNAME for reauth.yourdomain.com, TXT for _reauth.yourdomain.com |
| DNS propagation delay | Wait 5–30 minutes |
| Cloudflare proxy enabled | Set CNAME to DNS-only (gray cloud), not proxied (orange cloud) |
| Conflicting records | Remove any existing A/AAAA records for reauth.yourdomain.com |
Subscription Not Updating
Symptom: User subscribed but session.subscription.status still shows "none".
| Cause | Fix |
|---|---|
| JWT not refreshed | Call refresh() to get new JWT with updated subscription |
| Wrong org context | Subscription is org-scoped; check active_org_id matches |
| Stripe webhook delay | Wait a few seconds for Stripe → reauth webhook propagation |
Production Checklist
Authentication
- DNS records verified (CNAME + TXT)
- Redirect URLs configured (no
localhost) - OAuth callback URLs added to Google/Twitter console
- HTTPS enabled on all endpoints
Server SDK
-
REAUTH_API_KEYstored securely (env var or secret manager) -
REAUTH_DOMAINset correctly - API key is not exposed in client-side code
Billing
- Stripe connected in live mode (not test mode)
- Plans configured with correct pricing
- Checkout success/cancel URLs point to production URLs
- Webhook endpoint added for payment events
Webhooks
- Endpoint URL uses HTTPS
- Webhook secret stored securely
- Signature verification enabled (never skip in production)
- Using raw body for signature verification (not parsed JSON)
- Handler returns 200 quickly (async processing for slow operations)
- Idempotent event processing (use
event.idto deduplicate)
Credits
- Idempotency keys (
requestUuid) used for all charge/deposit calls - Insufficient balance errors handled gracefully (402 responses)
- Refund logic for failed operations after successful charge
Organizations
- Tested org switching + subscription scoping
- Deletion guards tested (sole owner with members)
General
- Error handling for all SDK calls (try/catch)
- Token refresh flow tested (expired access token → refresh → retry)
- Tested from incognito/private browsing (no stale cookies)
- Rate limiting awareness (check response status codes)