This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/auth/mtls_helpers.go
Jeffrey Smith fb5284f667
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m37s
CI/CD / test-sqlite (push) Successful in 2m48s
CI/CD / build-and-deploy (push) Successful in 52s
Feat v0.6.7 native mtls (#42)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 18:36:12 +00:00

109 lines
3.1 KiB
Go

package auth
import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"fmt"
"log"
"strings"
"switchboard-core/models"
"switchboard-core/store"
)
// ParseDN parses an RFC 2253 / RFC 4514 distinguished name into key-value pairs.
//
// Examples:
//
// "CN=Jeff Smith,O=Acme Corp,OU=Engineering"
// "CN=Jane Doe,emailAddress=jane@acme.com,O=Acme Corp"
//
// Handles simple comma-separated key=value pairs. Does NOT handle
// escaped commas in values (\,) or multi-valued RDNs (+). Sufficient
// for typical X.509 client cert DNs.
func ParseDN(dn string) map[string]string {
result := make(map[string]string)
parts := strings.Split(dn, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
idx := strings.Index(part, "=")
if idx < 1 {
continue
}
key := strings.TrimSpace(part[:idx])
val := strings.TrimSpace(part[idx+1:])
result[key] = val
}
return result
}
// FingerprintCert returns the hex-encoded SHA-256 hash of a certificate's
// raw DER encoding. This is used as the stable external_id for mTLS users.
func FingerprintCert(cert *x509.Certificate) string {
h := sha256.Sum256(cert.Raw)
return hex.EncodeToString(h[:])
}
// resolveOrProvision looks up an existing user by auth_source=mtls and external_id,
// or auto-provisions a new user from the certificate's CN and fingerprint.
//
// Shared by both MTLSProxyProvider and MTLSNativeProvider.
func resolveOrProvision(
ctx context.Context,
stores store.Stores,
cn string,
dnFields map[string]string,
fingerprint string,
autoActivate bool,
defaultTeam string,
) (*Result, error) {
// ── Look up existing user ──────────────────────────────────────
user, err := stores.Users.GetByExternalID(ctx, string(ModeMTLS), fingerprint)
if err == nil && user != nil {
if !user.IsActive {
return nil, ErrInactive
}
log.Printf("[auth/mtls] existing user %s (%s)", user.Username, cn)
return &Result{User: user, IsNewUser: false, VaultHint: ""}, nil
}
// ── Auto-provision ─────────────────────────────────────────────
if !autoActivate {
return nil, fmt.Errorf("%w: auto-provision disabled", ErrInvalidCreds)
}
email := dnFields["emailAddress"]
if email == "" {
email = models.HandleFromName(cn) + "@mtls.local"
}
handle := UniqueHandle(ctx, stores.Users, models.HandleFromName(cn))
user = &models.User{
Username: handle,
Email: email,
DisplayName: cn,
IsActive: true,
AuthSource: string(ModeMTLS),
ExternalID: &fingerprint,
Handle: handle,
}
if err := stores.Users.Create(ctx, user); err != nil {
return nil, fmt.Errorf("auto-provision failed: %w", err)
}
log.Printf("[auth/mtls] auto-provisioned user %s from CN=%s", user.ID, cn)
EnsureEveryoneGroup(ctx, stores, user.ID)
if defaultTeam != "" {
if err := stores.Teams.AddMember(ctx, defaultTeam, user.ID, "member"); err != nil {
log.Printf("[auth/mtls] warn: could not add %s to default team: %v", user.ID, err)
}
}
return &Result{User: user, IsNewUser: true, VaultHint: ""}, nil
}