feat(auth): AD-Authenticator mit Zwei-Schritt-Bind, DC-Failover und verschachtelter Gruppenprüfung

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
Carsten Abele 2026-08-14 09:11:38 +02:00
parent 2eb69bdcfe
commit 99ee8758cc
8 changed files with 908 additions and 1 deletions

47
internal/auth/addata.go Normal file
View file

@ -0,0 +1,47 @@
package auth
import (
"regexp"
"strings"
)
// dataCodeRe findet den AD-spezifischen data-Code im Text eines
// LDAP-Result-Code-49-Fehlers, z. B. "... error, data 52e, v4563".
var dataCodeRe = regexp.MustCompile(`(?i)\bdata\s+([0-9a-f]{3,4})\b`)
// dataCodeReasons bildet AD-data-Codes auf Audit-Reasons ab.
// 525 (kein solcher User) wird bewusst wie ein falsches Passwort behandelt,
// damit die Fehlermeldung keine Benutzerexistenz verrät.
var dataCodeReasons = map[string]string{
"525": ReasonInvalidCredentials,
"52e": ReasonInvalidCredentials,
"530": ReasonInvalidCredentials, // Anmeldung außerhalb erlaubter Zeiten
"531": ReasonInvalidCredentials, // Anmeldung an dieser Station nicht erlaubt
"532": ReasonPasswordExpired,
"533": ReasonAccountDisabled,
"701": ReasonAccountDisabled, // Konto abgelaufen
"773": ReasonPasswordChangeRequired,
"775": ReasonAccountLocked,
}
// ReasonFromLDAPError leitet den Audit-Reason aus einem LDAP-Bind-Fehler ab.
// Fehler ohne erkennbaren data-Code gelten als Backend-Problem, nicht als
// falsches Passwort — sonst würde ein DC-Ausfall als Fehlanmeldung gezählt
// und den Benutzer über den Rate-Limiter aussperren.
func ReasonFromLDAPError(err error) string {
if err == nil {
return ""
}
msg := err.Error()
if m := dataCodeRe.FindStringSubmatch(msg); m != nil {
if reason, ok := dataCodeReasons[strings.ToLower(m[1])]; ok {
return reason
}
// data-Code vorhanden, aber unbekannt: es war eine echte Ablehnung.
return ReasonInvalidCredentials
}
if strings.Contains(msg, "Result Code 49") || strings.Contains(msg, "Invalid Credentials") {
return ReasonInvalidCredentials
}
return ReasonBackendUnavailable
}