generated from coulomb/repo-seed
feat: implement T05, T08, T13 — OIDC discovery, JWKS, telemetry pipeline
- T05: /.well-known/openid-configuration — profile-only features advertised - T08: /jwks — RS256 JWK Set, stdlib crypto only, key rotation support - T13: Structured telemetry — Event types, LogEmitter/NoopEmitter/MultiEmitter, context helpers 38 server tests pass, go vet clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
86
src/internal/server/oidc/discovery.go
Normal file
86
src/internal/server/oidc/discovery.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user