api: gate all routes behind an OIDC session, add session endpoints
Router() now wraps every route except /health, /session/login, and /session/callback in a chi group requiring a valid session cookie (auth.RequireSession). Adds internal/api/session.go with the four session HTTP handlers (login/callback/logout/me) and SessionConfig. NewServer takes an auth.Verifier and SessionConfig. Test infra (doJSON, newTestServer) now mints/attaches a signed session cookie automatically so the 36 pre-existing tests keep exercising the already-logged-in path unchanged, plus 8 new tests cover the gating and session endpoints themselves.
This commit is contained in:
86
backend/internal/api/session.go
Normal file
86
backend/internal/api/session.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"geniusrun/backend/internal/auth"
|
||||
)
|
||||
|
||||
// SessionConfig configures how the app-login session cookie is minted and
|
||||
// validated. Secure should mirror config.Config.SessionSecure (true once
|
||||
// the app is served over HTTPS).
|
||||
type SessionConfig struct {
|
||||
Secret []byte
|
||||
Duration time.Duration
|
||||
Secure bool
|
||||
}
|
||||
|
||||
type sessionMeResponse struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (s *Server) handleSessionLogin(w http.ResponseWriter, r *http.Request) {
|
||||
authURL, txn, err := s.Auth.BeginLogin()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
cookie, err := auth.MintTxnCookie(txn, s.Session.Secret, s.Session.Secure)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
http.Redirect(w, r, authURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) {
|
||||
txnCookie, err := r.Cookie(auth.TxnCookieName)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/?auth_error=failed", http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, auth.ClearCookie(auth.TxnCookieName, s.Session.Secure))
|
||||
|
||||
txn, err := auth.ParseTxnCookie(txnCookie, s.Session.Secret)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/?auth_error=failed", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := s.Auth.HandleCallback(r.Context(), txn, r.URL.Query())
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/?auth_error=failed", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if !result.Authorized {
|
||||
http.Redirect(w, r, "/?auth_error=forbidden", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
sessionCookie, err := auth.MintSessionCookie(result.Claims, s.Session.Secret, s.Session.Duration, s.Session.Secure)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, sessionCookie)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *Server) handleSessionMe(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := auth.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
// Unreachable in practice -- RequireSession already 401s before this
|
||||
// handler runs -- but fail closed rather than panic if that ever changes.
|
||||
writeError(w, http.StatusUnauthorized, "not authenticated")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, sessionMeResponse{Name: claims.Name, Email: claims.Email})
|
||||
}
|
||||
Reference in New Issue
Block a user