Files
template-vitrine/backend/test/security/jwt_test.go
T
2026-09-14 20:50:19 +02:00

151 lines
5.1 KiB
Go

package security_test
import (
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"backend/internal/platform/security"
)
const testSecret = "test-secret-please-do-not-use-in-prod"
func TestIssueAndParseAccessToken_RoundTrip(t *testing.T) {
userID := uuid.New()
token, err := security.IssueAccessToken(testSecret, time.Minute, userID, "admin", security.AudienceAdmin)
if err != nil {
t.Fatalf("IssueAccessToken() error = %v", err)
}
claims, err := security.ParseAccessToken(testSecret, token, security.AudienceAdmin)
if err != nil {
t.Fatalf("ParseAccessToken() error = %v", err)
}
if claims.Subject != userID.String() {
t.Errorf("claims.Subject = %q, want %q", claims.Subject, userID.String())
}
if claims.Role != "admin" {
t.Errorf("claims.Role = %q, want %q", claims.Role, "admin")
}
if claims.ID == "" {
t.Error("claims.ID (jti) is empty, want a non-empty token id")
}
}
func TestIssueAccessToken_RejectsEmptyRole(t *testing.T) {
_, err := security.IssueAccessToken(testSecret, time.Minute, uuid.New(), "", security.AudienceAdmin)
if err == nil {
t.Fatal("IssueAccessToken() error = nil, want error for empty role")
}
}
func TestParseAccessToken_WrongAudienceRejected(t *testing.T) {
userID := uuid.New()
token, err := security.IssueAccessToken(testSecret, time.Minute, userID, "admin", security.AudienceAdmin)
if err != nil {
t.Fatalf("IssueAccessToken() error = %v", err)
}
// An admin-space token must never be accepted as a customer-space token,
// even though both spaces share the same signing secret.
if _, err := security.ParseAccessToken(testSecret, token, security.AudienceCustomer); err == nil {
t.Fatal("ParseAccessToken() error = nil, want error when audience does not match expected space")
}
}
func TestParseAccessToken_WrongSecretRejected(t *testing.T) {
token, err := security.IssueAccessToken(testSecret, time.Minute, uuid.New(), "admin", security.AudienceAdmin)
if err != nil {
t.Fatalf("IssueAccessToken() error = %v", err)
}
if _, err := security.ParseAccessToken("a-different-secret", token, security.AudienceAdmin); err == nil {
t.Fatal("ParseAccessToken() error = nil, want error for a token signed with a different secret")
}
}
func TestParseAccessToken_ExpiredTokenRejected(t *testing.T) {
token, err := security.IssueAccessToken(testSecret, -time.Minute, uuid.New(), "admin", security.AudienceAdmin)
if err != nil {
t.Fatalf("IssueAccessToken() error = %v", err)
}
if _, err := security.ParseAccessToken(testSecret, token, security.AudienceAdmin); err == nil {
t.Fatal("ParseAccessToken() error = nil, want error for an already-expired token")
}
}
func TestParseAccessToken_RejectsNoneAlgorithm(t *testing.T) {
// Craft a token that declares "alg":"none" and carries otherwise valid
// claims, to make sure the parser refuses it outright rather than
// trusting an attacker-chosen algorithm.
claims := security.Claims{
Role: "admin",
RegisteredClaims: jwt.RegisteredClaims{
Subject: uuid.New().String(),
Audience: jwt.ClaimStrings{string(security.AudienceAdmin)},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: uuid.NewString(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodNone, claims)
unsigned, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType)
if err != nil {
t.Fatalf("craft none-alg token: %v", err)
}
if _, err := security.ParseAccessToken(testSecret, unsigned, security.AudienceAdmin); err == nil {
t.Fatal("ParseAccessToken() error = nil, want error for an alg=none token")
}
}
func TestParseAccessToken_RejectsInvalidSubject(t *testing.T) {
secretBytes := []byte(testSecret)
claims := security.Claims{
Role: "admin",
RegisteredClaims: jwt.RegisteredClaims{
Subject: "not-a-uuid",
Audience: jwt.ClaimStrings{string(security.AudienceAdmin)},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: uuid.NewString(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(secretBytes)
if err != nil {
t.Fatalf("sign token: %v", err)
}
if _, err := security.ParseAccessToken(testSecret, signed, security.AudienceAdmin); err == nil {
t.Fatal("ParseAccessToken() error = nil, want error for a non-UUID sub claim")
}
}
func TestParseAccessToken_RejectsMissingRole(t *testing.T) {
secretBytes := []byte(testSecret)
claims := security.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: uuid.New().String(),
Audience: jwt.ClaimStrings{string(security.AudienceAdmin)},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: uuid.NewString(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(secretBytes)
if err != nil {
t.Fatalf("sign token: %v", err)
}
if _, err := security.ParseAccessToken(testSecret, signed, security.AudienceAdmin); err == nil {
t.Fatal("ParseAccessToken() error = nil, want error when role claim is missing")
}
}