checker-kerberos/checker/auth.go

93 lines
2.8 KiB
Go

package checker
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config"
)
// runAuthProbe performs a Login (AS-REQ + preauth) and, if a targetService
// is supplied, a TGS-REQ. kdcHosts maps reachable KDC hostnames to their
// TCP port so we can populate the krb5 config without doing DNS again.
func runAuthProbe(ctx context.Context, realm, principal, password, targetService string,
kdcHosts map[string]uint16, timeout time.Duration) *AuthProbeResult {
res := &AuthProbeResult{Attempted: true, Principal: principal, TargetService: targetService}
username := principal
if i := strings.LastIndex(principal, "@"); i >= 0 {
username = principal[:i]
}
cfg := config.New()
cfg.LibDefaults.DefaultRealm = realm
cfg.LibDefaults.NoAddresses = true
cfg.LibDefaults.TicketLifetime = 10 * time.Minute
cfg.LibDefaults.Clockskew = 5 * time.Minute
cfg.LibDefaults.UDPPreferenceLimit = 1 // force TCP
cfg.LibDefaults.DefaultTktEnctypeIDs = preferredEnctypes
cfg.LibDefaults.DefaultTGSEnctypeIDs = preferredEnctypes
cfg.LibDefaults.PermittedEnctypeIDs = preferredEnctypes
realmCfg := config.Realm{Realm: realm}
for host, port := range kdcHosts {
realmCfg.KDC = append(realmCfg.KDC, host+":"+strconv.Itoa(int(port)))
}
cfg.Realms = []config.Realm{realmCfg}
cfg.DomainRealm = config.DomainRealm{strings.ToLower(realm): realm}
start := time.Now()
cl := client.NewWithPassword(username, realm, password, cfg,
client.DisablePAFXFAST(true))
loginErr := cl.Login()
res.Latency = time.Since(start)
if loginErr != nil {
res.Error = loginErr.Error()
if code, ok := extractKRBErrorCode(loginErr); ok {
res.ErrorCode = code
res.ErrorName = errorcodeName(code)
}
return res
}
res.TGTAcquired = true
spn := targetService
if spn == "" {
// Self-test: request a fresh TGT (krbtgt/REALM@REALM).
spn = fmt.Sprintf("krbtgt/%s", realm)
}
if _, _, err := cl.GetServiceTicket(spn); err != nil {
res.Error = fmt.Sprintf("TGS failed for %s: %v", spn, err)
if code, ok := extractKRBErrorCode(err); ok {
res.ErrorCode = code
res.ErrorName = errorcodeName(code)
}
return res
}
res.TGSAcquired = true
return res
}
// extractKRBErrorCode tries to pull a Kerberos error code out of the
// wrapped errors returned by gokrb5. It's best-effort: if the code
// can't be determined, ok is false.
func extractKRBErrorCode(err error) (int32, bool) {
if err == nil {
return 0, false
}
// gokrb5 wraps KRBError values: their Error() string begins with "KRB Error: (N)".
msg := err.Error()
if _, after, ok := strings.Cut(msg, "KRB Error: ("); ok {
if code, _, ok := strings.Cut(after, ")"); ok {
if n, err := strconv.Atoi(code); err == nil {
return int32(n), true
}
}
}
return 0, false
}