Post-mortem for an issue encountered while setting up OIDC authentication with Better Auth's trustedClients feature.
Summary
Date: 2025-12-22 Duration: ~1 hour Impact: OIDC token exchange failed for trusted clients Root Cause: Better Auth bug — trusted clients not inserted into database Resolution: Automated database sync during migration
Problem
When configuring Better Auth's OIDC Provider with trustedClients (static client registration via environment variables), the token exchange step failed with a 500 error.
Symptoms
- OIDC authorize endpoint worked correctly
- Consent screen was correctly skipped (trusted client)
- Authorization code was issued successfully
- Token exchange failed with 500 Internal Server Error
- No useful error in HTTP response (empty body)
Server Logs
ERROR [Better Auth]:
code: '23503',
detail: 'Key (clientId)=(citizen-web) is not present in table "oauthApplication".',
constraint: 'oauthAccessToken_clientId_fkey'Root Cause
Better Auth's trustedClients configuration creates in-memory client objects that:
- ✅ Are recognized during authorization
- ✅ Skip consent screen (when
skipConsent: true) - ❌ Are NOT inserted into the
oauthApplicationdatabase table
However, the oauthAccessToken table has a foreign key constraint:
CONSTRAINT "oauthAccessToken_clientId_fkey"
FOREIGN KEY ("clientId")
REFERENCES "oauthApplication"("clientId")
ON DELETE CASCADEWhen Better Auth tries to create an access token, PostgreSQL rejects it because the clientId doesn't exist in oauthApplication.
Affected Versions
- Broken: Better Auth ≤ 1.4.7
- Fixed: Better Auth 1.4.8-beta.* (not stable yet)
GitHub Issue
#5468 - OIDC Plugin trustedClients cannot be used due to foreign key constraints
Configuration That Triggered the Bug
// apps/citizen/src/auth.ts
oidcProvider({
loginPage: "/login",
consentPage: "/consent",
allowDynamicClientRegistration: true,
trustedClients: [
{
clientId: "citizen-web",
clientSecret: "...",
name: "Citizen Web Console",
type: "web",
redirectURLs: ["http://localhost:3100/auth/callback"],
disabled: false,
skipConsent: true,
},
],
}),Environment variable format:
OAUTH_TRUSTED_CLIENTS="citizen-web:secret:http://localhost:3100/auth/callback"Resolution
Automated Workaround (Implemented)
Created apps/citizen/src/sync-oauth-clients.ts that runs during container startup:
- Parses
OAUTH_TRUSTED_CLIENTSenvironment variable - Inserts each client into
oauthApplicationtable - Uses
ON CONFLICT DO NOTHINGfor idempotency
The script runs automatically after migrations in both prod and dev:
# deploy/citizen-service/docker-compose.yml
command: ["sh", "-c", "node dist/migrate.js && node dist/sync-oauth-clients.js"]Example output:
Syncing 1 OIDC trusted client(s) to database...
✓ Inserted client: citizen-web
OIDC client sync completed successfullyManual Workaround (Original)
If needed manually, insert the trusted client into the oauthApplication table:
INSERT INTO "oauthApplication" (
id, name, "clientId", "clientSecret", "redirectUrls",
type, disabled, "userId", "createdAt", "updatedAt"
) VALUES (
'citizen-web-app',
'Citizen Web Console',
'citizen-web',
'your-client-secret',
'http://localhost:3100/auth/callback,https://prod.example.com/auth/callback',
'web',
false,
NULL,
NOW(),
NOW()
) ON CONFLICT ("clientId") DO NOTHING;Permanent Fix
Upgrade to Better Auth ≥ 1.4.8 when stable, which should handle this automatically.
Lessons Learned
-
Check server logs first — The 500 error returned an empty body, but the actual error was in the server logs with a clear PostgreSQL constraint violation.
-
GitHub issues are valuable — Searching for "trustedClients foreign key" led directly to issue #5468, which confirmed this was a known bug.
-
"Closed" doesn't mean "released" — The issue was marked COMPLETED but the fix was only in beta versions, not the stable release we were using.
-
Database constraints reveal architecture — The FK constraint exposed that
trustedClientswere never meant to bypass the database entirely.
Timeline
| Time | Event |
|---|---|
| T+0 | Configured OAUTH_TRUSTED_CLIENTS env var, restarted containers |
| T+5m | Tested OIDC flow, authorize worked, token exchange returned 500 |
| T+10m | Checked server logs, found FK constraint error |
| T+15m | Searched GitHub issues, found #5468 |
| T+20m | Checked Better Auth versions, confirmed fix is in beta only |
| T+30m | Applied workaround (manual DB insert) |
| T+35m | Verified full OIDC flow works |
Prevention
For future trusted client additions:
- Add to
OAUTH_TRUSTED_CLIENTSenvironment variable - Redeploy the service (sync script runs automatically)
- No manual SQL needed — the sync handles it
Related
- OIDC Integration Guide (internal docs)
- Better Auth OIDC Provider
- GitHub Issue #5468