1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
package admin
import (
"os"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
const testSecret = "test-secret-for-unit-tests-only"
func makeToken(role string, exp time.Time) string {
claims := jwt.MapClaims{
"issuer": "api-gateway",
"username": "admin",
"role": role,
"exp": exp.Unix(),
}
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
s, _ := tok.SignedString([]byte(testSecret))
return s
}
func TestMain(m *testing.M) {
os.Setenv("JWTSECRET", testSecret)
os.Exit(m.Run())
}
// βββ validateToken ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
func TestValidateToken_Valid(t *testing.T) {
tok := makeToken("admin", time.Now().Add(1*time.Hour))
if !validateToken(tok) {
t.Error("valid admin token should pass")
}
}
func TestValidateToken_WrongRole(t *testing.T) {
tok := makeToken("viewer", time.Now().Add(1*time.Hour))
if validateToken(tok) {
t.Error("non-admin role should be rejected")
}
}
func TestValidateToken_Expired(t *testing.T) {
tok := makeToken("admin", time.Now().Add(-1*time.Second))
if validateToken(tok) {
t.Error("expired token should be rejected")
}
}
func TestValidateToken_EmptyString(t *testing.T) {
if validateToken("") {
t.Error("empty string should be rejected")
}
}
func TestValidateToken_Garbage(t *testing.T) {
if validateToken("not.a.jwt") {
t.Error("garbage token should be rejected")
}
}
func TestValidateToken_WrongSecret(t *testing.T) {
claims := jwt.MapClaims{
"role": "admin",
"exp": time.Now().Add(1 * time.Hour).Unix(),
}
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
s, _ := tok.SignedString([]byte("wrong-secret"))
if validateToken(s) {
t.Error("token signed with wrong secret should be rejected")
}
}
func TestValidateToken_AlgNoneAttack(t *testing.T) {
// Classic JWT "alg=none" attack: craft a token with no signature.
// A vulnerable validator would accept this; ours must reject it.
header := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0" // {"alg":"none","typ":"JWT"}
payload := "eyJyb2xlIjoiYWRtaW4iLCJleHAiOjk5OTk5OTk5OTl9" // {"role":"admin","exp":9999999999}
noneToken := header + "." + payload + "."
if validateToken(noneToken) {
t.Error("alg=none token must be rejected")
}
}
func TestValidateToken_TamperedSignature(t *testing.T) {
tok := makeToken("admin", time.Now().Add(1*time.Hour))
// Replace the last character with a different one to ensure the signature is actually changed.
last := tok[len(tok)-1]
replacement := byte('X')
if last == 'X' {
replacement = 'Y'
}
tampered := tok[:len(tok)-1] + string(replacement)
if validateToken(tampered) {
t.Error("token with tampered signature must be rejected")
}
}
// βββ Login rate limiter βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
func resetLoginAttempts() {
loginMu.Lock()
defer loginMu.Unlock()
loginAttempts = make(map[string]*loginCounter)
}
func TestLoginRateLimit_AllowsUnderLimit(t *testing.T) {
resetLoginAttempts()
ip := "1.2.3.4"
for i := 0; i < maxLoginAttempts; i++ {
if !checkLoginRateLimit(ip) {
t.Fatalf("attempt %d should be allowed (limit=%d)", i+1, maxLoginAttempts)
}
}
}
func TestLoginRateLimit_BlocksAfterLimit(t *testing.T) {
resetLoginAttempts()
ip := "1.2.3.5"
for i := 0; i < maxLoginAttempts; i++ {
checkLoginRateLimit(ip)
}
if checkLoginRateLimit(ip) {
t.Errorf("attempt %d should be blocked (limit=%d)", maxLoginAttempts+1, maxLoginAttempts)
}
}
func TestLoginRateLimit_DifferentIPsIndependent(t *testing.T) {
resetLoginAttempts()
ipA := "10.0.0.1"
ipB := "10.0.0.2"
// Exhaust ipA
for i := 0; i < maxLoginAttempts+1; i++ {
checkLoginRateLimit(ipA)
}
// ipB should still be allowed
if !checkLoginRateLimit(ipB) {
t.Error("rate limit on one IP should not affect other IPs")
}
}
func TestLoginRateLimit_ResetsAfterWindow(t *testing.T) {
resetLoginAttempts()
ip := "10.0.0.9"
// Exhaust the limit
for i := 0; i < maxLoginAttempts+2; i++ {
checkLoginRateLimit(ip)
}
// Manually backdate the reset time so the window has passed
loginMu.Lock()
if entry := loginAttempts[ip]; entry != nil {
entry.resetAt = time.Now().Add(-1 * time.Second)
}
loginMu.Unlock()
if !checkLoginRateLimit(ip) {
t.Error("should be allowed again after window expires")
}
}
|