This commit is contained in:
CFOU
2026-09-14 20:50:19 +02:00
commit 3091fb4020
135 changed files with 10262 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
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")
}
}
+71
View File
@@ -0,0 +1,71 @@
package security_test
import (
"testing"
"backend/internal/platform/security"
)
func TestHashAndVerifyPassword_RoundTrip(t *testing.T) {
hash, err := security.HashPassword("correct-horse-battery-staple")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
ok, err := security.VerifyPassword(hash, "correct-horse-battery-staple")
if err != nil {
t.Fatalf("VerifyPassword() error = %v", err)
}
if !ok {
t.Fatal("VerifyPassword() = false, want true for the correct password")
}
}
func TestVerifyPassword_WrongPassword(t *testing.T) {
hash, err := security.HashPassword("correct-horse-battery-staple")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
ok, err := security.VerifyPassword(hash, "wrong-password")
if err != nil {
t.Fatalf("VerifyPassword() error = %v", err)
}
if ok {
t.Fatal("VerifyPassword() = true, want false for a wrong password")
}
}
func TestHashPassword_UniqueSaltPerCall(t *testing.T) {
hash1, err := security.HashPassword("same-password")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
hash2, err := security.HashPassword("same-password")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
if hash1 == hash2 {
t.Fatal("two hashes of the same password with random salts must differ")
}
}
func TestVerifyPassword_InvalidFormat(t *testing.T) {
_, err := security.VerifyPassword("not-a-valid-hash", "whatever")
if err == nil {
t.Fatal("VerifyPassword() error = nil, want error for malformed hash")
}
}
func TestVerifyPassword_TamperedHash(t *testing.T) {
hash, err := security.HashPassword("correct-horse-battery-staple")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
tampered := hash[:len(hash)-4] + "abcd"
ok, _ := security.VerifyPassword(tampered, "correct-horse-battery-staple")
if ok {
t.Fatal("VerifyPassword() = true for a tampered hash, want false")
}
}