diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 88d37d2..6c553bf 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -65,9 +65,12 @@ function callbackRedirectUri(request: FastifyRequest): string { const protocol = request.headers['x-forwarded-proto'] ? String(request.headers['x-forwarded-proto']).split(',')[0]?.trim() ?? 'http' : request.protocol; + // `request.host` keeps the port (`request.hostname` strips it), which the + // registered callback carries whenever the app isn't on 80/443 — local dev + // on :3001, a test instance on :3100. const host = request.headers['x-forwarded-host'] - ? String(request.headers['x-forwarded-host']).split(',')[0]?.trim() ?? request.hostname - : request.hostname; + ? String(request.headers['x-forwarded-host']).split(',')[0]?.trim() ?? request.host + : request.host; return `${protocol}://${host}/api/auth/github/callback`; } diff --git a/apps/api/tests/github-oauth.test.ts b/apps/api/tests/github-oauth.test.ts index 609ae14..d85f7a7 100644 --- a/apps/api/tests/github-oauth.test.ts +++ b/apps/api/tests/github-oauth.test.ts @@ -198,6 +198,28 @@ describe('GET /api/auth/github/start', () => { expect(state).toBeTruthy(); }); + it('derives redirect_uri from the request host, port included, or from X-Forwarded-* behind a proxy', async () => { + const direct = await app.inject({ + method: 'GET', + url: '/api/auth/github/start', + headers: { host: 'devbox:3100' }, + remoteAddress: nextTestIp(), + }); + expect(new URL(String(direct.headers['location'])).searchParams.get('redirect_uri')).toBe( + 'http://devbox:3100/api/auth/github/callback', + ); + + const proxied = await app.inject({ + method: 'GET', + url: '/api/auth/github/start', + headers: { host: '10.0.0.5:3001', 'x-forwarded-host': 'codeforphilly.org', 'x-forwarded-proto': 'https' }, + remoteAddress: nextTestIp(), + }); + expect(new URL(String(proxied.headers['location'])).searchParams.get('redirect_uri')).toBe( + 'https://codeforphilly.org/api/auth/github/callback', + ); + }); + it('sets cfp_oauth_state and cfp_oauth_session cookies with HttpOnly + SameSite=Lax', async () => { const res = await app.inject({ method: 'GET',