package auth import ( "errors" "fmt" "github.com/gin-gonic/gin" "armature/store" ) // ErrNoCert is returned when no client certificate is presented on a // connection that requires mTLS authentication. var ErrNoCert = errors.New("no client certificate presented") // MTLSNativeConfig holds configuration for the native (non-proxy) mTLS provider. type MTLSNativeConfig struct { AutoActivate bool // auto-activate new users (default true) DefaultTeam string // team ID for auto-provisioned users (optional) } // MTLSNativeProvider authenticates by reading the peer certificate // directly from the TLS connection state. Unlike MTLSProxyProvider, // it does not trust headers — identity is cryptographically verified // by the Go TLS stack before the HTTP layer runs. // // Requires TLS_MODE=mtls so the binary terminates TLS itself. type MTLSNativeProvider struct { cfg MTLSNativeConfig } // NewMTLSNativeProvider creates a native mTLS auth provider. func NewMTLSNativeProvider(cfg MTLSNativeConfig) *MTLSNativeProvider { return &MTLSNativeProvider{cfg: cfg} } func (p *MTLSNativeProvider) Mode() Mode { return ModeMTLS } func (p *MTLSNativeProvider) SupportsRegistration() bool { return false } func (p *MTLSNativeProvider) Register(_ *gin.Context, _ store.Stores) (*Result, error) { return nil, ErrNotSupported } // Authenticate reads the verified peer certificate from the TLS connection // state. The CN becomes the username, and sha256(cert.Raw) is the stable // external_id. Returns ErrNoCert when no TLS or no peer certificates. func (p *MTLSNativeProvider) Authenticate(c *gin.Context, stores store.Stores) (*Result, error) { if c.Request.TLS == nil || len(c.Request.TLS.PeerCertificates) == 0 { return nil, fmt.Errorf("%w", ErrNoCert) } peer := c.Request.TLS.PeerCertificates[0] cn := peer.Subject.CommonName if cn == "" { return nil, fmt.Errorf("%w: certificate has no CommonName", ErrInvalidCreds) } // Build DN fields from the certificate subject for resolveOrProvision dnFields := map[string]string{"CN": cn} if len(peer.EmailAddresses) > 0 { dnFields["emailAddress"] = peer.EmailAddresses[0] } fingerprint := FingerprintCert(peer) return resolveOrProvision( c.Request.Context(), stores, cn, dnFields, fingerprint, p.cfg.AutoActivate, p.cfg.DefaultTeam, ) }