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:
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
// Config is the top-level configuration for Metacrypt.
|
||||
type Config struct {
|
||||
Server ServerConfig `toml:"server"`
|
||||
Web WebConfig `toml:"web"`
|
||||
MCIAS MCIASConfig `toml:"mcias"`
|
||||
Server ServerConfig `toml:"server"`
|
||||
Web WebConfig `toml:"web"`
|
||||
MCIAS MCIASConfig `toml:"mcias"`
|
||||
SSO SSOConfig `toml:"sso"`
|
||||
Database mcdslconfig.DatabaseConfig `toml:"database"`
|
||||
Log mcdslconfig.LogConfig `toml:"log"`
|
||||
Seal SealConfig `toml:"seal"`
|
||||
Audit AuditConfig `toml:"audit"`
|
||||
Seal SealConfig `toml:"seal"`
|
||||
Audit AuditConfig `toml:"audit"`
|
||||
}
|
||||
|
||||
// ServerConfig holds HTTP/gRPC server settings. It embeds the standard
|
||||
@@ -33,6 +34,13 @@ type MCIASConfig struct {
|
||||
ServiceToken string `toml:"service_token"`
|
||||
}
|
||||
|
||||
// SSOConfig holds SSO redirect settings for the web UI.
|
||||
type SSOConfig struct {
|
||||
// RedirectURI is the callback URL that MCIAS redirects to after login.
|
||||
// Must exactly match the redirect_uri registered in MCIAS config.
|
||||
RedirectURI string `toml:"redirect_uri"`
|
||||
}
|
||||
|
||||
// WebConfig holds settings for the standalone web UI server (metacrypt-web).
|
||||
type WebConfig struct {
|
||||
// ListenAddr is the address the web server listens on (default: 127.0.0.1:8080).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
mcdslauth "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"
|
||||
"git.wntrmute.dev/mc/metacrypt/internal/config"
|
||||
webui "git.wntrmute.dev/mc/metacrypt/web"
|
||||
@@ -115,10 +116,11 @@ type cachedUsername struct {
|
||||
type WebServer struct {
|
||||
cfg *config.Config
|
||||
vault vaultBackend
|
||||
logger *slog.Logger
|
||||
logger *slog.Logger
|
||||
httpSrv *http.Server
|
||||
staticFS fs.FS
|
||||
csrf *csrf.Protect
|
||||
ssoClient *mcdsso.Client
|
||||
tgzCache sync.Map // key: UUID string → *tgzEntry
|
||||
userCache sync.Map // key: UUID string → *cachedUsername
|
||||
}
|
||||
@@ -169,6 +171,21 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
|
||||
csrf: csrf.New(secret, "metacrypt_csrf", "csrf_token"),
|
||||
}
|
||||
|
||||
// Create SSO client if the service has an SSO redirect_uri configured.
|
||||
if cfg.SSO.RedirectURI != "" {
|
||||
ssoClient, ssoErr := mcdsso.New(mcdsso.Config{
|
||||
MciasURL: cfg.MCIAS.ServerURL,
|
||||
ClientID: "metacrypt",
|
||||
RedirectURI: cfg.SSO.RedirectURI,
|
||||
CACert: cfg.MCIAS.CACert,
|
||||
})
|
||||
if ssoErr != nil {
|
||||
return nil, fmt.Errorf("webserver: create SSO client: %w", ssoErr)
|
||||
}
|
||||
ws.ssoClient = ssoClient
|
||||
logger.Info("SSO enabled: redirecting to MCIAS for login", "mcias_url", cfg.MCIAS.ServerURL)
|
||||
}
|
||||
|
||||
if tok := cfg.MCIAS.ServiceToken; tok != "" {
|
||||
a, err := mcdslauth.New(mcdslauth.Config{
|
||||
ServerURL: cfg.MCIAS.ServerURL,
|
||||
|
||||
Reference in New Issue
Block a user