Skip to content

fix(api): default OIDC discovery issuer to API_EXTERNAL_URL#2505

Open
mastermanas805 wants to merge 1 commit intosupabase:masterfrom
mastermanas805:fix/wellknown-openid-fallback-to-external-url
Open

fix(api): default OIDC discovery issuer to API_EXTERNAL_URL#2505
mastermanas805 wants to merge 1 commit intosupabase:masterfrom
mastermanas805:fix/wellknown-openid-fallback-to-external-url

Conversation

@mastermanas805
Copy link
Copy Markdown

@mastermanas805 mastermanas805 commented Apr 25, 2026

What

Fixes #2487.

The /.well-known/openid-configuration handler in internal/api/jwks.go reads config.JWT.Issuer directly. When self-hosted operators configure API_EXTERNAL_URL (which is required:"true" per internal/conf/configuration.go) but leave GOTRUE_JWT_ISSUER unset, the response comes back with an empty issuer field and relative endpoint URLs. That violates OpenID Connect Discovery 1.0 section 4.2 (issuer MUST be a non-empty URL) and RFC 8414 section 3 (endpoint URLs MUST be absolute).

A second smaller bug: even when JWT.Issuer IS set, the response uses the un-cleaned config value at jwks.go:76 instead of the trailing-slash-stripped local variable that the endpoint URLs at lines 77-80 use, so issuer and the endpoint paths can drift apart on configs with a trailing slash.

How to reproduce on master (for reviewers)

# 1. Bring up auth via the standard dev compose
cd auth
cp example.docker.env .env.docker
# Edit .env.docker: set GOTRUE_JWT_SECRET to a 32+ char string.
# Leave GOTRUE_JWT_ISSUER unset. API_EXTERNAL_URL stays as http://localhost:9999.
make dev
# In another shell:
make migrate_dev

# 2. Hit the discovery endpoint
curl -s http://localhost:9999/.well-known/openid-configuration | python3 -m json.tool

You will see this response on master:

{
  "issuer": "",
  "authorization_endpoint": "/oauth/authorize",
  "token_endpoint": "/oauth/token",
  "jwks_uri": "/.well-known/jwks.json",
  "userinfo_endpoint": "/oauth/userinfo",
  ...
}

Empty issuer + relative endpoints. That is the bug.

Root cause

internal/api/jwks.go, function WellKnownOpenID:

  • Line 68: issuer := config.JWT.Issuer reads the issuer with no fallback. If JWT.Issuer is empty, issuer is empty for the rest of the function.
  • Line 76: the response struct sets Issuer: config.JWT.Issuer rather than Issuer: issuer, so the trailing-slash strip at lines 71-73 never reaches the response field.

Fix

Two-line change in WellKnownOpenID:

  • Default issuer to config.API.ExternalURL when JWT.Issuer is empty (API.ExternalURL is already required:"true" so it is always populated).
  • Change Issuer: config.JWT.Issuer to Issuer: issuer so it picks up the cleaned value.

How to verify the fix manually (for reviewers)

After applying this PR's diff:

# 1. Stop and restart auth (CompileDaemon does the rebuild on file save in dev mode)
# Or restart explicitly:
docker compose -f docker-compose-dev.yml restart auth

# 2. Hit the same endpoint
curl -s http://localhost:9999/.well-known/openid-configuration | python3 -m json.tool

Expected response:

{
  "issuer": "http://localhost:9999",
  "authorization_endpoint": "http://localhost:9999/oauth/authorize",
  "token_endpoint": "http://localhost:9999/oauth/token",
  "jwks_uri": "http://localhost:9999/.well-known/jwks.json",
  "userinfo_endpoint": "http://localhost:9999/oauth/userinfo",
  ...
}

To verify the trailing-slash handling specifically, set GOTRUE_JWT_ISSUER="https://auth.example.com/" in .env.docker (note the trailing slash), restart, and the response issuer should come back as https://auth.example.com without the slash.

Automated tests

internal/api/jwks_test.go (added in this PR):

  • TestWellKnownOpenIDIssuerFallbackToExternalURL — asserts the response issuer and endpoint URLs are absolute and rooted at API.ExternalURL when JWT.Issuer is empty. Fails on master, passes after this fix.
  • TestWellKnownOpenIDIssuerStripsTrailingSlash — sets JWT.Issuer = "https://auth.example.com/" and asserts the response issuer is https://auth.example.com. Fails on master, passes after this fix.

Run them:

go test ./internal/api/ -run TestWellKnownOpenID -v -count=1

Verification I ran locally

  • go test ./internal/api/ -run TestWellKnownOpenID -v -count=1 — both new cases red on master, green after the fix
  • go test ./... -count=1 -p 1 -race — full module suite green
  • gofmt -l internal/api/jwks.go internal/api/jwks_test.go — empty
  • go vet ./... — empty
  • Live curl roundtrip against running auth shows empty issuer / relative endpoints before, absolute URLs after

Behavior change

  • Self-hosted operators who set only API_EXTERNAL_URL: discovery now returns spec-compliant absolute URLs. Previous behavior was a spec violation.
  • Operators who set GOTRUE_JWT_ISSUER explicitly: behavior unchanged, except trailing slashes are now stripped from issuer (already stripped from endpoints). Returning the stripped value matches what the endpoints already use, eliminating an inconsistency.

The /.well-known/openid-configuration handler used config.JWT.Issuer
unconditionally, returning an empty issuer and relative endpoint URLs
for self-hosted operators who configure API_EXTERNAL_URL but leave
GOTRUE_JWT_ISSUER unset. That violates OpenID Connect Discovery 1.0
section 4.2 (issuer MUST be present) and RFC 8414 section 3 (endpoint
URLs MUST be absolute).

Default issuer to config.API.ExternalURL when JWT.Issuer is empty;
API.ExternalURL is required:"true" in the config struct so it is
always populated. Also use the trailing-slash-stripped local variable
when returning the Issuer field so it matches how endpoint URLs are
constructed.

Adds in-process tests asserting that issuer and endpoint URLs are
absolute when JWT.Issuer is empty, and that a trailing slash on a
configured JWT.Issuer is stripped consistently from the issuer field.

Fixes supabase#2487

Signed-off-by: Manas Srivastava <mastermanas805@gmail.com>
@mastermanas805 mastermanas805 marked this pull request as ready for review April 27, 2026 01:36
@mastermanas805 mastermanas805 requested a review from a team as a code owner April 27, 2026 01:36
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.

Self-hosted OAuth/OIDC discovery returns empty issuer and relative endpoints even with API_EXTERNAL_URL and JWT signing keys configured

1 participant