fix(api): redirect the OIDC callback to FrontendURL, not a relative path
handleSessionCallback's redirects (success and all 4 failure branches) were relative paths, which resolve against the backend's own origin -- broken in this project's own supported split-origin local dev setup, since the Go backend serves no "/" route at all. Now uses the new config.Config.FrontendURL (defaults to PublicBaseURL, so no change for single-origin production deployments). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ func main() {
|
|||||||
Duration: cfg.SessionDuration,
|
Duration: cfg.SessionDuration,
|
||||||
Secure: cfg.SessionSecure,
|
Secure: cfg.SessionSecure,
|
||||||
PublicBaseURL: cfg.PublicBaseURL,
|
PublicBaseURL: cfg.PublicBaseURL,
|
||||||
|
FrontendURL: cfg.FrontendURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ var testSessionConfig = SessionConfig{
|
|||||||
Duration: time.Hour,
|
Duration: time.Hour,
|
||||||
Secure: false,
|
Secure: false,
|
||||||
PublicBaseURL: "https://geniusrun.example.com",
|
PublicBaseURL: "https://geniusrun.example.com",
|
||||||
|
FrontendURL: "https://app.geniusrun.example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestServer(t *testing.T) (*Server, *store.DB, int64) {
|
func newTestServer(t *testing.T) (*Server, *store.DB, int64) {
|
||||||
@@ -828,7 +829,7 @@ func TestSessionCallback_AuthorizedSetsSessionCookieAndRedirectsHome(t *testing.
|
|||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
s.Router().ServeHTTP(rec, req)
|
s.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/" {
|
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "https://app.geniusrun.example.com/" {
|
||||||
t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location"))
|
t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
var sessionCookie *http.Cookie
|
var sessionCookie *http.Cookie
|
||||||
@@ -864,7 +865,7 @@ func TestSessionCallback_UnauthorizedRedirectsWithoutSessionCookie(t *testing.T)
|
|||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
s.Router().ServeHTTP(rec, req)
|
s.Router().ServeHTTP(rec, req)
|
||||||
|
|
||||||
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/?auth_error=forbidden" {
|
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "https://app.geniusrun.example.com/?auth_error=forbidden" {
|
||||||
t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location"))
|
t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
for _, c := range rec.Result().Cookies() {
|
for _, c := range rec.Result().Cookies() {
|
||||||
@@ -879,7 +880,7 @@ func TestSessionCallback_MissingTxnCookieRedirectsFailed(t *testing.T) {
|
|||||||
req := httptest.NewRequest(http.MethodGet, "/api/session/callback?code=abc&state=s1", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/session/callback?code=abc&state=s1", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
s.Router().ServeHTTP(rec, req)
|
s.Router().ServeHTTP(rec, req)
|
||||||
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/?auth_error=failed" {
|
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "https://app.geniusrun.example.com/?auth_error=failed" {
|
||||||
t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location"))
|
t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ type SessionConfig struct {
|
|||||||
// providers, including Keycloak, require this to be an absolute URL
|
// providers, including Keycloak, require this to be an absolute URL
|
||||||
// matching one registered on the client, not a bare relative path.
|
// matching one registered on the client, not a bare relative path.
|
||||||
PublicBaseURL string
|
PublicBaseURL string
|
||||||
|
// FrontendURL is the origin the browser should land on after the OIDC
|
||||||
|
// callback (success or failure) -- see config.Config.FrontendURL for why
|
||||||
|
// this can differ from PublicBaseURL in a split-origin deployment.
|
||||||
|
FrontendURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type sessionMeResponse struct {
|
type sessionMeResponse struct {
|
||||||
@@ -49,7 +53,7 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
txnCookie, err := r.Cookie(auth.TxnCookieName)
|
txnCookie, err := r.Cookie(auth.TxnCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("session callback: missing txn cookie: %v", err)
|
log.Printf("session callback: missing txn cookie: %v", err)
|
||||||
http.Redirect(w, r, "/?auth_error=failed", http.StatusFound)
|
http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=failed", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.SetCookie(w, auth.ClearCookie(auth.TxnCookieName, s.Session.Secure))
|
http.SetCookie(w, auth.ClearCookie(auth.TxnCookieName, s.Session.Secure))
|
||||||
@@ -57,18 +61,18 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
txn, err := auth.ParseTxnCookie(txnCookie, s.Session.Secret)
|
txn, err := auth.ParseTxnCookie(txnCookie, s.Session.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("session callback: failed to parse txn cookie: %v", err)
|
log.Printf("session callback: failed to parse txn cookie: %v", err)
|
||||||
http.Redirect(w, r, "/?auth_error=failed", http.StatusFound)
|
http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=failed", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.Auth.HandleCallback(r.Context(), txn, r.URL.Query())
|
result, err := s.Auth.HandleCallback(r.Context(), txn, r.URL.Query())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("session callback: HandleCallback failed (state mismatch, code exchange, or ID-token verification): %v", err)
|
log.Printf("session callback: HandleCallback failed (state mismatch, code exchange, or ID-token verification): %v", err)
|
||||||
http.Redirect(w, r, "/?auth_error=failed", http.StatusFound)
|
http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=failed", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !result.Authorized {
|
if !result.Authorized {
|
||||||
http.Redirect(w, r, "/?auth_error=forbidden", http.StatusFound)
|
http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=forbidden", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.SetCookie(w, sessionCookie)
|
http.SetCookie(w, sessionCookie)
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, s.Session.FrontendURL+"/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleSessionLogout(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleSessionLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user