105 lines
2.9 KiB
Go
105 lines
2.9 KiB
Go
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, ""}, ".")
|
||
}
|