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://") }