ref!(envauth): change Verify return from bool to error

This commit is contained in:
AJ ONeal 2025-10-04 21:18:41 -06:00
parent 0893b3cb2d
commit e8fbe603af
No known key found for this signature in database
3 changed files with 60 additions and 40 deletions

View File

@ -12,7 +12,7 @@ creds := envauth.BasicCredentials{
Password: os.Getenv("BASIC_AUTH_PASSWORD"), Password: os.Getenv("BASIC_AUTH_PASSWORD"),
} }
verified := creds.Verify("username", "password") err := creds.Verify("username", "password")
``` ```
## Basic Credentials: Username + Password ## Basic Credentials: Username + Password
@ -44,12 +44,17 @@ func main() {
Password: password, Password: password,
} }
verified := creds.Verify("api", "secret") if err := creds.Verify("api", "secret"); err != nil {
if verified { switch err {
println("Authentication successful") case envauth.ErrUnauthorized:
} else { println("Authentication failed")
println("Authentication failed") default:
panic(err)
}
os.Exit(1)
} }
println("Authentication successful")
} }
``` ```
@ -104,11 +109,16 @@ func main() {
Iterations: iterations, Iterations: iterations,
} }
verified := creds.Verify("api", "secret") if err := creds.Verify("api", "secret"); err != nil {
if verified { switch err {
println("Authentication successful") case envauth.ErrUnauthorized:
} else { println("Authentication failed")
println("Authentication failed") default:
panic(err)
}
os.Exit(1)
} }
println("Authentication successful")
} }
``` ```

View File

