Finish KEY-WP-0008: registration handoff and client MFA isolation
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 34s

Add signed registration/enrollment handoffs, per-request assurance
policy with login-session isolation, and /logout. coulomb-social
stays AAL1 unless acr_values or another client raises the bar.
This commit is contained in:
tegwick 2026-08-16 01:05:27 +02:00
parent fff9e39478
commit b6af6c5268
22 changed files with 1636 additions and 42 deletions

View file

@ -0,0 +1,73 @@
package oidc
import (
"net/http"
"net/url"
"strings"
"keycape/internal/domain"
profileerrors "keycape/internal/errors"
)
// LogoutHandler implements GET /logout (OIDC RP-initiated logout subset).
// It clears the KeyCape login session and, when requested, redirects only to
// a statically registered client redirect URI.
type LogoutHandler struct {
ClientConfig map[string]*domain.Client
Logins *LoginSessionStore
SecureCookie bool
}
// ServeHTTP handles GET /logout.
func (h *LogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if sess := h.Logins.fromRequest(r); sess != nil {
h.Logins.Delete(sess.ID)
}
clearLoginCookie(w, h.SecureCookie)
clientID := r.URL.Query().Get("client_id")
postLogout := r.URL.Query().Get("post_logout_redirect_uri")
if postLogout == "" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("logged out"))
return
}
client, ok := h.ClientConfig[clientID]
if !ok {
profileerrors.InvalidProfileUsage("unknown client_id", "client_id").
Write(w, http.StatusBadRequest)
return
}
if !uriRegistered(client.RedirectURIs, postLogout) {
profileerrors.RejectedForSafety(
"post_logout_redirect_uri is not a registered redirect URI",
"post_logout_redirect_uri",
).Write(w, http.StatusBadRequest)
return
}
loc, err := url.Parse(postLogout)
if err != nil {
profileerrors.InvalidProfileUsage("invalid post_logout_redirect_uri", "post_logout_redirect_uri").
Write(w, http.StatusBadRequest)
return
}
if state := r.URL.Query().Get("state"); state != "" {
q := loc.Query()
q.Set("state", state)
loc.RawQuery = q.Encode()
}
http.Redirect(w, r, loc.String(), http.StatusFound)
}
func issuerIsHTTPS(issuer string) bool {
return strings.HasPrefix(strings.ToLower(issuer), "https://")
}