golib/auth/csvauth/csvauth_test.go

200 lines
6.4 KiB
Go

package csvauth
import (
"encoding/base64"
"fmt"
"strings"
"testing"
"time"
)
// TestAuthenticateTokenNoDeadlock guards against the v1.2.4 regression where
// Authenticate held a.mux via defer and then called loadAndVerifyToken, which
// also tried to acquire a.mux — causing a deadlock on all token auth requests.
// Fixed in c32acd5 (ref(auth/csvauth): don't hold mutex longer than necessary).
func TestAuthenticateTokenNoDeadlock(t *testing.T) {
var key [16]byte
a := New(key[:])
const secret = "supersecrettoken"
c := a.NewCredential(PurposeToken, "ci-bot", secret, []string{"plain"}, []string{"deploy"}, "")
if err := a.CacheCredential(*c); err != nil {
t.Fatal(err)
}
type result struct {
p any
err error
}
// Authenticate("", token) — the token-as-password form
ch := make(chan result, 1)
go func() {
p, err := a.Authenticate("", secret)
ch <- result{p, err}
}()
select {
case r := <-ch:
if r.err != nil {
t.Fatalf("Authenticate(\"\", token): unexpected error: %v", r.err)
}
case <-time.After(time.Second):
t.Fatal("Authenticate deadlocked — mutex was not released before calling loadAndVerifyToken")
}
// Authenticate("api", token) — the named-token-username form
ch2 := make(chan result, 1)
go func() {
p, err := a.Authenticate("api", secret)
ch2 <- result{p, err}
}()
select {
case r := <-ch2:
if r.err != nil {
t.Fatalf("Authenticate(\"api\", token): unexpected error: %v", r.err)
}
case <-time.After(time.Second):
t.Fatal("Authenticate deadlocked — mutex was not released before calling loadAndVerifyToken")
}
}
func TestCredentialCreationAndVerification(t *testing.T) {
type testCase struct {
purpose string
name string
params []string
roles []string
extra string
isLogin bool
isRecoverable bool
}
tests := []testCase{
{"service1", "acme", []string{"aes-128-gcm"}, nil, "token1", false, true},
{"service2", "acme", []string{"plain"}, nil, "token2", false, true},
{"service3", "user3", []string{"pbkdf2", "1000", "16", "SHA-256"}, nil, "token3", false, false},
{"service4", "user4", []string{"bcrypt"}, []string{"audit", "triage"}, "token4", false, false},
// {"token", "api~vkdAIZ2O", []string{"aes-128-gcm"}, nil, "api1", true, true},
// {"token", "api~b5ZF2sRQ", []string{"aes-128-gcm"}, nil, "api2", true, true},
{"login", "user1", []string{"pbkdf2", "1000", "16", "SHA-256"}, nil, "pass1", true, false},
{"login", "user2", []string{"bcrypt"}, nil, "pass2", true, false},
{"login", "user3", []string{"aes-128-gcm"}, nil, "pass3", true, true},
{"login", "user4", []string{"plain"}, nil, "pass4", true, true},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%s/%s", tc.purpose, tc.name), func(t *testing.T) {
var key [16]byte
a := &Auth{
aes128key: key,
credentials: make(map[Name]Credential),
hashedCredentials: make(map[string]Credential),
serviceAccounts: make(map[Purpose]Credential),
}
secret := tc.extra
c := a.NewCredential(tc.purpose, tc.name, secret, tc.params, tc.roles, tc.extra)
if c == nil {
t.Fatal("NewCredential returned nil")
}
if tc.isLogin {
_ = a.CacheCredential(*c)
} else {
_ = a.CacheServiceAccount(*c)
}
record := c.ToRecord()
// Verify record format
if record[0] != tc.purpose {
t.Errorf("purpose mismatch: got %q want %q", record[0], tc.purpose)
}
if record[1] != tc.name {
t.Errorf("name mismatch: got %q want %q", record[1], tc.name)
}
if record[2] != strings.Join(tc.params, " ") {
t.Errorf("params mismatch: got %q want %q", record[2], strings.Join(tc.params, " "))
}
salt64 := record[3]
derived64 := record[4]
algo := tc.params[0]
switch algo {
case "plain":
if salt64 != "" {
t.Errorf("plain salt should be empty, got %q", salt64)
}
if derived64 != secret {
t.Errorf("plain derived mismatch: got %q want %q", derived64, secret)
}
case "aes-128-gcm":
saltb, err := base64.RawURLEncoding.DecodeString(salt64)
if err != nil || len(saltb) != 12 {
t.Errorf("gcm salt invalid: len %d err %v", len(saltb), err)
}
derivedb, err := base64.RawURLEncoding.DecodeString(derived64)
if err != nil {
t.Errorf("gcm derived %q invalid: err %v", derivedb, err)
}
case "pbkdf2":
saltb, err := base64.RawURLEncoding.DecodeString(salt64)
if err != nil || len(saltb) != 16 {
t.Errorf("pbkdf2 salt invalid: len %d err %v", len(saltb), err)
}
derivedb, err := base64.RawURLEncoding.DecodeString(derived64)
if err != nil || len(derivedb) != 16 {
t.Errorf("pbkdf2 derived invalid: len %d err %v", len(derivedb), err)
}
case "bcrypt":
if salt64 != "" {
t.Errorf("bcrypt salt should be empty, got %q", salt64)
}
if !strings.HasPrefix(derived64, "$2a$12$") {
t.Errorf("bcrypt derived invalid: got %q", derived64)
}
}
if len(tc.roles) > 0 && record[5] != strings.Join(tc.roles, " ") {
t.Errorf("roles mismatch: got %q want %q", record[5], strings.Join(tc.roles, " "))
}
if len(tc.extra) > 0 && record[6] != tc.extra {
t.Errorf("extra mismatch: got %q want %q", record[6], tc.extra)
}
// Verify functionality
var c2 Credential
var err error
if tc.isLogin {
if err := a.Verify(tc.name, secret); err != nil {
t.Errorf("Auth.Verify failed for %s %s with %s: %v", tc.purpose, tc.name, secret, err)
}
c2, err = a.LoadCredential(tc.name)
if err != nil {
t.Errorf("LoadCredential failed for %s %s: %v", tc.purpose, tc.name, err)
}
} else {
c2, err = a.LoadServiceAccount(tc.purpose)
if err != nil {
t.Errorf("LoadServiceAccount failed for %s %s: %v", tc.purpose, tc.name, err)
}
}
if tc.isRecoverable {
if c2.Secret() != secret {
t.Errorf("Secret mismatch: got %q want %q", c2.Secret(), secret)
}
} else {
if c2.Secret() != "" {
t.Errorf("Secret should be empty for hashed service account, got %q", c2.Secret())
}
}
if err := c2.Verify(tc.name, secret); err != nil {
t.Errorf("Auth.Verify failed for %s: %v", tc.name, err)
}
if err := c2.Verify(tc.name, ""); err == nil {
t.Errorf("Auth.Verify incorrectly passed an empty password for %s %s", tc.purpose, tc.name)
}
if err := c2.Verify(tc.name, "wrong"); err == nil {
t.Errorf("Auth.Verify incorrectly passed a wrong password for %s %s", tc.purpose, tc.name)
}
})
}
}