Skip to content

fix(config): let TMI_OAUTH_CALLBACK_URL outrank the database row - #626

Merged
ericfitz merged 1 commit into
mainfrom
fix/oauth-callback-url-env-precedence
Jul 30, 2026
Merged

fix(config): let TMI_OAUTH_CALLBACK_URL outrank the database row#626
ericfitz merged 1 commit into
mainfrom
fix/oauth-callback-url-env-precedence

Conversation

@ericfitz

Copy link
Copy Markdown
Owner

Symptom

GET https://api.tmi.dev/oauth2/providers advertised localhost for every provider:

{"id":"google","redirect_uri":"http://localhost:8080/oauth2/callback"}
{"id":"github","redirect_uri":"http://localhost:8080/oauth2/callback"}
{"id":"microsoft","redirect_uri":"http://localhost:8080/oauth2/callback"}

OAuth sign-in cannot complete — Google rejects the redirect_uri mismatch.

The deployment's env var was correct the whole time: the tmi-oauth-providers Secret has TMI_OAUTH_CALLBACK_URL=https://api.tmi.dev/oauth2/callback, and the pod started 4 days after the Secret was last patched, so it definitely had that value. A stale-pod-env explanation is ruled out.

Root cause

auth.oauth_callback_url was the one singleton setting that hardcoded Source: "config" rather than routing through settingSource() — the other 130 all do it correctly. Compare:

setting registration
server.base_url Source: settingSource("TMI_SERVER_BASE_URL"), EnvVar: …
auth.oauth_callback_url Source: "config", no EnvVar

Source feeds Explicit, and SettingsService.getConfigSetting discards any operational setting that is not Explicit (#415). So GetString("auth.oauth_callback_url") always fell through to the database no matter what the operator set. The database held the localhost struct default, and since Handlers.oauthCallbackURL returns the runtime value whenever it's non-empty, that stale row won and the correct env-backed h.config.OAuth.CallbackURL was never reached.

The chain, all confirmed by reading code and live state:

oauthCallbackURL → GetOAuthCallbackURL → GetString("auth.oauth_callback_url")
  → getConfigSetting: found, but !Explicit → not-found
  → DB row: "http://localhost:8080/oauth2/callback"   ← wins
  → (correct env value never consulted)

Fix

Route Source through settingSource("TMI_OAUTH_CALLBACK_URL") and record EnvVar, matching every other env-backed key.

One deliberate subtlety

Only TMI_OAUTH_CALLBACK_URL is named. auth/config.go also honors a legacy OAUTH_CALLBACK_URL, and my first attempt counted both — that was wrong and would have been a worse regression. This package's OAuthConfig.CallbackURL struct tag binds only TMI_OAUTH_CALLBACK_URL, so a legacy-only operator would get Value = localhost default with Source = "environment" → the default becomes Explicit and outranks a correct DB row. A fallback inverted into an override. A test pins this, and settingSource's doc comment now states the constraint.

Tests

Three cases, all verified failing before the fix:

  • env var set → Source: "environment", Explicit: true, EnvVar recorded
  • env var unset → stays non-Explicit so the DB remains reachable (this is an admin-only, hot-reloadable operational key)
  • unbound legacy name set → must not mark the default Explicit

make lint 0 issues · make test-unit 2447 passed / 0 failed

Deploying this is what restores AWS sign-in

Note the ordering problem: the broken callback blocks interactive OAuth login, which blocks the admin API, which is the only way to hot-fix the DB row (service-account tokens are denied on /admin/* per #399). So the DB-row route is not reachable while the bug is live — shipping this fix is the practical path.

config-reference.md is unchanged: auth.oauth_callback_url sits in the Operational table, which has no Env var column.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WnrVyySuyJFqcMxB8q3pNc

On AWS, GET /oauth2/providers advertised
redirect_uri=http://localhost:8080/oauth2/callback for Google, GitHub and
Microsoft, so OAuth sign-in could not complete — even though the deployment's
TMI_OAUTH_CALLBACK_URL was correctly set to
https://api.tmi.dev/oauth2/callback.

auth.oauth_callback_url was the one singleton setting that hardcoded
Source: "config" instead of routing through settingSource(); the other 130 all
do. Source feeds Explicit, and SettingsService.getConfigSetting discards any
operational setting that is not Explicit, so GetString("auth.oauth_callback_url")
always fell through to the database regardless of what the operator configured.
The database held the localhost struct default, and because
Handlers.oauthCallbackURL returns the runtime value whenever it is non-empty,
that stale row won and the correct env-backed value was never reached.

Route Source through settingSource("TMI_OAUTH_CALLBACK_URL") and record EnvVar,
matching every other env-backed key.

Only TMI_OAUTH_CALLBACK_URL is named, deliberately. auth/config.go also honors a
legacy OAUTH_CALLBACK_URL, but this package's OAuthConfig.CallbackURL struct tag
does not bind it -- so counting it would report "environment" while Value was
still the localhost default, making that default Explicit and letting it outrank
a correct database row. That inverts a fallback into an override and is a worse
failure than the one being fixed; a test pins the behavior, and settingSource's
doc comment now states the constraint.

Tests cover all three paths: env set wins, unset defers to the database, and the
unbound legacy name does not mark the default Explicit. Verified failing before
the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnrVyySuyJFqcMxB8q3pNc
@ericfitz
ericfitz merged commit 98d8987 into main Jul 30, 2026
12 checks passed
@ericfitz
ericfitz deleted the fix/oauth-callback-url-env-precedence branch July 30, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant