c1cc781279
- argon2id (t=3, m=64MiB, p=2) PHC hashing honoring embedded params - token-bucket rate limiting (10/15min per IP+email) on login/register - opaque 32B session tokens in Mongo, 30-day sliding expiry, logout-all - CSRF double-submit cookie/header on authenticated mutations - bootstrap admin from env with forced first-login password change - requireAuth/requireRole middleware with disabled-account enforcement - OIDC code flow with PKCE: lazy discovery, account linking only on verified email, auto-created developer accounts - unit tests (RBAC matrix, CSRF, password, rate limiter) + integration suite covering the full auth matrix incl. an in-test fake IdP Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
76 lines
1.9 KiB
Go
76 lines
1.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
func TestHashVerifyRoundTrip(t *testing.T) {
|
|
hash, err := HashPassword("correct horse battery staple")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(hash, "$argon2id$v=19$m=65536,t=3,p=2$") {
|
|
t.Fatalf("hash %q does not embed the spec parameters", hash)
|
|
}
|
|
|
|
ok, err := VerifyPassword("correct horse battery staple", hash)
|
|
if err != nil || !ok {
|
|
t.Fatalf("correct password: ok=%v err=%v", ok, err)
|
|
}
|
|
ok, err = VerifyPassword("wrong password", hash)
|
|
if err != nil || ok {
|
|
t.Fatalf("wrong password: ok=%v err=%v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestHashesAreSalted(t *testing.T) {
|
|
a, _ := HashPassword("same")
|
|
b, _ := HashPassword("same")
|
|
if a == b {
|
|
t.Fatal("two hashes of the same password must differ")
|
|
}
|
|
}
|
|
|
|
func TestVerifyRejectsGarbage(t *testing.T) {
|
|
for _, bad := range []string{
|
|
"",
|
|
"plaintext",
|
|
"$bcrypt$whatever",
|
|
"$argon2id$v=19$m=65536,t=3,p=2$!!!$AAA",
|
|
"$argon2id$v=18$m=65536,t=3,p=2$c2FsdA$AAA",
|
|
} {
|
|
if ok, err := VerifyPassword("x", bad); ok || err == nil {
|
|
t.Errorf("VerifyPassword(%q): ok=%v err=%v, want rejection", bad, ok, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVerifyHonorsEmbeddedParams(t *testing.T) {
|
|
// A hash created with different (weaker) params must still verify:
|
|
// params come from the hash string, not the current constants.
|
|
salt := []byte("saltsaltsaltsalt")
|
|
key := argon2.IDKey([]byte("pw"), salt, 1, 16*1024, 1, 32)
|
|
legacy := fmt.Sprintf("$argon2id$v=19$m=16384,t=1,p=1$%s$%s",
|
|
base64.RawStdEncoding.EncodeToString(salt),
|
|
base64.RawStdEncoding.EncodeToString(key))
|
|
ok, err := VerifyPassword("pw", legacy)
|
|
if err != nil || !ok {
|
|
t.Fatalf("legacy-param hash: ok=%v err=%v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestNewTokenShape(t *testing.T) {
|
|
a, b := NewToken(), NewToken()
|
|
if a == b {
|
|
t.Fatal("tokens must be unique")
|
|
}
|
|
if len(a) != 43 { // 32 bytes base64url, unpadded
|
|
t.Fatalf("token length = %d, want 43", len(a))
|
|
}
|
|
}
|