diff --git a/auth/csvauth/csvauth.go b/auth/csvauth/csvauth.go index de0add1..1bb9986 100644 --- a/auth/csvauth/csvauth.go +++ b/auth/csvauth/csvauth.go @@ -9,6 +9,7 @@ import ( "crypto/sha1" "crypto/sha256" "encoding/csv" + "encoding/hex" "errors" "fmt" "hash" @@ -23,6 +24,7 @@ import ( ) var ErrNotFound = errors.New("not found") +var ErrKeySize = errors.New("invalid key size") var ErrUnauthorized = errors.New("unauthorized") var ErrUnknownAlgorithm = errors.New("unknown algorithm") var ErrLockedCredential = errors.New("credential is locked") @@ -83,6 +85,18 @@ func New(aes128key []byte) *Auth { } } +// MustNewFromHex parses a hex string and uses the first 16 bytes to construct a key +func MustNewFromHex(aes128key string) *Auth { + bytes, err := hex.DecodeString(aes128key) + if err != nil { + panic(err) + } + if len(bytes) < 16 { + panic(fmt.Errorf("%w: %d < 16", ErrKeySize, len(bytes))) + } + return New(bytes) +} + // Load reads a credentials CSV from the given NamedReadCloser (e.g. file, wrapped http request) func (a *Auth) LoadCSV(f NamedReadCloser, comma rune) error { csvr := csv.NewReader(f) @@ -331,10 +345,11 @@ func (a *Auth) gcmDecrypt(aes128key [16]byte, gcmNonce [12]byte, derived []byte) return string(plaintext), nil } -// Verify checks Basic Auth credentials, i.e. as decoded from Authorization Basic . -// It also supports tokens. In short: -// - if : and 'user' is found, then "login" credentials -// - if :"" or :, then "token" credentials +// Authenticate verifies credentials - either Basic Auth style (user/pass) or Bearer Token style. +// +// In short: +// - if : and 'user' is found, then "login" (basic auth) credentials +// - if :"" or :, then "token" (bearer) credentials // // With a little more nuance and clarity: // - if 'user' is found in the "login" credential store, token is NEVER tried @@ -342,12 +357,15 @@ func (a *Auth) gcmDecrypt(aes128key [16]byte, gcmNonce [12]byte, derived []byte) // (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) Authenticate(name, secret string) (*Credential, error) { a.mux.Lock() defer a.mux.Unlock() c, ok := a.credentials[name] if ok { - return c.Verify(name, secret) + if err := c.Verify(name, secret); err != nil { + return nil, err + } + return &c, nil } if secret == "" { @@ -355,10 +373,16 @@ func (a *Auth) Verify(name, secret string) error { } if slices.Contains(a.BasicAuthTokenNames, name) { // this still returns ErrNotFound first - return a.VerifyToken(secret) + return a.loadAndVerifyToken(secret) } - return ErrNotFound + return nil, ErrNotFound +} + +// Same as Login, but without returning the credential +func (a *Auth) Verify(name, secret string) error { + _, err := a.Authenticate(name, secret) + return err } // Verify checks Basic Auth credentials diff --git a/auth/csvauth/token.go b/auth/csvauth/token.go index 421addc..4eb0802 100644 --- a/auth/csvauth/token.go +++ b/auth/csvauth/token.go @@ -6,7 +6,24 @@ import ( "encoding/base64" ) +// Provided for consistency. Often better to use Authenticate("", token) func (a *Auth) LoadToken(secret string) (Credential, error) { + var credential Credential + c, err := a.loadAndVerifyToken(secret) + if c != nil { + credential = *c + } + return credential, err +} + +// VerifyToken uses a shortened, but timing-safe HMAC to find the token, +// and then verifies it according to the chosen algorithm +func (a *Auth) VerifyToken(secret string) error { + _, err := a.loadAndVerifyToken(secret) + return err +} + +func (a *Auth) loadAndVerifyToken(secret string) (*Credential, error) { hashID := a.tokenCacheID(secret) a.mux.Lock() @@ -14,28 +31,21 @@ func (a *Auth) LoadToken(secret string) (Credential, error) { a.mux.Unlock() if !ok { - return Credential{}, ErrNotFound + return nil, ErrNotFound } if c.plain == "" { var err error if c.plain, err = a.maybeDecryptCredential(c); err != nil { - return Credential{}, err + return nil, err } } if err := c.Verify("", secret); err != nil { - return Credential{}, err + return nil, err } - return c, nil -} - -// VerifyToken uses a short, but timing-safe hash to find the token, -// and then verifies it with HMAC -func (a *Auth) VerifyToken(secret string) error { - _, err := a.LoadToken(secret) - return err + return &c, nil } func (a *Auth) tokenCacheID(secret string) string {