Security(Go): Login rate limit + WS origin allowlist + REST bearer auth
This commit is contained in:
@@ -14,19 +14,31 @@ func TestSHA256Vector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRoundTripAdminEmptySalt(t *testing.T) {
|
||||
// adminLoginResponse helps tests compute the right login response for an
|
||||
// admin account that now uses a real per-instance salt.
|
||||
func adminLoginResponse(t *testing.T, a *Authenticator, username, password, nonce string) string {
|
||||
t.Helper()
|
||||
salt, ok := a.GetSalt(username)
|
||||
if !ok {
|
||||
t.Fatalf("admin %s not registered", username)
|
||||
}
|
||||
return ComputeSHA256(HashPassword(password, salt) + nonce)
|
||||
}
|
||||
|
||||
func TestLoginRoundTripAdmin(t *testing.T) {
|
||||
a := New()
|
||||
a.AddAdminFromPlainPassword("admin", "hunter2")
|
||||
|
||||
salt, ok := a.GetSalt("admin")
|
||||
if !ok || salt != "" {
|
||||
t.Fatalf("admin salt: ok=%v salt=%q", ok, salt)
|
||||
if !ok {
|
||||
t.Fatal("admin should be found")
|
||||
}
|
||||
if len(salt) != 2*saltBytes {
|
||||
t.Fatalf("admin salt should be a real 16-hex value, got %q (len=%d)", salt, len(salt))
|
||||
}
|
||||
|
||||
// Simulate the browser: nonce = "abc123", response = SHA256(passwordHash + nonce)
|
||||
nonce := "abc123"
|
||||
passwordHash := ComputeSHA256("hunter2")
|
||||
response := ComputeSHA256(passwordHash + nonce)
|
||||
response := adminLoginResponse(t, a, "admin", "hunter2", nonce)
|
||||
|
||||
token, role, err := a.VerifyLogin("admin", response, nonce)
|
||||
if err != nil {
|
||||
@@ -71,6 +83,33 @@ func TestLoginRoundTripViewerWithSalt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSaltUnknownUserShape verifies the salt-probe mitigation: an
|
||||
// unknown user must get back a value that's shape-identical to a real
|
||||
// salt, so an attacker can't tell from /get_salt alone whether a
|
||||
// username exists.
|
||||
func TestGetSaltUnknownUserShape(t *testing.T) {
|
||||
a := New()
|
||||
a.AddAdminFromPlainPassword("admin", "pw")
|
||||
|
||||
fake, ok := a.GetSalt("nobody")
|
||||
if ok {
|
||||
t.Fatal("ok should be false for unknown user")
|
||||
}
|
||||
if len(fake) != 2*saltBytes {
|
||||
t.Fatalf("fake salt should be %d hex chars; got %q (len=%d)", 2*saltBytes, fake, len(fake))
|
||||
}
|
||||
// Determinism: repeated probes for the same username get the same fake.
|
||||
fake2, _ := a.GetSalt("nobody")
|
||||
if fake != fake2 {
|
||||
t.Fatalf("fake salt should be deterministic for repeated probes; got %q vs %q", fake, fake2)
|
||||
}
|
||||
// Different usernames get different fake salts.
|
||||
other, _ := a.GetSalt("ghost")
|
||||
if fake == other {
|
||||
t.Fatalf("fake salts should differ across usernames; both = %q", fake)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRejectsWrongResponse(t *testing.T) {
|
||||
a := New()
|
||||
a.AddAdminFromPlainPassword("admin", "x")
|
||||
@@ -89,7 +128,7 @@ func TestTokenExpiry(t *testing.T) {
|
||||
a.SetTokenExpire(50 * time.Millisecond)
|
||||
a.AddAdminFromPlainPassword("admin", "x")
|
||||
nonce, _ := NewNonce()
|
||||
response := ComputeSHA256(ComputeSHA256("x") + nonce)
|
||||
response := adminLoginResponse(t, a, "admin", "x", nonce)
|
||||
token, _, err := a.VerifyLogin("admin", response, nonce)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -107,10 +146,57 @@ func TestRevoke(t *testing.T) {
|
||||
a := New()
|
||||
a.AddAdminFromPlainPassword("admin", "x")
|
||||
nonce, _ := NewNonce()
|
||||
response := ComputeSHA256(ComputeSHA256("x") + nonce)
|
||||
response := adminLoginResponse(t, a, "admin", "x", nonce)
|
||||
token, _, _ := a.VerifyLogin("admin", response, nonce)
|
||||
a.RevokeToken(token)
|
||||
if _, err := a.ValidateToken(token); err == nil {
|
||||
t.Fatal("revoked token should not validate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterAllowsBurstThenBlocks(t *testing.T) {
|
||||
r := NewRateLimiter(3, time.Minute)
|
||||
for i := 0; i < 3; i++ {
|
||||
if !r.Allow("ip-a") {
|
||||
t.Fatalf("attempt %d should be allowed", i+1)
|
||||
}
|
||||
}
|
||||
if r.Allow("ip-a") {
|
||||
t.Fatal("4th attempt should be denied")
|
||||
}
|
||||
// Different key has independent budget.
|
||||
if !r.Allow("ip-b") {
|
||||
t.Fatal("different key should still be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterReset(t *testing.T) {
|
||||
r := NewRateLimiter(2, time.Minute)
|
||||
r.Allow("k")
|
||||
r.Allow("k")
|
||||
if r.Allow("k") {
|
||||
t.Fatal("3rd should be denied")
|
||||
}
|
||||
r.Reset("k")
|
||||
if !r.Allow("k") {
|
||||
t.Fatal("after Reset, should be allowed again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterDisabledWhenZeroLimit(t *testing.T) {
|
||||
r := NewRateLimiter(0, time.Minute)
|
||||
for i := 0; i < 100; i++ {
|
||||
if !r.Allow("k") {
|
||||
t.Fatalf("limit=0 should never deny, denied at i=%d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterNilSafe(t *testing.T) {
|
||||
var r *RateLimiter
|
||||
if !r.Allow("anything") {
|
||||
t.Fatal("nil limiter should allow")
|
||||
}
|
||||
r.Reset("anything")
|
||||
r.Sweep()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user