Knowledge Base

Better Auth OIDC trustedClients Foreign Key Bug

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

  1. OIDC authorize endpoint worked correctly
  2. Consent screen was correctly skipped (trusted client)
  3. Authorization code was issued successfully
  4. Token exchange failed with 500 Internal Server Error
  5. 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 oauthApplication database table

However, the oauthAccessToken table has a foreign key constraint:

CONSTRAINT "oauthAccessToken_clientId_fkey"
  FOREIGN KEY ("clientId")
  REFERENCES "oauthApplication"("clientId")
  ON DELETE CASCADE

When 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:

  1. Parses OAUTH_TRUSTED_CLIENTS environment variable
  2. Inserts each client into oauthApplication table
  3. Uses ON CONFLICT DO NOTHING for 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 successfully

Manual 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

  1. 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.

  2. GitHub issues are valuable — Searching for "trustedClients foreign key" led directly to issue #5468, which confirmed this was a known bug.

  3. "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.

  4. Database constraints reveal architecture — The FK constraint exposed that trustedClients were never meant to bypass the database entirely.

Timeline

TimeEvent
T+0Configured OAUTH_TRUSTED_CLIENTS env var, restarted containers
T+5mTested OIDC flow, authorize worked, token exchange returned 500
T+10mChecked server logs, found FK constraint error
T+15mSearched GitHub issues, found #5468
T+20mChecked Better Auth versions, confirmed fix is in beta only
T+30mApplied workaround (manual DB insert)
T+35mVerified full OIDC flow works

Prevention

For future trusted client additions:

  1. Add to OAUTH_TRUSTED_CLIENTS environment variable
  2. Redeploy the service (sync script runs automatically)
  3. No manual SQL needed — the sync handles it

Related