Add SSO login support
MCAT can now redirect users to MCIAS for SSO login (including passkey support) instead of showing its own login form. SSO is opt-in via the [sso] config section. - Add SSO landing page with "Sign in with MCIAS" button - Add /sso/redirect and /sso/callback routes - Update mcdsl to v1.5.0 (sso package) - Fix .gitignore: /mcat ignores only the root binary, not cmd/mcat/ - Track cmd/mcat/ source files (previously gitignored by accident) Security: - State cookie uses SameSite=Lax for cross-site redirect compatibility - Session cookie remains SameSite=Strict after login Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"git.wntrmute.dev/mc/mcdsl/auth"
|
||||
"git.wntrmute.dev/mc/mcdsl/csrf"
|
||||
"git.wntrmute.dev/mc/mcdsl/httpserver"
|
||||
"git.wntrmute.dev/mc/mcdsl/sso"
|
||||
"git.wntrmute.dev/mc/mcdsl/web"
|
||||
|
||||
mcatweb "git.wntrmute.dev/mc/mcat/web"
|
||||
@@ -33,16 +34,17 @@ type Config struct {
|
||||
|
||||
// Server is the mcat web UI server.
|
||||
type Server struct {
|
||||
wsCfg Config
|
||||
auth *auth.Authenticator
|
||||
logger *slog.Logger
|
||||
csrf *csrf.Protect
|
||||
staticFS fs.FS
|
||||
handler http.Handler
|
||||
wsCfg Config
|
||||
auth *auth.Authenticator
|
||||
logger *slog.Logger
|
||||
csrf *csrf.Protect
|
||||
staticFS fs.FS
|
||||
handler http.Handler
|
||||
ssoClient *sso.Client
|
||||
}
|
||||
|
||||
// New creates a new web UI server.
|
||||
func New(wsCfg Config, authenticator *auth.Authenticator, logger *slog.Logger) (*Server, error) {
|
||||
func New(wsCfg Config, authenticator *auth.Authenticator, logger *slog.Logger, ssoClient *sso.Client) (*Server, error) {
|
||||
staticFS, err := fs.Sub(mcatweb.FS, "static")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webserver: static fs: %w", err)
|
||||
@@ -54,11 +56,12 @@ func New(wsCfg Config, authenticator *auth.Authenticator, logger *slog.Logger) (
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
wsCfg: wsCfg,
|
||||
auth: authenticator,
|
||||
logger: logger,
|
||||
csrf: csrf.New(secret, csrfCookieName, csrfFieldName),
|
||||
staticFS: staticFS,
|
||||
wsCfg: wsCfg,
|
||||
auth: authenticator,
|
||||
logger: logger,
|
||||
csrf: csrf.New(secret, csrfCookieName, csrfFieldName),
|
||||
staticFS: staticFS,
|
||||
ssoClient: ssoClient,
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
@@ -79,8 +82,14 @@ func (s *Server) registerRoutes(r chi.Router) {
|
||||
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(s.staticFS))))
|
||||
|
||||
r.Get("/", s.handleRoot)
|
||||
r.Get("/login", s.handleLogin)
|
||||
r.Post("/login", s.handleLogin)
|
||||
if s.ssoClient != nil {
|
||||
r.Get("/login", s.handleSSOLogin)
|
||||
r.Get("/sso/redirect", s.handleSSORedirect)
|
||||
r.Get("/sso/callback", s.handleSSOCallback)
|
||||
} else {
|
||||
r.Get("/login", s.handleLogin)
|
||||
r.Post("/login", s.handleLogin)
|
||||
}
|
||||
r.Post("/logout", s.requireAuth(s.handleLogout))
|
||||
r.Get("/dashboard", s.requireAuth(s.handleDashboard))
|
||||
}
|
||||
@@ -131,6 +140,37 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||
}
|
||||
|
||||
// handleSSOLogin renders a landing page with a "Sign in with MCIAS" button.
|
||||
func (s *Server) handleSSOLogin(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderTemplate(w, "login.html", map[string]interface{}{
|
||||
"SSO": true,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSSORedirect initiates the SSO redirect to MCIAS.
|
||||
func (s *Server) handleSSORedirect(w http.ResponseWriter, r *http.Request) {
|
||||
if err := sso.RedirectToLogin(w, r, s.ssoClient, "mcat"); err != nil {
|
||||
s.logger.Error("sso: redirect to login", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// handleSSOCallback exchanges the authorization code for a JWT and sets the session.
|
||||
func (s *Server) handleSSOCallback(w http.ResponseWriter, r *http.Request) {
|
||||
token, returnTo, err := sso.HandleCallback(w, r, s.ssoClient, "mcat")
|
||||
if err != nil {
|
||||
s.logger.Error("sso: callback", "error", err)
|
||||
s.renderTemplate(w, "login.html", map[string]interface{}{
|
||||
"SSO": s.ssoClient != nil,
|
||||
"Error": "Login failed. Please try again.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
web.SetSessionCookie(w, sessionCookieName, token)
|
||||
http.Redirect(w, r, returnTo, http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
token := web.GetSessionToken(r, sessionCookieName)
|
||||
if token != "" {
|
||||
|
||||
Reference in New Issue
Block a user