mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 23:57:59 +00:00
feat(auth/csvauth): add Authenticate(user, pass string) to get verified Credential
This commit is contained in:
parent
7d35551fa7
commit
01a4cdda8a
@ -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 <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
|
||||
// Authenticate verifies credentials - either Basic Auth style (user/pass) or Bearer Token style.
|
||||
//
|
||||
// In short:
|
||||
// - if <user>:<pass> and 'user' is found, then "login" (basic auth) credentials
|
||||
// - if <token>:"" or <allowed-token-name>:<token>, 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
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user