Add SSO login support

- Add [sso] config section with redirect_uri
- Create mcdsl/sso client when SSO is configured
- Add /login (landing page), /sso/redirect, /sso/callback routes
- Add /logout route
- Update login template with SSO landing page variant
- Bump mcdsl to v1.6.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 20:23:25 -07:00
parent ae4cc8b420
commit 647fd26e60
2619 changed files with 6833933 additions and 9 deletions

View File

@@ -17,6 +17,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
mcdsso "git.wntrmute.dev/mc/mcdsl/sso"
"git.wntrmute.dev/mc/mcdsl/web"
)
@@ -37,7 +38,14 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
r.Get("/", ws.handleRoot)
r.HandleFunc("/init", ws.handleInit)
r.HandleFunc("/unseal", ws.handleUnseal)
r.HandleFunc("/login", ws.handleLogin)
if ws.ssoClient != nil {
r.Get("/login", ws.handleSSOLogin)
r.Get("/sso/redirect", ws.handleSSORedirect)
r.Get("/sso/callback", ws.handleSSOCallback)
} else {
r.HandleFunc("/login", ws.handleLogin)
}
r.Get("/logout", ws.handleLogout)
r.Get("/dashboard", ws.requireAuth(ws.handleDashboard))
r.Post("/dashboard/mount-ca", ws.requireAuth(ws.handleDashboardMountCA))
r.Post("/dashboard/mount-engine", ws.requireAuth(ws.handleDashboardMountEngine))
@@ -236,6 +244,43 @@ func (ws *WebServer) handleLogin(w http.ResponseWriter, r *http.Request) {
}
}
// handleSSOLogin renders a landing page with a "Sign in with MCIAS" button.
func (ws *WebServer) handleSSOLogin(w http.ResponseWriter, r *http.Request) {
state, _ := ws.vault.Status(r.Context())
if state != "unsealed" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
ws.renderTemplate(w, "login.html", map[string]interface{}{"SSO": true})
}
// handleSSORedirect initiates the SSO redirect to MCIAS.
func (ws *WebServer) handleSSORedirect(w http.ResponseWriter, r *http.Request) {
if err := mcdsso.RedirectToLogin(w, r, ws.ssoClient, "metacrypt"); err != nil {
ws.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 (ws *WebServer) handleSSOCallback(w http.ResponseWriter, r *http.Request) {
token, returnTo, err := mcdsso.HandleCallback(w, r, ws.ssoClient, "metacrypt")
if err != nil {
ws.logger.Error("sso: callback", "error", err)
http.Error(w, "Login failed. Please try again.", http.StatusUnauthorized)
return
}
web.SetSessionCookie(w, "metacrypt_token", token)
http.Redirect(w, r, returnTo, http.StatusSeeOther)
}
// handleLogout clears the session and redirects to login.
func (ws *WebServer) handleLogout(w http.ResponseWriter, r *http.Request) {
web.ClearSessionCookie(w, "metacrypt_token")
http.Redirect(w, r, "/login", http.StatusFound)
}
func (ws *WebServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context())
token := extractCookie(r)