Auth: post-login return-to redirect (internal paths only)
closedGoal
Return the user to the page they were heading to before login, without introducing an open redirect.
Task
- Session middleware redirects unauthenticated requests to
/login?next=<original-path>. - Login flow carries
nextthrough to the OAuth callback. - Callback validates
nextis strictly an internal path before redirecting: must start with a single/, and reject//,/\, and any:or\(blockshttp://, protocol-relative, and backslash tricks). Default to/when absent or invalid. - Never redirect to a path outside the app.
Acceptance
Table-driven test for the next validator:
- accept: /domains/x, /
- reject: https://evil.example, //evil.example, /\\evil, javascript:alert(1)
Depends on: 40e284e (session middleware redirect) + 70790ed S4.
1 Comment
Implementation complete
Post-login return-to redirect landed, with the validator as the single choke point. Not closing until you’ve reviewed.
Flow
SessionAuth.Middleware(internal/appview/ui/session.go) now 303s unauthenticated requests to/login?next=<escaped RequestURI>(path + query preserved).GET /loginpasses?next=intoLoginPage(next)(handler.go,login.templ), which renders a hiddennextfield.POST /oauth/loginstashesnext(form, then query fallback) in a short-livedsovrn_return_tocookie (HttpOnly; Secure; SameSite=Lax; Path=/; MaxAge=300). Cookie transport is required: indigo’sStartAuthFlowgenerates its ownstatewith no passthrough, andCallbackURLis fixed in client metadata.GET /oauth/callbackone-shot consumes + clears the cookie and redirects to the validated target (still 302Found); absent/invalid defaults to/.LogoutHandlerstill redirects to bare/login.Hardening (
internal/authbroker/redirect.go: SafeRedirectTarget)Accept only strings starting with exactly one
/; reject//and/\prefixes; reject any:or\; re-check afterPathUnescape(blocks/%2f%2fevil,/%5c,%3a); reject ASCII controls. Stored raw, validated once at consume time.Tests
authbroker/redirect_test.go: acceptance table (accept/domains/x,/; rejecthttps://evil.example,//evil.example,/\evil,javascript:alert(1), plus empty/bare-path/colon/backslash/encoded variants), cookie round-trip +HttpOnly+ one-shot clear, evil-defaults-to-/, missing-defaults-to-/, andTestLoginHandlerStashesReturnToCookie(login 400s on garbage handle but still stashes the cookie, which pops to/domains/x).ui:TestSessionAuthRedirectCarriesNext(/domains/d1?msg=x->/login?next=...),TestLoginPagePreservesNext(hidden field rendered); existing/loginexpectations updated to/login?next=%2F.go test ./...green,go vet ./...clean,gofmtclean. Regeneratedlogin_templ.goviatempl generate; reverted unrelatedFileName-churn in other*_templ.gofiles.