fix(auth): correct redirect-hash encoding so oauth.authorize reaches /login#redirect

The previous attempt updated the oauth-authorize e2e assertion to expect
/login#redirect= but never ran the test, and the implementation did not
actually produce it.

Root cause: the guard wrote the redirect hash as
encodeURIComponent(to.fullPath), but vue-router runs its own encodeURI over
the hash field, turning the embedded %xx into %25xx (double-encoding). On the
follow-up navigation to /login the address-bar hash (to.fullPath) was then
double-encoded while to.hash was single-decoded, so the pre-existing
`!to.fullPath.endsWith(to.hash)` fallback never matched and kept re-appending
the hash — an infinite redirect loop that left the URL stuck on
/oauth/authorize.

Fix:
- Pass to.fullPath to the hash raw and read it back without an extra
  decodeURIComponent. vue-router's encode/decode round-trips it cleanly, so
  the address bar carries a single-encoded, copyable destination and the
  guard recovers the exact original fullPath for saveLastVisited.
- Skip the endsWith hash re-attach when the hash is the redirect hash, which
  vue-router keeps url-encoded in fullPath, breaking the loop.

Verified locally: tests/e2e/user/oauth-authorize.spec.ts now passes
(redirect to /login#redirect=, decoded hash contains /oauth/authorize, and
the post-login PKCE code exchange completes); logout.spec.ts still passes.
openid-login.spec.ts requires the Dex service container that only CI
provides, so it cannot run locally; it does not touch the changed paths.
This commit is contained in:
kolaente 2026-06-19 18:26:51 +02:00
parent a9355fc247
commit 6dc7f8ba1e
1 changed files with 13 additions and 4 deletions

View File

@ -502,18 +502,20 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
// Keep the destination in the address bar (not just per-browser localStorage) so a native
// client's /oauth/authorize URL stays copyable into another browser. Hash, not query, so the
// embedded OAuth params never reach access logs (#2654).
// embedded OAuth params never reach access logs (#2654). Pass fullPath raw: vue-router encodes
// the hash itself, so an extra encodeURIComponent here would be double-encoded in the URL.
if (to.name === 'oauth.authorize') {
return {
name: 'user.login',
hash: REDIRECT_HASH_PREFIX + encodeURIComponent(to.fullPath),
hash: REDIRECT_HASH_PREFIX + to.fullPath,
}
}
// Fold the hash destination into localStorage: it's the only bridge that survives the
// external OIDC round-trip out of the SPA, so redirectIfSaved() works after any auth method.
// vue-router already decoded to.hash once, so it equals the fullPath we wrote above as-is.
if (to.hash.startsWith(REDIRECT_HASH_PREFIX)) {
const destination = decodeURIComponent(to.hash.slice(REDIRECT_HASH_PREFIX.length))
const destination = to.hash.slice(REDIRECT_HASH_PREFIX.length)
const resolved = router.resolve(destination)
saveLastVisited(resolved.name as string, resolved.params, resolved.query)
}
@ -589,7 +591,14 @@ router.beforeEach(async (to, from) => {
...newRoute,
}
}
// to.fullPath keeps the redirect hash url-encoded while to.hash is decoded, so the endsWith
// check below never matches and would re-append the hash forever. The hash is already on the
// URL here, so skip the re-attach (#2654).
if (to.hash.startsWith(REDIRECT_HASH_PREFIX)) {
return
}
if(!to.fullPath.endsWith(to.hash)) {
return to.fullPath + to.hash
}