@ -4,10 +4,13 @@ import (
"crypto/pbkdf2" "crypto/pbkdf2"
"crypto/sha256" "crypto/sha256"
"crypto/subtle" "crypto/subtle"
"errors"
) )
var ErrUnauthorized = errors.New("unauthorized")
type BasicAuthVerifier interface { type BasicAuthVerifier interface {
Verify(string, string) bool Verify(string, string) error
} }
// BasicCredentials holds user credentials // BasicCredentials holds user credentials
@ -18,9 +21,9 @@ type BasicCredentials struct {
// Returns true if username and password match. // Returns true if username and password match.
// Uses SHA-256 and constant-time techniques to avoid revealing whether the username or password matches through timing attacks. // Uses SHA-256 and constant-time techniques to avoid revealing whether the username or password matches through timing attacks.
func (c BasicCredentials) Verify(username string, password string) bool { func (c BasicCredentials) Verify(username string, password string) error {
if len(password) == 0 { if len(password) == 0 {
return false return ErrUnauthorized
} }
equal := 1 equal := 1
@ -38,7 +41,10 @@ func (c BasicCredentials) Verify(username string, password string) bool {
v = subtle.ConstantTimeCompare(knownPasswordHash[:], passwordHash[:]) // 1 if same v = subtle.ConstantTimeCompare(knownPasswordHash[:], passwordHash[:]) // 1 if same
equal = subtle.ConstantTimeSelect(v, equal, 0) // v ? x : y equal = subtle.ConstantTimeSelect(v, equal, 0) // v ? x : y
return equal == 1 if equal == 1 {
return nil
}
return ErrUnauthorized
} }
// PBKDF2Credentials holds user credentials // PBKDF2Credentials holds user credentials
@ -51,24 +57,24 @@ type PBKDF2Credentials struct {
// Returns true if username and password match. // Returns true if username and password match.
// Uses PBKDF2 and constant-time techniques to avoid revealing whether the username or password matches through timing attacks. // Uses PBKDF2 and constant-time techniques to avoid revealing whether the username or password matches through timing attacks.
func (c PBKDF2Credentials) Verify(username string, password string) bool { func (c PBKDF2Credentials) Verify(username string, password string) error {
keyLen := len(c.DerivedKey) keyLen := len(c.DerivedKey)
dkKnownUser, err := pbkdf2.Key(sha256.New, c.Username, c.Salt, c.Iterations, keyLen) dkKnownUser, err := pbkdf2.Key(sha256.New, c.Username, c.Salt, c.Iterations, keyLen)
if err != nil { if err != nil {
return false return err
} }
if len(password) == 0 { if len(password) == 0 {
return false return ErrUnauthorized
} }
dkUser, err := pbkdf2.Key(sha256.New, username, c.Salt, c.Iterations, keyLen) dkUser, err := pbkdf2.Key(sha256.New, username, c.Salt, c.Iterations, keyLen)
if err != nil { if err != nil {
return false return err
} }
dkPass, err := pbkdf2.Key(sha256.New, password, c.Salt, c.Iterations, keyLen) dkPass, err := pbkdf2.Key(sha256.New, password, c.Salt, c.Iterations, keyLen)
if err != nil { if err != nil {
return false return err
} }
equal := 1 equal := 1
@ -79,7 +85,11 @@ func (c PBKDF2Credentials) Verify(username string, password string) bool {
v = subtle.ConstantTimeCompare(dkPass, c.DerivedKey) // 1 if same v = subtle.ConstantTimeCompare(dkPass, c.DerivedKey) // 1 if same
equal = subtle.ConstantTimeSelect(v, equal, 0) // v ? x : y equal = subtle.ConstantTimeSelect(v, equal, 0) // v ? x : y
return equal == 1 if equal == 1 {
return nil
}
return ErrUnauthorized
} }
func (c PBKDF2Credentials) DeriveKey(username string, password string, keyLen int) ([]byte, error) { func (c PBKDF2Credentials) DeriveKey(username string, password string, keyLen int) ([]byte, error) {

View File

@ -14,50 +14,50 @@ func TestBasicCredentials_Verify(t *testing.T) {
creds BasicCredentials creds BasicCredentials
username string username string
password string password string
want bool want error
}{ }{
{ {
name: "empty username, correct password", name: "empty username, correct password",
creds: BasicCredentials{Username: "", Password: "secret"}, creds: BasicCredentials{Username: "", Password: "secret"},
username: "", username: "",
password: "secret", password: "secret",
want: true, want: nil,
}, },
{ {
name: "correct username, correct password", name: "correct username, correct password",
creds: BasicCredentials{Username: "user", Password: "secret"}, creds: BasicCredentials{Username: "user", Password: "secret"},
username: "user", username: "user",
password: "secret", password: "secret",
want: true, want: nil,
}, },
{ {
name: "incorrect username, correct password", name: "incorrect username, correct password",
creds: BasicCredentials{Username: "user", Password: "secret"}, creds: BasicCredentials{Username: "user", Password: "secret"},
username: "wrong", username: "wrong",
password: "secret", password: "secret",
want: false, want: ErrUnauthorized,
}, },
{ {
name: "correct username, incorrect password", name: "correct username, incorrect password",
creds: BasicCredentials{Username: "user", Password: "secret"}, creds: BasicCredentials{Username: "user", Password: "secret"},
username: "user", username: "user",
password: "wrong", password: "wrong",
want: false, want: ErrUnauthorized,
}, },
{ {
name: "correct username, empty password", name: "correct username, empty password",
creds: BasicCredentials{Username: "user", Password: "secret"}, creds: BasicCredentials{Username: "user", Password: "secret"},
username: "user", username: "user",
password: "", password: "",
want: false, want: ErrUnauthorized,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := tt.creds.Verify(tt.username, tt.password) err := tt.creds.Verify(tt.username, tt.password)
if got != tt.want { if err != tt.want {
t.Errorf("Verify(%q, %q) = %v; want %v", tt.username, tt.password, got, tt.want) t.Errorf("Verify(%q, %q) = %v; want %v", tt.username, tt.password, err, tt.want)
} }
}) })
} }
@ -78,57 +78,57 @@ func TestPBKDF2Credentials_Verify(t *testing.T) {
creds PBKDF2Credentials creds PBKDF2Credentials
username string username string
password string password string
want bool want error
}{ }{
{ {
name: "empty username, correct password", name: "empty username, correct password",
creds: PBKDF2Credentials{Username: "", DerivedKey: secretDigest, Salt: salt, Iterations: 1000}, creds: PBKDF2Credentials{Username: "", DerivedKey: secretDigest, Salt: salt, Iterations: 1000},
username: "", username: "",
password: "secret", password: "secret",
want: true, want: nil,
}, },
{ {
name: "correct username, correct password", name: "correct username, correct password",
creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000}, creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000},
username: "user", username: "user",
password: "secret", password: "secret",
want: true, want: nil,
}, },
{ {
name: "incorrect username, correct password", name: "incorrect username, correct password",
creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000}, creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000},
username: "wrong", username: "wrong",
password: "secret", password: "secret",
want: false, want: ErrUnauthorized,
}, },
{ {
name: "correct username, incorrect password", name: "correct username, incorrect password",
creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000}, creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000},
username: "user", username: "user",
password: "wrong", password: "wrong",
want: false, want: ErrUnauthorized,
}, },
{ {
name: "correct username, empty password", name: "correct username, empty password",
creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000}, creds: PBKDF2Credentials{Username: "user", DerivedKey: secretDigest, Salt: salt, Iterations: 1000},
username: "user", username: "user",
password: "", password: "",
want: false, want: ErrUnauthorized,
}, },
{ {
name: "empty username, empty pre-computed digest", name: "empty username, empty pre-computed digest",
creds: PBKDF2Credentials{Username: "", DerivedKey: emptyDigest, Salt: salt, Iterations: 1000}, creds: PBKDF2Credentials{Username: "", DerivedKey: emptyDigest, Salt: salt, Iterations: 1000},
username: "", username: "",
password: "", password: "",
want: false, want: ErrUnauthorized,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := tt.creds.Verify(tt.username, tt.password) err := tt.creds.Verify(tt.username, tt.password)
if got != tt.want { if err != tt.want {
t.Errorf("Verify(%q, %q) = %v; want %v", tt.username, tt.password, got, tt.want) t.Errorf("Verify(%q, %q) = %v; want %v", tt.username, tt.password, err, tt.want)
} }
}) })
} }