Files
etalon c1cc781279 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>
2026-06-12 18:31:25 +02:00

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))
}
}