fix: OIDC callback sent wrong scheme in token exchange redirect_uri
CI / web (push) Successful in 17s
CI / api (push) Successful in 24s

Fastify only sees plain HTTP -- TLS terminates at NPM/Cloudflare
before reaching this process. Building the callback's currentUrl from
req.headers.host with a hardcoded "http://" sent
redirect_uri=http://monitor.jerodrigged.com/... during the token
exchange, which Authentik rejects (logged as generic "invalid_client"
to the client, but its own event log said plainly: "Invalid redirect
URI used by provider"). Fixed by reusing the known-correct
redirectUri's origin and only taking the query string from the actual
request, instead of trying to infer scheme from headers.

Also fixes the Playwright OIDC test's selectors (Authentik's password
field has no <label> association -- placeholder text, not getByLabel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:27:39 -06:00
parent 9b5051d3ac
commit 294be26500
2 changed files with 13 additions and 2 deletions
+10 -1
View File
@@ -27,7 +27,16 @@ export function registerOidcRoutes(
return reply.code(400).send({ error: "no OIDC login in progress" });
}
try {
const currentUrl = new URL(req.url, `http://${req.headers.host}`);
// Fastify only ever sees plain HTTP here -- TLS terminates at NPM/Cloudflare
// before reaching this process (see docs/oidc-setup.md). Building this URL
// from req.headers.host with a hardcoded "http://" sent the wrong scheme in
// the token exchange's redirect_uri, which Authentik rejects as a mismatch
// against the https:// URL registered for this provider. Since redirectUri
// is guaranteed correct (it's the exact value used to build the original
// authorize request), reuse its origin and only take the query string from
// the actual incoming request.
const currentUrl = new URL(redirectUri);
currentUrl.search = new URL(req.url, "http://placeholder").search;
const { username } = await handleCallback(oidcConfig, currentUrl, oidcState, oidcCodeVerifier);
req.session.username = username;
req.session.oidcState = undefined;
+3 -1
View File
@@ -20,7 +20,9 @@ test("OIDC login via Authentik stays on public domains throughout", async ({ pag
await page.getByPlaceholder(/email or username/i).fill(USERNAME);
await page.getByRole("button", { name: /log in|continue|next/i }).click();
await page.getByLabel(/password/i).fill(PASSWORD);
// Authentik's password field has no <label> association -- it's identified
// by placeholder text ("Please enter your password"), not an a11y label.
await page.getByPlaceholder(/password/i).fill(PASSWORD);
await page.getByRole("button", { name: /log in|continue|sign in/i }).click();
await page.waitForURL(/^https:\/\/monitor\.jerodrigged\.com\//, { timeout: 15_000 });