feat: authentication with local accounts, sessions, CSRF, RBAC, and OIDC PKCE (phase 3)
- 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>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user