ref(auth/csvauth): enable token use with Verify(dummy, token)

This commit is contained in:
AJ ONeal 2026-02-21 15:39:03 -07:00
parent 85d42550bf
commit 85c7b78ca6
No known key found for this signature in database

View File

@ -70,6 +70,7 @@ type Auth struct {
tokens map[string]Credential tokens map[string]Credential
serviceAccounts map[Purpose]Credential serviceAccounts map[Purpose]Credential
mux sync.Mutex mux sync.Mutex
BasicAuthTokenNames []string
} }
// New initializes an Auth with an encryption key // New initializes an Auth with an encryption key
@ -82,6 +83,7 @@ func New(aes128key []byte) *Auth {
credentials: map[Name]Credential{}, credentials: map[Name]Credential{},
tokens: map[string]Credential{}, tokens: map[string]Credential{},
serviceAccounts: map[Purpose]Credential{}, serviceAccounts: map[Purpose]Credential{},
BasicAuthTokenNames: []string{"", "api", "apikey"},
} }
} }
@ -307,6 +309,31 @@ func (a *Auth) CredentialKeys() iter.Seq[Name] {
return maps.Keys(a.credentials) return maps.Keys(a.credentials)
} }
func (a *Auth) LoadToken(secret string) (Credential, error) {
hashID := a.tokenCacheID(secret)
a.mux.Lock()
c, ok := a.tokens[hashID]
a.mux.Unlock()
if !ok {
return Credential{}, ErrNotFound
}
if c.plain == "" {
var err error
if c.plain, err = a.maybeDecryptCredential(c); err != nil {
return Credential{}, err
}
}
if err := c.Verify("", secret); err != nil {
return Credential{}, err
}
return c, nil
}
func (a *Auth) LoadCredential(name Name) (Credential, error) { func (a *Auth) LoadCredential(name Name) (Credential, error) {
a.mux.Lock() a.mux.Lock()
c, ok := a.credentials[name] c, ok := a.credentials[name]
@ -402,42 +429,50 @@ func (a *Auth) CacheServiceAccount(c Credential) error {
return nil return nil
} }
// Verify checks Basic Auth credentials // Verify checks Basic Auth credentials, i.e. as decoded from Authorization Basic <base64(user:pass)>.
// It also supports tokens. In short:
// - if <user>:<pass> and 'user' is found, then "login" credentials
// - if <token>:"" or <allowed-token-name>:<token>, then "token" credentials
//
// With a little more nuance and clarity:
// - if 'user' is found in the "login" credential store, token is NEVER tried
// - either 'user' or 'pass' may be used as the token
// (because 'pass' is swapped with 'user' when 'pass' is empty)
// - the resulting 'user' must match BasicAuthTokenNames ("", "api", and "apikey" are the defaults)
// - then the token is (timing-safe) hashed to check if it exists, and then verified by its algorithm
func (a *Auth) Verify(name, secret string) error { func (a *Auth) Verify(name, secret string) error {
a.mux.Lock() a.mux.Lock()
defer a.mux.Unlock() defer a.mux.Unlock()
c, ok := a.credentials[name] c, ok := a.credentials[name]
if !ok { if ok {
return ErrNotFound
}
return c.Verify(name, secret) return c.Verify(name, secret)
} }
func (a *Auth) VerifyToken(secret string) error { if secret == "" {
hashID := a.tokenCacheID(secret) secret, name = name, secret
}
if slices.Contains(a.BasicAuthTokenNames, name) {
// this still returns ErrNotFound first
return a.VerifyToken(secret)
}
a.mux.Lock()
c, ok := a.tokens[hashID]
a.mux.Unlock()
if !ok {
return ErrNotFound return ErrNotFound
} }
if c.plain == "" { // VerifyToken uses a short, but timing-safe hash to find the token,
var err error // and then verifies it with HMAC
if c.plain, err = a.maybeDecryptCredential(c); err != nil { func (a *Auth) VerifyToken(secret string) error {
_, err := a.LoadToken(secret)
return err return err
} }
}
return c.Verify(hashID, secret)
}
func (a *Auth) tokenCacheID(secret string) string { func (a *Auth) tokenCacheID(secret string) string {
key := a.aes128key[:] key := a.aes128key[:]
mac := hmac.New(sha256.New, key) mac := hmac.New(sha256.New, key)
message := []byte(secret) message := []byte(secret)
mac.Write(message) mac.Write(message)
// attack collisions are possible, but will still fail to pass HMAC
// practical collisions are not possible for the CSV use case
nameBytes := mac.Sum(nil)[:6] nameBytes := mac.Sum(nil)[:6]
name := base64.RawURLEncoding.EncodeToString(nameBytes) name := base64.RawURLEncoding.EncodeToString(nameBytes)