package auth import ( "fmt" "github.com/gin-gonic/gin" "switchboard-core/store" ) // MTLSProxyConfig holds configuration for the proxy-terminated mTLS provider. type MTLSProxyConfig struct { HeaderDN string // header carrying cert DN (default "X-SSL-Client-DN") HeaderVerify string // header carrying verify status (default "X-SSL-Client-Verify") HeaderFingerprint string // header carrying cert fingerprint (default "X-SSL-Client-Fingerprint") AutoActivate bool // auto-activate new users (default true) DefaultTeam string // team ID for auto-provisioned users (optional) } // MTLSProxyProvider authenticates via client certificate headers injected // by the TLS-terminating reverse proxy (nginx, Traefik, Istio). // // The backend never sees the actual TLS handshake — it trusts headers // injected by the proxy after cert validation. After reading and parsing // the DN, the provider resolves an existing user or auto-provisions a // new one, then returns a Result. The auth handler issues an internal JWT. // // Header flow: // // Client cert → nginx ssl_verify_client → injects X-SSL-Client-DN, // X-SSL-Client-Verify, X-SSL-Client-Fingerprint → backend reads headers. type MTLSProxyProvider struct { cfg MTLSProxyConfig } func NewMTLSProxyProvider(cfg MTLSProxyConfig) *MTLSProxyProvider { if cfg.HeaderDN == "" { cfg.HeaderDN = "X-SSL-Client-DN" } if cfg.HeaderVerify == "" { cfg.HeaderVerify = "X-SSL-Client-Verify" } if cfg.HeaderFingerprint == "" { cfg.HeaderFingerprint = "X-SSL-Client-Fingerprint" } return &MTLSProxyProvider{cfg: cfg} } func (p *MTLSProxyProvider) Mode() Mode { return ModeMTLS } func (p *MTLSProxyProvider) SupportsRegistration() bool { return false } func (p *MTLSProxyProvider) Register(_ *gin.Context, _ store.Stores) (*Result, error) { return nil, ErrNotSupported } // Authenticate reads the cert DN and verify status from proxy headers, // parses the DN fields, and resolves or auto-provisions the user. // // Returns ErrInvalidCreds when headers are missing or verify fails. // Returns ErrInactive when user exists but is deactivated. func (p *MTLSProxyProvider) Authenticate(c *gin.Context, stores store.Stores) (*Result, error) { // ── Validate headers ─────────────────────────────────────────── verify := c.GetHeader(p.cfg.HeaderVerify) if verify == "" { return nil, fmt.Errorf("%w: missing %s header", ErrInvalidCreds, p.cfg.HeaderVerify) } // nginx: "SUCCESS", Traefik: "0" (both mean valid cert) if verify != "SUCCESS" && verify != "0" { return nil, fmt.Errorf("%w: cert verify=%s", ErrInvalidCreds, verify) } dn := c.GetHeader(p.cfg.HeaderDN) if dn == "" { return nil, fmt.Errorf("%w: missing %s header", ErrInvalidCreds, p.cfg.HeaderDN) } fields := ParseDN(dn) cn := fields["CN"] if cn == "" { return nil, fmt.Errorf("%w: cert DN has no CN field", ErrInvalidCreds) } // Stable external identity: fingerprint if available, else full DN fingerprint := c.GetHeader(p.cfg.HeaderFingerprint) if fingerprint == "" { fingerprint = dn } return resolveOrProvision( c.Request.Context(), stores, cn, fields, fingerprint, p.cfg.AutoActivate, p.cfg.DefaultTeam, ) }