2 Commits

Author SHA1 Message Date
453c52584c Fix SSO cookies not stored on Firefox 302 redirects
Firefox does not reliably store Set-Cookie headers on 302 responses
that redirect to a different origin. Change RedirectToLogin to return
a 200 with an HTML meta-refresh instead, ensuring cookies are stored
before navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:13:37 -07:00
bcab16f2bf Fix SSO return-to redirect loop
SetReturnToCookie stored /sso/redirect as the return-to path,
causing a redirect loop after successful SSO login: the callback
would redirect back to /sso/redirect instead of /. Filter all
/sso/* paths, not just /sso/callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:54:55 -07:00
2 changed files with 18 additions and 3 deletions

View File

@@ -22,6 +22,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@@ -229,7 +230,7 @@ func ValidateStateCookie(w http.ResponseWriter, r *http.Request, prefix, querySt
// redirect back to it after SSO login completes. // redirect back to it after SSO login completes.
func SetReturnToCookie(w http.ResponseWriter, r *http.Request, prefix string) { func SetReturnToCookie(w http.ResponseWriter, r *http.Request, prefix string) {
path := r.URL.Path path := r.URL.Path
if path == "" || path == "/login" || path == "/sso/callback" { if path == "" || path == "/login" || strings.HasPrefix(path, "/sso/") {
path = "/" path = "/"
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
@@ -268,6 +269,12 @@ func ConsumeReturnToCookie(w http.ResponseWriter, r *http.Request, prefix string
// RedirectToLogin generates a state, sets the state and return-to cookies, // RedirectToLogin generates a state, sets the state and return-to cookies,
// and redirects the user to the MCIAS authorize URL. // and redirects the user to the MCIAS authorize URL.
//
// The redirect is performed via a 200 response with an HTML meta-refresh
// instead of a 302. Some browsers (notably Firefox) do not reliably store
// Set-Cookie headers on 302 responses that redirect to a different origin,
// even when the origins are same-site. Using a 200 response ensures the
// cookies are stored before the browser navigates away.
func RedirectToLogin(w http.ResponseWriter, r *http.Request, client *Client, cookiePrefix string) error { func RedirectToLogin(w http.ResponseWriter, r *http.Request, client *Client, cookiePrefix string) error {
state, err := GenerateState() state, err := GenerateState()
if err != nil { if err != nil {
@@ -276,7 +283,15 @@ func RedirectToLogin(w http.ResponseWriter, r *http.Request, client *Client, coo
SetStateCookie(w, cookiePrefix, state) SetStateCookie(w, cookiePrefix, state)
SetReturnToCookie(w, r, cookiePrefix) SetReturnToCookie(w, r, cookiePrefix)
http.Redirect(w, r, client.AuthorizeURL(state), http.StatusFound)
authorizeURL := client.AuthorizeURL(state)
escaped := html.EscapeString(authorizeURL)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprintf(w, `<!DOCTYPE html>
<html><head><meta http-equiv="refresh" content="0;url=%s"></head>
<body><p>Redirecting to <a href="%s">MCIAS</a>...</p></body></html>`,
escaped, escaped)
return nil return nil
} }

View File

@@ -193,7 +193,7 @@ func TestReturnToDefaultsToRoot(t *testing.T) {
} }
func TestReturnToSkipsLoginPaths(t *testing.T) { func TestReturnToSkipsLoginPaths(t *testing.T) {
for _, p := range []string{"/login", "/sso/callback"} { for _, p := range []string{"/login", "/sso/callback", "/sso/redirect"} {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, p, nil) req := httptest.NewRequest(http.MethodGet, p, nil)
SetReturnToCookie(rec, req, "mcr") SetReturnToCookie(rec, req, "mcr")