Skip to content

Commit c17b1d6

Browse files
fix(api): keep the port in the GitHub OAuth callback URL
`request.hostname` strips the port, so an instance on anything but 80/443 sent GitHub a redirect_uri of `http://localhost/api/auth/github/callback` and the round-trip failed with a redirect_uri mismatch — local dev on :3001 and the devbox test instance on :3100 both hit it. `request.host` keeps the port; the X-Forwarded-Host path behind the gateway is unchanged. Test covers both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGqwPjt3v4eMfiHpPmCKnR
1 parent 72c3137 commit c17b1d6

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

‎apps/api/src/routes/auth.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,12 @@ function callbackRedirectUri(request: FastifyRequest): string {
6565
const protocol = request.headers['x-forwarded-proto']
6666
? String(request.headers['x-forwarded-proto']).split(',')[0]?.trim() ?? 'http'
6767
: request.protocol;
68+
// `request.host` keeps the port (`request.hostname` strips it), which the
69+
// registered callback carries whenever the app isn't on 80/443 — local dev
70+
// on :3001, a test instance on :3100.
6871
const host = request.headers['x-forwarded-host']
69-
? String(request.headers['x-forwarded-host']).split(',')[0]?.trim() ?? request.hostname
70-
: request.hostname;
72+
? String(request.headers['x-forwarded-host']).split(',')[0]?.trim() ?? request.host
73+
: request.host;
7174
return `${protocol}://${host}/api/auth/github/callback`;
7275
}
7376

‎apps/api/tests/github-oauth.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,28 @@ describe('GET /api/auth/github/start', () => {
198198
expect(state).toBeTruthy();
199199
});
200200

201+
it('derives redirect_uri from the request host, port included, or from X-Forwarded-* behind a proxy', async () => {
202+
const direct = await app.inject({
203+
method: 'GET',
204+
url: '/api/auth/github/start',
205+
headers: { host: 'devbox:3100' },
206+
remoteAddress: nextTestIp(),
207+
});
208+
expect(new URL(String(direct.headers['location'])).searchParams.get('redirect_uri')).toBe(
209+
'http://devbox:3100/api/auth/github/callback',
210+
);
211+
212+
const proxied = await app.inject({
213+
method: 'GET',
214+
url: '/api/auth/github/start',
215+
headers: { host: '10.0.0.5:3001', 'x-forwarded-host': 'codeforphilly.org', 'x-forwarded-proto': 'https' },
216+
remoteAddress: nextTestIp(),
217+
});
218+
expect(new URL(String(proxied.headers['location'])).searchParams.get('redirect_uri')).toBe(
219+
'https://codeforphilly.org/api/auth/github/callback',
220+
);
221+
});
222+
201223
it('sets cfp_oauth_state and cfp_oauth_session cookies with HttpOnly + SameSite=Lax', async () => {
202224
const res = await app.inject({
203225
method: 'GET',

0 commit comments

Comments
 (0)