axe/internal/auth/jwt_test.go

105 lines
2.9 KiB
Go
Raw Normal View History

2026-10-18 19:07:25 +00:00
package auth
import (
"encoding/base64"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
const testSecret = "test-secret-at-least-32-characters-long"
func TestGenerateAndParseToken(t *testing.T) {
token, err := GenerateToken(testSecret, "user-1", "worker")
if err != nil {
t.Fatalf("generate: %v", err)
}
claims, err := ParseToken(testSecret, token)
if err != nil {
t.Fatalf("parse: %v", err)
}
if claims.UserID != "user-1" || claims.Role != "worker" {
t.Errorf("claims = %+v, want user_id=user-1 role=worker", claims)
}
}
func TestParseTokenRejects(t *testing.T) {
cases := []struct {
name string
token string
}{
{
name: "подписан другим секретом",
token: sign(t, "another-secret", jwt.SigningMethodHS256, time.Hour),
},
{
name: "другой алгоритм при том же секрете (HS512)",
token: sign(t, testSecret, jwt.SigningMethodHS512, time.Hour),
},
{
name: "просроченный",
token: sign(t, testSecret, jwt.SigningMethodHS256, -time.Hour),
},
{
name: "alg=none, подпись не подставлена",
token: forgeNone(t),
},
{
name: "мусор вместо токена",
token: "not-a-token",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if _, err := ParseToken(testSecret, tc.token); err == nil {
t.Error("token was accepted, want rejection")
}
})
}
}
func TestEmptySecretFailsFast(t *testing.T) {
if _, err := GenerateToken("", "user-1", "client"); err == nil {
t.Error("GenerateToken with empty secret: want error, got token")
}
if _, err := ParseToken("", "whatever"); err == nil {
t.Error("ParseToken with empty secret: want error, got claims")
}
}
// sign собирает токен вручную, чтобы протестировать сценарии,
// которые GenerateToken сам не порождает.
func sign(t *testing.T, secret string, method jwt.SigningMethod, ttl time.Duration) string {
t.Helper()
claims := Claims{
UserID: "user-1",
Role: "admin",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token, err := jwt.NewWithClaims(method, claims).SignedString([]byte(secret))
if err != nil {
t.Fatalf("sign: %v", err)
}
return token
}
// forgeNone подделывает токен с alg=none: раньше такой заголовок
// проходил, потому что ключевая функция не сверяла метод подписи.
func forgeNone(t *testing.T) string {
t.Helper()
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
payload := base64.RawURLEncoding.EncodeToString(
[]byte(`{"user_id":"hacker","role":"admin","exp":4102444800}`),
)
return strings.Join([]string{header, payload, ""}, ".")
}