Add SSO login support via mcdsl/sso
When [sso].redirect_uri is configured, the web UI shows a "Sign in with MCIAS" button instead of the username/password form. Upgrades mcdsl to v1.7.0 which includes the Firefox cookie fix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.wntrmute.dev/mc/mcdsl/auth"
|
||||
"git.wntrmute.dev/mc/mcdsl/csrf"
|
||||
mcdsso "git.wntrmute.dev/mc/mcdsl/sso"
|
||||
"git.wntrmute.dev/mc/mcdsl/web"
|
||||
|
||||
mcqweb "git.wntrmute.dev/mc/mcq/web"
|
||||
@@ -25,16 +26,22 @@ const cookieName = "mcq_session"
|
||||
type Config struct {
|
||||
ServiceName string
|
||||
Tags []string
|
||||
// SSO fields — when RedirectURI is non-empty, the web UI uses SSO instead
|
||||
// of the direct username/password login form.
|
||||
MciasURL string
|
||||
CACert string
|
||||
RedirectURI string
|
||||
}
|
||||
|
||||
// Server is the MCQ web UI server.
|
||||
type Server struct {
|
||||
db *db.DB
|
||||
auth *auth.Authenticator
|
||||
csrf *csrf.Protect
|
||||
render *render.Renderer
|
||||
logger *slog.Logger
|
||||
config Config
|
||||
db *db.DB
|
||||
auth *auth.Authenticator
|
||||
csrf *csrf.Protect
|
||||
ssoClient *mcdsso.Client
|
||||
render *render.Renderer
|
||||
logger *slog.Logger
|
||||
config Config
|
||||
}
|
||||
|
||||
// New creates a web UI server.
|
||||
@@ -45,20 +52,43 @@ func New(cfg Config, database *db.DB, authenticator *auth.Authenticator, logger
|
||||
}
|
||||
csrfProtect := csrf.New(csrfSecret, "_csrf", "csrf_token")
|
||||
|
||||
return &Server{
|
||||
s := &Server{
|
||||
db: database,
|
||||
auth: authenticator,
|
||||
csrf: csrfProtect,
|
||||
render: render.New(),
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create SSO client if the service has an SSO redirect_uri configured.
|
||||
if cfg.RedirectURI != "" {
|
||||
ssoClient, err := mcdsso.New(mcdsso.Config{
|
||||
MciasURL: cfg.MciasURL,
|
||||
ClientID: "mcq",
|
||||
RedirectURI: cfg.RedirectURI,
|
||||
CACert: cfg.CACert,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create SSO client: %w", err)
|
||||
}
|
||||
s.ssoClient = ssoClient
|
||||
logger.Info("SSO enabled: redirecting to MCIAS for login", "mcias_url", cfg.MciasURL)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// RegisterRoutes adds web UI routes to the given router.
|
||||
func (s *Server) RegisterRoutes(r chi.Router) {
|
||||
r.Get("/login", s.handleLoginPage)
|
||||
r.Post("/login", s.csrf.Middleware(http.HandlerFunc(s.handleLogin)).ServeHTTP)
|
||||
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.handleLoginPage)
|
||||
r.Post("/login", s.csrf.Middleware(http.HandlerFunc(s.handleLogin)).ServeHTTP)
|
||||
}
|
||||
r.Get("/static/*", http.FileServer(http.FS(mcqweb.FS)).ServeHTTP)
|
||||
|
||||
// Authenticated routes.
|
||||
@@ -80,6 +110,7 @@ type pageData struct {
|
||||
Error string
|
||||
Title string
|
||||
Content any
|
||||
SSO bool
|
||||
}
|
||||
|
||||
func (s *Server) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -101,6 +132,32 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
// handleSSOLogin renders a landing page with a "Sign in with MCIAS" button.
|
||||
func (s *Server) handleSSOLogin(w http.ResponseWriter, r *http.Request) {
|
||||
web.RenderTemplate(w, mcqweb.FS, "login.html", pageData{SSO: true})
|
||||
}
|
||||
|
||||
// handleSSORedirect initiates the SSO redirect to MCIAS.
|
||||
func (s *Server) handleSSORedirect(w http.ResponseWriter, r *http.Request) {
|
||||
if err := mcdsso.RedirectToLogin(w, r, s.ssoClient, "mcq"); 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 := mcdsso.HandleCallback(w, r, s.ssoClient, "mcq")
|
||||
if err != nil {
|
||||
s.logger.Error("sso: callback", "error", err)
|
||||
http.Error(w, "Login failed. Please try again.", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
web.SetSessionCookie(w, cookieName, token)
|
||||
http.Redirect(w, r, returnTo, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
token := web.GetSessionToken(r, cookieName)
|
||||
if token != "" {
|
||||
|
||||
Reference in New Issue
Block a user