Go backend that drives kernel WireGuard over netlink (wireguard-go as the fallback), nftables NAT with MSS clamping, forwarding and buffer sysctls, SQLite for peers, users, sessions, traffic history and the audit log. React console: dashboard with live rates and usage history, peer management with QR codes and .conf downloads, disconnect, session reset, key rotation, expiry, client-supplied keys, settings, users with admin and viewer roles, two-factor authentication with recovery codes, audit log. Docker image on Alpine with compose files for bridged and host networking, CI and GHCR publish workflows, performance notes.
107 lines
2.5 KiB
Go
107 lines
2.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPasswordRoundTrip(t *testing.T) {
|
|
h, err := HashPassword("correct horse battery staple")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !VerifyPassword(h, "correct horse battery staple") {
|
|
t.Fatal("right password rejected")
|
|
}
|
|
if VerifyPassword(h, "correct horse battery stapl") {
|
|
t.Fatal("wrong password accepted")
|
|
}
|
|
if VerifyPassword("garbage", "x") {
|
|
t.Fatal("garbage hash accepted")
|
|
}
|
|
}
|
|
|
|
func TestValidatePassword(t *testing.T) {
|
|
if err := ValidatePassword("short"); err == nil {
|
|
t.Fatal("short password accepted")
|
|
}
|
|
if err := ValidatePassword("twelve chars"); err != nil {
|
|
t.Fatalf("12-character password rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTOTP(t *testing.T) {
|
|
// RFC 6238 test vector: secret "12345678901234567890" (base32
|
|
// GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ), time 59 -> 287082 with SHA1.
|
|
secret := "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
|
code, err := TOTPNow(secret, time.Unix(59, 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if code != "287082" {
|
|
t.Fatalf("got %s, want 287082", code)
|
|
}
|
|
if !VerifyTOTP(secret, "287082", time.Unix(59, 0)) {
|
|
t.Fatal("valid code rejected")
|
|
}
|
|
// One step later still accepted (window of one either side).
|
|
if !VerifyTOTP(secret, "287082", time.Unix(59+30, 0)) {
|
|
t.Fatal("previous-step code rejected")
|
|
}
|
|
if VerifyTOTP(secret, "287082", time.Unix(59+120, 0)) {
|
|
t.Fatal("stale code accepted")
|
|
}
|
|
if VerifyTOTP(secret, "28708", time.Unix(59, 0)) {
|
|
t.Fatal("short code accepted")
|
|
}
|
|
}
|
|
|
|
func TestRecoveryCodes(t *testing.T) {
|
|
codes, hashes, err := NewRecoveryCodes(8)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(codes) != 8 || len(hashes) != 8 {
|
|
t.Fatal("wrong count")
|
|
}
|
|
if HashToken(NormaliseRecoveryCode(" "+codes[0]+" ")) != hashes[0] {
|
|
t.Fatal("normalised code does not hash to stored value")
|
|
}
|
|
if len(codes[0]) != 14 {
|
|
t.Fatalf("unexpected format %q", codes[0])
|
|
}
|
|
}
|
|
|
|
func TestLimiter(t *testing.T) {
|
|
l := NewLimiter(2, time.Minute)
|
|
if ok, _ := l.Allowed("a"); !ok {
|
|
t.Fatal("fresh key blocked")
|
|
}
|
|
l.Fail("a")
|
|
l.Fail("a")
|
|
if ok, wait := l.Allowed("a"); ok || wait <= 0 {
|
|
t.Fatal("key not blocked after max failures")
|
|
}
|
|
if ok, _ := l.Allowed("b"); !ok {
|
|
t.Fatal("unrelated key blocked")
|
|
}
|
|
l.Reset("a")
|
|
if ok, _ := l.Allowed("a"); !ok {
|
|
t.Fatal("reset key still blocked")
|
|
}
|
|
}
|
|
|
|
func TestTokens(t *testing.T) {
|
|
tok, hash, err := NewToken()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if HashToken(tok) != hash {
|
|
t.Fatal("hash mismatch")
|
|
}
|
|
id, err := NewID()
|
|
if err != nil || len(id) != 16 {
|
|
t.Fatalf("bad id %q %v", id, err)
|
|
}
|
|
}
|