mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-28 18:15:30 +00:00
Guards against the v1.2.4 bug (fixed in c32acd5) where Authenticate
held a.mux via defer for its full duration, then called
loadAndVerifyToken which also tries to acquire a.mux — deadlock on
every token auth request.
TestAuthenticateTokenNoDeadlock exercises both the bare-token
("", token) and named-username ("api", token) forms with a 1s
timeout, so a regression fails fast rather than hanging the suite.
198 lines
6.2 KiB
Go
198 lines
6.2 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},
|
|
{"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)
|
|
}
|
|
})
|
|
}
|
|
}
|