mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-15 19:06:16 +08:00
The auth guard asked the issuer to come back to the page the visitor wanted. The issuer appends ?code= to whatever URL it is given, but only /auth/callback redeems a code — so it arrived on the target page, sat there unread, the guard found no session and bounced back for a fresh code. The browser ping-ponged between the two hosts until the user gave up. The guard now hands over /auth/callback and carries the wanted page in its redirect parameter, which is exactly what that page already expected. A spent code in the URL is dropped rather than carried along, so a stale link cannot turn into a redemption error one hop later. URL building moved into shared/utils/ssoHandoff.ts because the same mistake existed twice — the retry button on the callback page had it too — and because a redirect loop deserves a regression test that does not need a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
33 lines
1.3 KiB
TypeScript
33 lines
1.3 KiB
TypeScript
/**
|
|
* Builds the URL that sends an unauthenticated visitor to the SSO issuer.
|
|
*
|
|
* The one rule that matters: the issuer appends `?code=` to whatever URL it is
|
|
* handed, and only `/auth/callback` redeems that code. Handing it the page the
|
|
* user actually wanted leaves the code unread in the address bar — the auth
|
|
* guard then bounces back to the issuer for a fresh one, and the browser ping
|
|
* pongs between the two hosts forever.
|
|
*/
|
|
|
|
/**
|
|
* Drops a `code` already present in a path. Codes are single-use, so carrying
|
|
* a spent one through the round trip can only produce a redemption error on
|
|
* the far side.
|
|
*/
|
|
export function stripSsoCode(path: string, origin: string): string {
|
|
if (!path.includes('code=')) return path
|
|
const url = new URL(path, origin)
|
|
url.searchParams.delete('code')
|
|
return `${url.pathname}${url.search}${url.hash}`
|
|
}
|
|
|
|
/**
|
|
* @param issuer the website origin, without a trailing slash
|
|
* @param origin this app's own origin
|
|
* @param target the in-app path the visitor was trying to reach
|
|
*/
|
|
export function buildIssuerLoginUrl(issuer: string, origin: string, target: string): string {
|
|
const callback = new URL('/auth/callback', origin)
|
|
callback.searchParams.set('redirect', stripSsoCode(target || '/', origin))
|
|
return `${issuer.replace(/\/+$/, '')}/login?redirect=${encodeURIComponent(callback.toString())}`
|
|
}
|