// Package oidc implements OIDC profile endpoints for KeyCape. // Only profile-supported features are advertised — no implicit flow, // no dynamic registration, no request objects. package oidc import ( "encoding/json" "net/http" ) // DiscoveryConfig holds the issuer and endpoint URLs for the discovery document. // UserinfoEndpoint is optional; if empty it is omitted from the document. type DiscoveryConfig struct { Issuer string // e.g. "https://auth.netkingdom.local" AuthorizationEndpoint string TokenEndpoint string JWKSUri string UserinfoEndpoint string // optional, empty = not advertised } // discoveryDocument is the JSON shape of /.well-known/openid-configuration. // Fields are ordered to match common OIDC implementations for readability. // registration_endpoint is intentionally absent — no dynamic client registration. type discoveryDocument struct { Issuer string `json:"issuer"` AuthorizationEndpoint string `json:"authorization_endpoint"` TokenEndpoint string `json:"token_endpoint"` JWKSUri string `json:"jwks_uri"` UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` ResponseTypesSupported []string `json:"response_types_supported"` GrantTypesSupported []string `json:"grant_types_supported"` CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` ScopesSupported []string `json:"scopes_supported"` TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` ClaimsSupported []string `json:"claims_supported"` SubjectTypesSupported []string `json:"subject_types_supported"` RequestParameterSupported bool `json:"request_parameter_supported"` ClaimsParameterSupported bool `json:"claims_parameter_supported"` } // discoveryHandler implements http.Handler for GET /.well-known/openid-configuration. type discoveryHandler struct { doc []byte } // NewDiscoveryHandler returns an http.Handler that serves the OIDC discovery document. // The document is pre-serialised at construction time so every request is a cheap copy. func NewDiscoveryHandler(cfg DiscoveryConfig) http.Handler { d := discoveryDocument{ Issuer: cfg.Issuer, AuthorizationEndpoint: cfg.AuthorizationEndpoint, TokenEndpoint: cfg.TokenEndpoint, JWKSUri: cfg.JWKSUri, UserinfoEndpoint: cfg.UserinfoEndpoint, // Profile-locked values — not negotiable. ResponseTypesSupported: []string{"code"}, GrantTypesSupported: []string{"authorization_code"}, CodeChallengeMethodsSupported: []string{"S256"}, IDTokenSigningAlgValuesSupported: []string{"RS256"}, ScopesSupported: []string{"openid", "profile", "email", "groups"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post", "none"}, ClaimsSupported: []string{ "sub", "iss", "aud", "exp", "iat", "preferred_username", "email", "name", "groups", "roles", }, SubjectTypesSupported: []string{"public"}, RequestParameterSupported: false, ClaimsParameterSupported: false, } b, err := json.Marshal(d) if err != nil { // This can only fail if the struct contains un-marshallable types, which it does not. panic("oidc: failed to marshal discovery document: " + err.Error()) } return &discoveryHandler{doc: b} } func (h *discoveryHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "max-age=3600") w.WriteHeader(http.StatusOK) _, _ = w.Write(h.doc) }