From 6dc7f8ba1e153ad3c1bb01316802b54f88c99c19 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 19 Jun 2026 18:26:51 +0200 Subject: [PATCH] fix(auth): correct redirect-hash encoding so oauth.authorize reaches /login#redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/router/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 7c16d42cb..580ade3c5 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -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 }