diff --git a/backend/cmd/geniusrund/main.go b/backend/cmd/geniusrund/main.go index f9ac804..126b47d 100644 --- a/backend/cmd/geniusrund/main.go +++ b/backend/cmd/geniusrund/main.go @@ -61,9 +61,10 @@ func main() { } server := api.NewServer(db, garminClient, syncSvc, authVerifier, api.SessionConfig{ - Secret: cfg.SessionSecret, - Duration: cfg.SessionDuration, - Secure: cfg.SessionSecure, + Secret: cfg.SessionSecret, + Duration: cfg.SessionDuration, + Secure: cfg.SessionSecure, + PublicBaseURL: cfg.PublicBaseURL, }) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) diff --git a/backend/internal/api/api_test.go b/backend/internal/api/api_test.go index 591cc3b..16174c7 100644 --- a/backend/internal/api/api_test.go +++ b/backend/internal/api/api_test.go @@ -23,9 +23,10 @@ import ( func newCtx() context.Context { return context.Background() } var testSessionConfig = SessionConfig{ - Secret: []byte("test-session-secret-at-least-32-bytes-long"), - Duration: time.Hour, - Secure: false, + Secret: []byte("test-session-secret-at-least-32-bytes-long"), + Duration: time.Hour, + Secure: false, + PublicBaseURL: "https://geniusrun.example.com", } func newTestServer(t *testing.T) (*Server, *store.DB) { diff --git a/backend/internal/api/session.go b/backend/internal/api/session.go index 840bf54..4d0aefe 100644 --- a/backend/internal/api/session.go +++ b/backend/internal/api/session.go @@ -1,6 +1,7 @@ package api import ( + "log" "net/http" "time" @@ -14,6 +15,12 @@ type SessionConfig struct { Secret []byte Duration time.Duration Secure bool + // PublicBaseURL is this app's own externally reachable origin (e.g. + // "https://geniusrun.example.com", no trailing slash), used to build an + // absolute post_logout_redirect_uri for the identity provider -- some + // providers, including Keycloak, require this to be an absolute URL + // matching one registered on the client, not a bare relative path. + PublicBaseURL string } type sessionMeResponse struct { @@ -39,6 +46,7 @@ func (s *Server) handleSessionLogin(w http.ResponseWriter, r *http.Request) { func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) { txnCookie, err := r.Cookie(auth.TxnCookieName) if err != nil { + log.Printf("session callback: missing txn cookie: %v", err) http.Redirect(w, r, "/?auth_error=failed", http.StatusFound) return } @@ -46,12 +54,14 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) { txn, err := auth.ParseTxnCookie(txnCookie, s.Session.Secret) if err != nil { + log.Printf("session callback: failed to parse txn cookie: %v", err) http.Redirect(w, r, "/?auth_error=failed", http.StatusFound) return } result, err := s.Auth.HandleCallback(r.Context(), txn, r.URL.Query()) if err != nil { + 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) return } @@ -71,7 +81,7 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) { func (s *Server) handleSessionLogout(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, auth.ClearCookie(auth.SessionCookieName, s.Session.Secure)) - http.Redirect(w, r, s.Auth.EndSessionURL("/"), http.StatusFound) + http.Redirect(w, r, s.Auth.EndSessionURL(s.Session.PublicBaseURL+"/"), http.StatusFound) } func (s *Server) handleSessionMe(w http.ResponseWriter, r *http.Request) { diff --git a/frontend/src/App.css b/frontend/src/App.css index 6bffae5..64d42dc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -94,9 +94,14 @@ body { } .logout-link { + background: none; + border: none; + padding: 0; + font: inherit; color: #9aa0ab; font-size: 0.85rem; text-decoration: none; + cursor: pointer; } .logout-link:hover { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d32e67a..d5c9243 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { api } from "./api/client"; +import { api, BASE_URL } from "./api/client"; import "./App.css"; import { Dashboard } from "./pages/Dashboard"; import { Plan } from "./pages/Plan"; @@ -56,9 +56,11 @@ function App({ session }: { session: SessionInfo }) { > {profileName ?? "Profile"} - - Log out - +
{AUTH_ERROR_MESSAGES[authError] ?? "Login failed, please try again."}
} - + Log in