package ui import ( "net/http" "net/url" "git.wntrmute.dev/mc/mcias/internal/audit" "git.wntrmute.dev/mc/mcias/internal/model" "git.wntrmute.dev/mc/mcias/internal/sso" ) // handleSSOAuthorize validates the SSO request parameters against registered // clients, creates an SSO session, and redirects to /login with the SSO nonce. // // Security: the client_id and redirect_uri are validated against the MCIAS // config (exact match). The state parameter is opaque and carried through // unchanged. An SSO session is created server-side so the nonce is the only // value embedded in the login form. func (u *UIServer) handleSSOAuthorize(w http.ResponseWriter, r *http.Request) { clientID := r.URL.Query().Get("client_id") redirectURI := r.URL.Query().Get("redirect_uri") state := r.URL.Query().Get("state") if clientID == "" || redirectURI == "" || state == "" { http.Error(w, "missing required parameters: client_id, redirect_uri, state", http.StatusBadRequest) return } // Security: validate client_id against registered SSO clients. client := u.cfg.SSOClient(clientID) if client == nil { u.logger.Warn("sso: unknown client_id", "client_id", clientID) http.Error(w, "unknown client_id", http.StatusBadRequest) return } // Security: redirect_uri must exactly match the registered URI to prevent // open-redirect attacks. if redirectURI != client.RedirectURI { u.logger.Warn("sso: redirect_uri mismatch", "client_id", clientID, "expected", client.RedirectURI, "got", redirectURI) http.Error(w, "redirect_uri does not match registered URI", http.StatusBadRequest) return } nonce, err := sso.StoreSession(clientID, redirectURI, state) if err != nil { u.logger.Error("sso: store session", "error", err) http.Error(w, "internal error", http.StatusInternalServerError) return } u.writeAudit(r, model.EventSSOAuthorize, nil, nil, audit.JSON("client_id", clientID)) http.Redirect(w, r, "/login?sso="+url.QueryEscape(nonce), http.StatusFound) } // buildSSOCallback consumes the SSO session, generates an authorization code, // and returns the callback URL with code and state parameters. Returns ("", false) // if the SSO session is expired or already consumed. // // Security: the SSO session is consumed (single-use) and the authorization code // is stored server-side for exchange via POST /v1/sso/token. The state parameter // is carried through unchanged for the service to validate. func (u *UIServer) buildSSOCallback(r *http.Request, ssoNonce string, accountID int64) (string, bool) { sess, ok := sso.ConsumeSession(ssoNonce) if !ok { return "", false } code, err := sso.Store(sess.ClientID, sess.RedirectURI, sess.State, accountID) if err != nil { u.logger.Error("sso: store auth code", "error", err) return "", false } u.writeAudit(r, model.EventSSOLoginOK, &accountID, nil, audit.JSON("client_id", sess.ClientID)) return sess.RedirectURI + "?code=" + url.QueryEscape(code) + "&state=" + url.QueryEscape(sess.State), true }