fixing Keycloak JwtService entfernt
This commit is contained in:
@@ -255,8 +255,6 @@ services:
|
||||
KEYCLOAK_REALM: meldestelle
|
||||
KEYCLOAK_CLIENT_ID: api-gateway
|
||||
KEYCLOAK_CLIENT_SECRET: K5RqonwVOaxPKaXVH4mbthSRbjRh5tOK
|
||||
# Custom JWT filter disabled - using oauth2ResourceServer instead
|
||||
GATEWAY_SECURITY_KEYCLOAK_ENABLED: "false"
|
||||
ports:
|
||||
- "${GATEWAY_PORT:-8081}:8081"
|
||||
depends_on:
|
||||
|
||||
@@ -63,7 +63,7 @@ resilience4j = "2.3.0"
|
||||
# --- Utilities ---
|
||||
#uuid = "0.9.0"
|
||||
bignum = "0.3.10"
|
||||
logback = "1.5.18"
|
||||
logback = "1.5.19"
|
||||
caffeine = "3.2.2"
|
||||
reactorKafka = "1.3.23"
|
||||
jackson = "2.19.2"
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Dieses Modul enthält die clientseitige Logik für die Authentifizierung.
|
||||
// Es stellt Konfigurationen und Beans bereit, um mit einem OAuth2/OIDC-Provider
|
||||
// wie Keycloak zu interagieren und JWTs zu validieren.
|
||||
plugins {
|
||||
`java-library`
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(21))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
// Stellt gemeinsame Abhängigkeiten wie Coroutines und Logging bereit.
|
||||
implementation(projects.platform.platformDependencies)
|
||||
// Stellt Domänenobjekte und technische Utilities bereit.
|
||||
implementation(projects.core.coreUtils)
|
||||
// Spring Security für OAuth2-Client-Funktionalität und JWT-Verarbeitung.
|
||||
implementation(libs.spring.boot.starter.oauth2.client)
|
||||
implementation(libs.spring.boot.starter.security)
|
||||
implementation(libs.spring.security.oauth2.jose)
|
||||
// Bibliothek zur einfachen Handhabung von JWTs.
|
||||
implementation(libs.auth0.java.jwt)
|
||||
// JSON-Serialization für konsistente API-Datenverarbeitung.
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service für Benutzerauthentifizierung und Passwortverwaltung.
|
||||
*/
|
||||
interface AuthenticationService {
|
||||
/**
|
||||
* Authentifiziert einen Benutzer mit Benutzernamen und Passwort.
|
||||
*
|
||||
* @param username Der Benutzername
|
||||
* @param password Das Passwort
|
||||
* @return Das Authentifizierungsergebnis
|
||||
*/
|
||||
suspend fun authenticate(username: String, password: String): AuthResult
|
||||
|
||||
/**
|
||||
* Ändert das Passwort eines Benutzers.
|
||||
*
|
||||
* @param userId Die Benutzer-ID
|
||||
* @param currentPassword Das aktuelle Passwort
|
||||
* @param newPassword Das neue Passwort
|
||||
* @return Das Ergebnis der Passwortänderung
|
||||
*/
|
||||
suspend fun changePassword(userId: Uuid?, currentPassword: String, newPassword: String): PasswordChangeResult
|
||||
|
||||
/**
|
||||
* Mögliche Ergebnisse eines Authentifizierungsversuchs.
|
||||
*/
|
||||
sealed class AuthResult {
|
||||
/**
|
||||
* Authentifizierung war erfolgreich.
|
||||
*
|
||||
* @param token Das JWT-Token
|
||||
* @param user Der authentifizierte Benutzer
|
||||
*/
|
||||
data class Success(val token: String, val user: AuthenticatedUser) : AuthResult()
|
||||
|
||||
/**
|
||||
* Authentication failed.
|
||||
*
|
||||
* @param reason The reason for the failure
|
||||
*/
|
||||
data class Failure(val reason: String) : AuthResult()
|
||||
|
||||
/**
|
||||
* The account is locked.
|
||||
*
|
||||
* @param lockedUntil The time until which the account is locked
|
||||
*/
|
||||
data class Locked(val lockedUntil: LocalDateTime) : AuthResult()
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible results of a password change attempt.
|
||||
*/
|
||||
sealed class PasswordChangeResult {
|
||||
/**
|
||||
* The password change was successful.
|
||||
*/
|
||||
data object Success : PasswordChangeResult()
|
||||
|
||||
/**
|
||||
* Password change failed.
|
||||
*
|
||||
* @param reason The reason for the failure
|
||||
*/
|
||||
data class Failure(val reason: String) : PasswordChangeResult()
|
||||
|
||||
/**
|
||||
* The new password is too weak.
|
||||
*/
|
||||
data object WeakPassword : PasswordChangeResult()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an authenticated user.
|
||||
*/
|
||||
data class AuthenticatedUser(
|
||||
val userId: Uuid?,
|
||||
val personId: Uuid?,
|
||||
val username: String,
|
||||
val email: String,
|
||||
val permissions: List<BerechtigungE>
|
||||
)
|
||||
}
|
||||
-203
@@ -1,203 +0,0 @@
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Service for JWT token generation and validation.
|
||||
*/
|
||||
class JwtService(
|
||||
private val secret: String,
|
||||
private val issuer: String,
|
||||
private val audience: String,
|
||||
private val expiration: Duration = 60.minutes
|
||||
) {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
init {
|
||||
require(secret.length >= 32) { "JWT secret must be at least 32 characters for HMAC512" }
|
||||
require(issuer.isNotBlank()) { "JWT issuer must not be blank" }
|
||||
require(audience.isNotBlank()) { "JWT audience must not be blank" }
|
||||
}
|
||||
|
||||
private val algorithm = Algorithm.HMAC512(secret)
|
||||
private val verifier = JWT.require(algorithm)
|
||||
.withIssuer(issuer)
|
||||
.withAudience(audience)
|
||||
.build()
|
||||
|
||||
fun generateToken(
|
||||
userId: String,
|
||||
username: String,
|
||||
permissions: List<BerechtigungE>
|
||||
): String {
|
||||
return JWT.create()
|
||||
.withSubject(userId)
|
||||
.withIssuer(issuer)
|
||||
.withAudience(audience)
|
||||
.withClaim("username", username)
|
||||
.withArrayClaim("permissions", permissions.map { it.name }.toTypedArray())
|
||||
.withExpiresAt(Date(System.currentTimeMillis() + expiration.inWholeMilliseconds))
|
||||
.sign(algorithm)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a JWT token.
|
||||
*
|
||||
* @param token The JWT token to validate
|
||||
* @return Result with true if the token is valid, or failure with error details
|
||||
*/
|
||||
fun validateToken(token: String): Result<Boolean> {
|
||||
return try {
|
||||
// Strict pre-check to ensure the exact Base64URL signature matches before decoding.
|
||||
// This defends against edge cases where Base64URL decoders may ignore insignificant bits
|
||||
// in the last character, which could allow certain tamperings to slip through.
|
||||
if (!hasValidSignature(token)) {
|
||||
throw JWTVerificationException("Invalid token signature")
|
||||
}
|
||||
|
||||
// Library verifier performs cryptographic verification and claim checks (issuer, audience, exp, ...)
|
||||
verifier.verify(token)
|
||||
Result.success(true)
|
||||
} catch (e: JWTVerificationException) {
|
||||
// Keep logging minimal to avoid timing variations under high frequency invalid inputs
|
||||
logger.debug { "JWT token validation failed" }
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
logger.debug { "Unexpected error during JWT token validation" }
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a JWT token (legacy method for backward compatibility).
|
||||
*
|
||||
* @param token The JWT token to validate
|
||||
* @return True if the token is valid, false otherwise
|
||||
*/
|
||||
@Deprecated("Use validateToken(token: String): Result<Boolean> instead", ReplaceWith("validateToken(token).isSuccess"))
|
||||
fun isValidToken(token: String): Boolean {
|
||||
return validateToken(token).isSuccess
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user ID from a JWT token.
|
||||
*
|
||||
* @param token The JWT token
|
||||
* @return Result with the user ID, or failure with error details
|
||||
*/
|
||||
fun getUserIdFromToken(token: String): Result<String> {
|
||||
return try {
|
||||
val subject = verifier.verify(token).subject
|
||||
if (subject.isNullOrBlank()) {
|
||||
logger.warn { "JWT token has no subject (user ID)" }
|
||||
Result.failure(IllegalStateException("JWT token has no subject"))
|
||||
} else {
|
||||
logger.debug { "Successfully extracted user ID from JWT token" }
|
||||
Result.success(subject)
|
||||
}
|
||||
} catch (e: JWTVerificationException) {
|
||||
logger.warn { "Failed to extract user ID from JWT token: ${e.message}" }
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Unexpected error while extracting user ID from JWT token" }
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user ID from a JWT token (legacy method for backward compatibility).
|
||||
*
|
||||
* @param token The JWT token
|
||||
* @return The user ID, or null if the token is invalid
|
||||
*/
|
||||
@Deprecated("Use getUserIdFromToken(token: String): Result<String> instead", ReplaceWith("getUserIdFromToken(token).getOrNull()"))
|
||||
fun getUserId(token: String): String? {
|
||||
return getUserIdFromToken(token).getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the permissions from a JWT token.
|
||||
*
|
||||
* @param token The JWT token
|
||||
* @return Result with the permissions, or failure with error details
|
||||
*/
|
||||
fun getPermissionsFromToken(token: String): Result<List<BerechtigungE>> {
|
||||
return try {
|
||||
val decodedJWT = verifier.verify(token)
|
||||
val permissionStrings = decodedJWT.getClaim("permissions").asArray(String::class.java)
|
||||
val permissions = permissionStrings?.mapNotNull { permissionString ->
|
||||
try {
|
||||
BerechtigungE.valueOf(permissionString)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
logger.warn { "Unknown permission in JWT token: $permissionString" }
|
||||
null
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
logger.debug { "Successfully extracted ${permissions.size} permissions from JWT token" }
|
||||
Result.success(permissions)
|
||||
} catch (e: JWTVerificationException) {
|
||||
logger.warn { "Failed to extract permissions from JWT token: ${e.message}" }
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Unexpected error while extracting permissions from JWT token" }
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the permissions from a JWT token (legacy method for backward compatibility).
|
||||
*
|
||||
* @param token The JWT token
|
||||
* @return The permissions, or an empty list if the token is invalid
|
||||
*/
|
||||
@Deprecated("Use getPermissionsFromToken(token: String): Result<List<BerechtigungE>> instead", ReplaceWith("getPermissionsFromToken(token).getOrElse { emptyList() }"))
|
||||
fun getPermissions(token: String): List<BerechtigungE> {
|
||||
return getPermissionsFromToken(token).getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
// ====== Internal helpers for strict signature validation ======
|
||||
private fun hasValidSignature(token: String): Boolean {
|
||||
return try {
|
||||
val parts = token.split('.')
|
||||
if (parts.size != 3) return false
|
||||
val header = parts[0]
|
||||
val payload = parts[1]
|
||||
val signature = parts[2]
|
||||
if (header.isBlank() || payload.isBlank() || signature.isBlank()) return false
|
||||
|
||||
val mac = Mac.getInstance("HmacSHA512")
|
||||
mac.init(SecretKeySpec(secret.toByteArray(StandardCharsets.UTF_8), "HmacSHA512"))
|
||||
val signingInput = "$header.$payload".toByteArray(StandardCharsets.UTF_8)
|
||||
val expected = mac.doFinal(signingInput)
|
||||
val expectedB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(expected)
|
||||
|
||||
constantTimeEquals(expectedB64, signature)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun constantTimeEquals(a: String, b: String): Boolean {
|
||||
val aBytes = a.toByteArray(StandardCharsets.UTF_8)
|
||||
val bBytes = b.toByteArray(StandardCharsets.UTF_8)
|
||||
var diff = aBytes.size xor bBytes.size
|
||||
val minLen = if (aBytes.size < bBytes.size) aBytes.size else bBytes.size
|
||||
var i = 0
|
||||
while (i < minLen) {
|
||||
diff = diff or (aBytes[i].toInt() xor bBytes[i].toInt())
|
||||
i++
|
||||
}
|
||||
return diff == 0
|
||||
}
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package at.mocode.infrastructure.auth.client.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* User role enumeration for member management
|
||||
*/
|
||||
@Serializable
|
||||
enum class RolleE {
|
||||
ADMIN, // System administrator
|
||||
VEREINS_ADMIN, // Club administrator
|
||||
FUNKTIONAER, // Official/functionary
|
||||
REITER, // Rider
|
||||
TRAINER, // Trainer
|
||||
RICHTER, // Judge
|
||||
TIERARZT, // Veterinarian
|
||||
ZUSCHAUER, // Spectator
|
||||
GAST // Guest
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission enumeration for access control
|
||||
*/
|
||||
@Serializable
|
||||
enum class BerechtigungE {
|
||||
// Person management
|
||||
PERSON_READ,
|
||||
PERSON_CREATE,
|
||||
PERSON_UPDATE,
|
||||
PERSON_DELETE,
|
||||
|
||||
// Club management
|
||||
VEREIN_READ,
|
||||
VEREIN_CREATE,
|
||||
VEREIN_UPDATE,
|
||||
VEREIN_DELETE,
|
||||
|
||||
// Event management
|
||||
VERANSTALTUNG_READ,
|
||||
VERANSTALTUNG_CREATE,
|
||||
VERANSTALTUNG_UPDATE,
|
||||
VERANSTALTUNG_DELETE,
|
||||
|
||||
// Horse management
|
||||
PFERD_READ,
|
||||
PFERD_CREATE,
|
||||
PFERD_UPDATE,
|
||||
PFERD_DELETE
|
||||
}
|
||||
-399
@@ -1,399 +0,0 @@
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Tag
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertTimeoutPreemptively
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Performance tests for authentication operations.
|
||||
* These tests ensure that JWT operations meet performance requirements under various load conditions.
|
||||
*/
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class AuthPerformanceTest {
|
||||
|
||||
private lateinit var jwtService: JwtService
|
||||
private val testSecret = "a-very-long-and-secure-test-secret-that-is-at-least-512-bits-long-for-hmac512"
|
||||
private val testIssuer = "test-issuer"
|
||||
private val testAudience = "test-audience"
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jwtService = JwtService(
|
||||
secret = testSecret,
|
||||
issuer = testIssuer,
|
||||
audience = testAudience,
|
||||
expiration = 60.minutes
|
||||
)
|
||||
}
|
||||
|
||||
// ========== JWT Validation Performance Tests ==========
|
||||
|
||||
@Test
|
||||
fun `JWT validation should complete under 50ms`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
|
||||
|
||||
// Act & Assert - Single validation should be reasonably fast
|
||||
repeat(100) {
|
||||
val timeMs = measureTimeMillis {
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
assertTrue(timeMs < 50, "JWT validation should complete under 50ms (took ${timeMs}ms)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JWT validation should handle burst load efficiently`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
|
||||
val iterations = 10000
|
||||
|
||||
// Act
|
||||
val timeMs = measureTimeMillis {
|
||||
repeat(iterations) {
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert - 10,000 validations should complete within reasonable time
|
||||
val avgTimeMs = timeMs.toDouble() / iterations
|
||||
assertTrue(timeMs < 5000, "10,000 validations should complete within 5 seconds (took ${timeMs}ms)")
|
||||
assertTrue(avgTimeMs < 0.5, "Average validation time should be under 0.5ms (was ${avgTimeMs}ms)")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Test too flaky - JVM warmup and system load cause high variance making it unsuitable for CI")
|
||||
fun `JWT validation performance should be consistent`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
|
||||
val measurements = mutableListOf<Long>()
|
||||
|
||||
// Act - Measure multiple batches
|
||||
repeat(10) {
|
||||
val batchTime = measureTimeMillis {
|
||||
repeat(1000) {
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
}
|
||||
measurements.add(batchTime)
|
||||
}
|
||||
|
||||
// Assert - Performance should be consistent across batches
|
||||
val avgTime = measurements.average()
|
||||
val maxDeviation = measurements.maxOf { kotlin.math.abs(it - avgTime) }
|
||||
assertTrue(maxDeviation < avgTime * 2.5,
|
||||
"Performance should be consistent (max deviation: ${maxDeviation}ms, avg: ${avgTime}ms, tolerance: 250%)")
|
||||
}
|
||||
|
||||
// ========== Token Generation Performance Tests ==========
|
||||
|
||||
@Tag("perf")
|
||||
@Test
|
||||
fun `token generation should complete under 5ms`() {
|
||||
// Arrange
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE, BerechtigungE.VEREIN_UPDATE)
|
||||
|
||||
// Warmup to stabilize JIT/caches
|
||||
repeat(3) {
|
||||
val t = jwtService.generateToken("warm-$it", "warmuser$it", permissions)
|
||||
assertTrue(t.isNotEmpty())
|
||||
}
|
||||
|
||||
// Act & Assert
|
||||
repeat(100) {
|
||||
val timeMs = measureTimeMillis {
|
||||
val token = jwtService.generateToken("user-$it", "testuser$it", permissions)
|
||||
assertNotNull(token)
|
||||
assertTrue(token.isNotEmpty())
|
||||
}
|
||||
assertTrue(timeMs < 80, "Token generation should complete under 80ms (took ${timeMs}ms)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `token generation should handle high throughput`() {
|
||||
// Arrange
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.VEREIN_READ)
|
||||
val iterations = 5000
|
||||
|
||||
// Act
|
||||
val timeMs = measureTimeMillis {
|
||||
repeat(iterations) {
|
||||
val token = jwtService.generateToken("user-$it", "testuser$it", permissions)
|
||||
assertTrue(token.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
// Assert - Should generate 5000 tokens within a reasonable time
|
||||
val tokensPerSecond = (iterations * 1000.0) / timeMs
|
||||
assertTrue(tokensPerSecond > 1000,
|
||||
"Should generate at least 1000 tokens/second (achieved ${tokensPerSecond.toInt()}/second)")
|
||||
}
|
||||
|
||||
// ========== Concurrent Access Performance Tests ==========
|
||||
|
||||
@Test
|
||||
fun `token generation should handle concurrent requests`() {
|
||||
// Arrange
|
||||
val threadCount = 10
|
||||
val operationsPerThread = 500
|
||||
val executor = Executors.newFixedThreadPool(threadCount)
|
||||
val latch = CountDownLatch(threadCount)
|
||||
val results = mutableListOf<Boolean>()
|
||||
val errors = mutableListOf<Exception>()
|
||||
|
||||
// Act
|
||||
val totalTime = measureTimeMillis {
|
||||
repeat(threadCount) { threadIndex ->
|
||||
executor.submit {
|
||||
try {
|
||||
repeat(operationsPerThread) { opIndex ->
|
||||
val token = jwtService.generateToken(
|
||||
"user-$threadIndex-$opIndex",
|
||||
"testuser$threadIndex",
|
||||
listOf(BerechtigungE.PERSON_READ)
|
||||
)
|
||||
val isValid = jwtService.validateToken(token).isSuccess
|
||||
synchronized(results) {
|
||||
results.add(isValid)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
synchronized(errors) {
|
||||
errors.add(e)
|
||||
}
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(latch.await(30, TimeUnit.SECONDS), "All threads should complete within 30 seconds")
|
||||
}
|
||||
|
||||
executor.shutdown()
|
||||
|
||||
// Assert
|
||||
assertTrue(errors.isEmpty(), "No errors should occur during concurrent operations: ${errors.firstOrNull()}")
|
||||
assertEquals(threadCount * operationsPerThread, results.size)
|
||||
assertTrue(results.all { it }, "All tokens should be valid")
|
||||
|
||||
val operationsPerSecond = (threadCount * operationsPerThread * 1000.0) / totalTime
|
||||
assertTrue(operationsPerSecond > 500,
|
||||
"Should handle at least 500 operations/second under concurrent load (achieved ${operationsPerSecond.toInt()}/second)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `token validation should handle concurrent requests`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
|
||||
val threadCount = 20
|
||||
val validationsPerThread = 1000
|
||||
val executor = Executors.newFixedThreadPool(threadCount)
|
||||
val latch = CountDownLatch(threadCount)
|
||||
val results = mutableListOf<Boolean>()
|
||||
|
||||
// Act
|
||||
val totalTime = measureTimeMillis {
|
||||
repeat(threadCount) {
|
||||
executor.submit {
|
||||
repeat(validationsPerThread) {
|
||||
val isValid = jwtService.validateToken(token).isSuccess
|
||||
synchronized(results) {
|
||||
results.add(isValid)
|
||||
}
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS), "All validations should complete within 10 seconds")
|
||||
}
|
||||
|
||||
executor.shutdown()
|
||||
|
||||
// Assert
|
||||
assertEquals(threadCount * validationsPerThread, results.size)
|
||||
assertTrue(results.all { it }, "All validations should succeed")
|
||||
|
||||
val validationsPerSecond = (threadCount * validationsPerThread * 1000.0) / totalTime
|
||||
assertTrue(validationsPerSecond > 10000,
|
||||
"Should handle at least 10,000 validations/second under concurrent load (achieved ${validationsPerSecond.toInt()}/second)")
|
||||
}
|
||||
|
||||
// ========== Memory Usage Performance Tests ==========
|
||||
|
||||
@Test
|
||||
fun `memory usage should be stable under load`() {
|
||||
// Arrange
|
||||
val runtime = Runtime.getRuntime()
|
||||
val initialMemory = runtime.totalMemory() - runtime.freeMemory()
|
||||
|
||||
// Act - Perform many operations to test for memory leaks
|
||||
repeat(10000) {
|
||||
val token = jwtService.generateToken("user-$it", "testuser$it", listOf(BerechtigungE.PERSON_READ))
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
|
||||
// Extract data to ensure full processing
|
||||
jwtService.getUserIdFromToken(token)
|
||||
jwtService.getPermissionsFromToken(token)
|
||||
}
|
||||
|
||||
// Force garbage collection
|
||||
System.gc()
|
||||
Thread.sleep(100) // Give GC time to run
|
||||
|
||||
val finalMemory = runtime.totalMemory() - runtime.freeMemory()
|
||||
val memoryIncrease = finalMemory - initialMemory
|
||||
|
||||
// Assert - Memory increase should be reasonable (less than 50MB)
|
||||
assertTrue(memoryIncrease < 50 * 1024 * 1024,
|
||||
"Memory increase should be less than 50MB (was ${memoryIncrease / 1024 / 1024}MB)")
|
||||
}
|
||||
|
||||
// ========== Complex Permissions Performance Tests ==========
|
||||
|
||||
@Tag("perf")
|
||||
@Test
|
||||
fun `should handle large permission sets efficiently`() {
|
||||
// Arrange - Create a token with all available permissions
|
||||
val allPermissions = BerechtigungE.entries
|
||||
|
||||
// Warmup - allow JIT and caches to stabilize
|
||||
repeat(3) {
|
||||
val warmToken = jwtService.generateToken("admin-user", "admin", allPermissions)
|
||||
jwtService.validateToken(warmToken)
|
||||
jwtService.getPermissionsFromToken(warmToken)
|
||||
}
|
||||
|
||||
// Act & Assert - Generation should still be fast
|
||||
val generationTime = measureTimeMillis {
|
||||
val token = jwtService.generateToken("admin-user", "admin", allPermissions)
|
||||
assertNotNull(token)
|
||||
}
|
||||
assertTrue(generationTime < 500, "Generation with all permissions should be under 500ms (was ${generationTime}ms)")
|
||||
|
||||
// Validation should also be fast
|
||||
val token = jwtService.generateToken("admin-user", "admin", allPermissions)
|
||||
val validationTime = measureTimeMillis {
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
|
||||
val permissions = jwtService.getPermissionsFromToken(token).getOrElse { emptyList() }
|
||||
assertEquals(allPermissions.size, permissions.size)
|
||||
}
|
||||
assertTrue(validationTime < 120, "Validation with all permissions should be under 120ms (was ${validationTime}ms)")
|
||||
}
|
||||
|
||||
// ========== Stress Tests ==========
|
||||
|
||||
@Test
|
||||
fun `should handle sustained load without degradation`() {
|
||||
// Arrange
|
||||
val testDurationMs = 5000L // 5 seconds
|
||||
val startTime = System.currentTimeMillis()
|
||||
var operationCount = 0
|
||||
val measurementPoints = mutableListOf<Pair<Long, Int>>() // time, operations per second
|
||||
|
||||
// Act - Sustained load test
|
||||
while (System.currentTimeMillis() - startTime < testDurationMs) {
|
||||
val intervalStart = System.currentTimeMillis()
|
||||
var intervalOperations = 0
|
||||
|
||||
// Run operations for 1-second intervals
|
||||
while (System.currentTimeMillis() - intervalStart < 1000) {
|
||||
val token = jwtService.generateToken("user-$operationCount", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
val isValid = jwtService.validateToken(token).isSuccess
|
||||
assertTrue(isValid)
|
||||
operationCount++
|
||||
intervalOperations++
|
||||
}
|
||||
|
||||
measurementPoints.add(Pair(System.currentTimeMillis() - startTime, intervalOperations))
|
||||
}
|
||||
|
||||
// Assert - Performance should not degrade significantly over time
|
||||
assertTrue(measurementPoints.size >= 4, "Should have at least 4 measurement points")
|
||||
|
||||
val firstHalf = measurementPoints.take(measurementPoints.size / 2).map { it.second }
|
||||
val secondHalf = measurementPoints.drop(measurementPoints.size / 2).map { it.second }
|
||||
|
||||
val firstHalfAvg = firstHalf.average()
|
||||
val secondHalfAvg = secondHalf.average()
|
||||
|
||||
// Performance in the second half should not be significantly worse than the first half
|
||||
assertTrue(secondHalfAvg > firstHalfAvg * 0.8,
|
||||
"Performance should not degrade by more than 20% over time " +
|
||||
"(first half: ${firstHalfAvg.toInt()} ops/sec, second half: ${secondHalfAvg.toInt()} ops/sec)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `operations should complete within timeout under extreme load`() {
|
||||
// Arrange - Very high-load scenario
|
||||
val operations = 50000
|
||||
|
||||
// Act & Assert - Should complete within a reasonable timeout
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(30)) {
|
||||
repeat(operations) {
|
||||
val token = jwtService.generateToken("user-$it", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Benchmarking Tests ==========
|
||||
|
||||
@Test
|
||||
fun `benchmark basic JWT operations`() {
|
||||
// This test provides baseline performance metrics for monitoring
|
||||
val iterations = 1000
|
||||
|
||||
// Token Generation Benchmark
|
||||
val generationTime = measureTimeMillis {
|
||||
repeat(iterations) {
|
||||
jwtService.generateToken("user-$it", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
}
|
||||
}
|
||||
val avgGenerationMs = generationTime.toDouble() / iterations
|
||||
println("[DEBUG_LOG] Token generation: ${avgGenerationMs}ms average (${iterations} iterations)")
|
||||
|
||||
// Token Validation Benchmark
|
||||
val token = jwtService.generateToken("benchmark-user", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
val validationTime = measureTimeMillis {
|
||||
repeat(iterations) {
|
||||
jwtService.validateToken(token)
|
||||
}
|
||||
}
|
||||
val avgValidationMs = validationTime.toDouble() / iterations
|
||||
println("[DEBUG_LOG] Token validation: ${avgValidationMs}ms average (${iterations} iterations)")
|
||||
|
||||
// Data Extraction Benchmark
|
||||
val extractionTime = measureTimeMillis {
|
||||
repeat(iterations) {
|
||||
jwtService.getUserIdFromToken(token)
|
||||
jwtService.getPermissionsFromToken(token)
|
||||
}
|
||||
}
|
||||
val avgExtractionMs = extractionTime.toDouble() / iterations
|
||||
println("[DEBUG_LOG] Data extraction: ${avgExtractionMs}ms average (${iterations} iterations)")
|
||||
|
||||
// Performance should meet baseline requirements
|
||||
assertTrue(avgGenerationMs < 2.0, "Token generation should average under 2ms")
|
||||
assertTrue(avgValidationMs < 1.0, "Token validation should average under 1ms")
|
||||
assertTrue(avgExtractionMs < 1.0, "Data extraction should average under 1ms")
|
||||
}
|
||||
}
|
||||
-346
@@ -1,346 +0,0 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) // <-- HINZUGEFÜGT: Für die neue Kotlin UUID API
|
||||
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
// import com.benasher44.uuid.uuid4 // <-- ENTFERNT: Alter Import
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Tests for the AuthenticationService interface using mocks.
|
||||
* These tests verify the contract and behavior expectations without requiring real implementations.
|
||||
*/
|
||||
class AuthenticationServiceTest {
|
||||
|
||||
private lateinit var authService: AuthenticationService
|
||||
private val testUserId = Uuid.random() // <-- GEÄNDERT: von uuid4() zu Uuid.random()
|
||||
private val testPersonId = Uuid.random() // <-- GEÄNDERT: von uuid4() zu Uuid.random()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
authService = mockk<AuthenticationService>()
|
||||
}
|
||||
|
||||
// ========== Authentication Tests ==========
|
||||
|
||||
@Test
|
||||
fun `authenticate should return Success for valid credentials`() = runTest {
|
||||
// Arrange
|
||||
val username = "testuser"
|
||||
val password = "validpassword"
|
||||
val expectedUser = AuthenticationService.AuthenticatedUser(
|
||||
userId = testUserId,
|
||||
personId = testPersonId,
|
||||
username = username,
|
||||
email = "test@example.com",
|
||||
permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.VEREIN_READ)
|
||||
)
|
||||
val expectedToken = "valid.jwt.token"
|
||||
|
||||
coEvery { authService.authenticate(username, password) } returns
|
||||
AuthenticationService.AuthResult.Success(expectedToken, expectedUser)
|
||||
|
||||
// Act
|
||||
val result = authService.authenticate(username, password)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.AuthResult.Success)
|
||||
val successResult = result as AuthenticationService.AuthResult.Success
|
||||
assertEquals(expectedToken, successResult.token)
|
||||
assertEquals(expectedUser.userId, successResult.user.userId)
|
||||
assertEquals(expectedUser.username, successResult.user.username)
|
||||
assertEquals(expectedUser.email, successResult.user.email)
|
||||
assertEquals(2, successResult.user.permissions.size)
|
||||
assertTrue(successResult.user.permissions.contains(BerechtigungE.PERSON_READ))
|
||||
assertTrue(successResult.user.permissions.contains(BerechtigungE.VEREIN_READ))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate should return Failure for invalid credentials`() = runTest {
|
||||
// Arrange
|
||||
val username = "testuser"
|
||||
val password = "wrongpassword"
|
||||
val expectedReason = "Invalid username or password"
|
||||
|
||||
coEvery { authService.authenticate(username, password) } returns
|
||||
AuthenticationService.AuthResult.Failure(expectedReason)
|
||||
|
||||
// Act
|
||||
val result = authService.authenticate(username, password)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.AuthResult.Failure)
|
||||
val failureResult = result as AuthenticationService.AuthResult.Failure
|
||||
assertEquals(expectedReason, failureResult.reason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate should return Locked for locked accounts`() = runTest {
|
||||
// Arrange
|
||||
val username = "lockeduser"
|
||||
val password = "password"
|
||||
val lockedUntil = LocalDateTime.now().plusHours(1)
|
||||
|
||||
coEvery { authService.authenticate(username, password) } returns
|
||||
AuthenticationService.AuthResult.Locked(lockedUntil)
|
||||
|
||||
// Act
|
||||
val result = authService.authenticate(username, password)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.AuthResult.Locked)
|
||||
val lockedResult = result as AuthenticationService.AuthResult.Locked
|
||||
assertEquals(lockedUntil, lockedResult.lockedUntil)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate should handle empty username gracefully`() = runTest {
|
||||
// Arrange
|
||||
val emptyUsername = ""
|
||||
val password = "password"
|
||||
|
||||
coEvery { authService.authenticate(emptyUsername, password) } returns
|
||||
AuthenticationService.AuthResult.Failure("Username cannot be empty")
|
||||
|
||||
// Act
|
||||
val result = authService.authenticate(emptyUsername, password)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.AuthResult.Failure)
|
||||
val failureResult = result as AuthenticationService.AuthResult.Failure
|
||||
assertTrue(failureResult.reason.contains("Username"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate should handle empty password gracefully`() = runTest {
|
||||
// Arrange
|
||||
val username = "testuser"
|
||||
val emptyPassword = ""
|
||||
|
||||
coEvery { authService.authenticate(username, emptyPassword) } returns
|
||||
AuthenticationService.AuthResult.Failure("Password cannot be empty")
|
||||
|
||||
// Act
|
||||
val result = authService.authenticate(username, emptyPassword)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.AuthResult.Failure)
|
||||
val failureResult = result as AuthenticationService.AuthResult.Failure
|
||||
assertTrue(failureResult.reason.contains("Password"))
|
||||
}
|
||||
|
||||
// ========== Password Change Tests ==========
|
||||
|
||||
@Test
|
||||
fun `changePassword should return Success for valid password change`() = runTest {
|
||||
// Arrange
|
||||
val currentPassword = "oldpassword"
|
||||
val newPassword = "newpassword123"
|
||||
|
||||
coEvery { authService.changePassword(testUserId, currentPassword, newPassword) } returns
|
||||
AuthenticationService.PasswordChangeResult.Success
|
||||
|
||||
// Act
|
||||
val result = authService.changePassword(testUserId, currentPassword, newPassword)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.PasswordChangeResult.Success)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changePassword should validate current password`() = runTest {
|
||||
// Arrange
|
||||
val wrongCurrentPassword = "wrongpassword"
|
||||
val newPassword = "newpassword123"
|
||||
|
||||
coEvery { authService.changePassword(testUserId, wrongCurrentPassword, newPassword) } returns
|
||||
AuthenticationService.PasswordChangeResult.Failure("Current password is incorrect")
|
||||
|
||||
// Act
|
||||
val result = authService.changePassword(testUserId, wrongCurrentPassword, newPassword)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.PasswordChangeResult.Failure)
|
||||
val failureResult = result as AuthenticationService.PasswordChangeResult.Failure
|
||||
assertTrue(failureResult.reason.contains("Current password"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changePassword should reject weak passwords`() = runTest {
|
||||
// Arrange
|
||||
val currentPassword = "oldpassword"
|
||||
val weakPassword = "123" // Too short and simple
|
||||
|
||||
coEvery { authService.changePassword(testUserId, currentPassword, weakPassword) } returns
|
||||
AuthenticationService.PasswordChangeResult.WeakPassword
|
||||
|
||||
// Act
|
||||
val result = authService.changePassword(testUserId, currentPassword, weakPassword)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.PasswordChangeResult.WeakPassword)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changePassword should handle concurrent modifications`() = runTest {
|
||||
// Arrange
|
||||
val currentPassword = "oldpassword"
|
||||
val newPassword = "newpassword123"
|
||||
|
||||
coEvery { authService.changePassword(testUserId, currentPassword, newPassword) } returns
|
||||
AuthenticationService.PasswordChangeResult.Failure("User was modified concurrently")
|
||||
|
||||
// Act
|
||||
val result = authService.changePassword(testUserId, currentPassword, newPassword)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.PasswordChangeResult.Failure)
|
||||
val failureResult = result as AuthenticationService.PasswordChangeResult.Failure
|
||||
assertTrue(failureResult.reason.contains("concurrently"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changePassword should handle user not found scenario`() = runTest {
|
||||
// Arrange
|
||||
val nonExistentUserId = Uuid.random() // <-- GEÄNDERT: von uuid4() zu Uuid.random()
|
||||
val currentPassword = "password"
|
||||
val newPassword = "newpassword123"
|
||||
|
||||
coEvery { authService.changePassword(nonExistentUserId, currentPassword, newPassword) } returns
|
||||
AuthenticationService.PasswordChangeResult.Failure("User not found")
|
||||
|
||||
// Act
|
||||
val result = authService.changePassword(nonExistentUserId, currentPassword, newPassword)
|
||||
|
||||
// Assert
|
||||
assertTrue(result is AuthenticationService.PasswordChangeResult.Failure)
|
||||
val failureResult = result as AuthenticationService.PasswordChangeResult.Failure
|
||||
assertTrue(failureResult.reason.contains("not found"))
|
||||
}
|
||||
|
||||
// ========== AuthenticatedUser Model Tests ==========
|
||||
|
||||
@Test
|
||||
fun `AuthenticatedUser should properly encapsulate user data`() {
|
||||
// Arrange & Act
|
||||
val user = AuthenticationService.AuthenticatedUser(
|
||||
userId = testUserId,
|
||||
personId = testPersonId,
|
||||
username = "testuser",
|
||||
email = "test@example.com",
|
||||
permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE)
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertEquals(testUserId, user.userId)
|
||||
assertEquals(testPersonId, user.personId)
|
||||
assertEquals("testuser", user.username)
|
||||
assertEquals("test@example.com", user.email)
|
||||
assertEquals(2, user.permissions.size)
|
||||
assertTrue(user.permissions.contains(BerechtigungE.PERSON_READ))
|
||||
assertTrue(user.permissions.contains(BerechtigungE.PFERD_CREATE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuthenticatedUser should handle empty permissions list`() {
|
||||
// Arrange & Act
|
||||
val user = AuthenticationService.AuthenticatedUser(
|
||||
userId = testUserId,
|
||||
personId = testPersonId,
|
||||
username = "limiteduser",
|
||||
email = "limited@example.com",
|
||||
permissions = emptyList()
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertTrue(user.permissions.isEmpty())
|
||||
assertEquals("limiteduser", user.username)
|
||||
}
|
||||
|
||||
// ========== Result Type Pattern Tests ==========
|
||||
|
||||
@Test
|
||||
fun `AuthResult sealed class should support pattern matching`() = runTest {
|
||||
// Arrange
|
||||
val successResult = AuthenticationService.AuthResult.Success(
|
||||
"token",
|
||||
AuthenticationService.AuthenticatedUser(
|
||||
testUserId, testPersonId, "user", "email@test.com", emptyList()
|
||||
)
|
||||
)
|
||||
val failureResult = AuthenticationService.AuthResult.Failure("Failed")
|
||||
val lockedResult = AuthenticationService.AuthResult.Locked(LocalDateTime.now())
|
||||
|
||||
// Act & Assert
|
||||
// Test Success result
|
||||
when (successResult) {
|
||||
is AuthenticationService.AuthResult.Success -> {
|
||||
assertNotNull(successResult.token)
|
||||
assertNotNull(successResult.user)
|
||||
}
|
||||
|
||||
else -> fail("Should have been a Success result")
|
||||
}
|
||||
|
||||
// Test Failure result
|
||||
when (failureResult) {
|
||||
is AuthenticationService.AuthResult.Failure -> {
|
||||
assertEquals("Failed", failureResult.reason)
|
||||
}
|
||||
|
||||
else -> fail("Should have been a Failure result")
|
||||
}
|
||||
|
||||
// Test Locked result
|
||||
when (lockedResult) {
|
||||
is AuthenticationService.AuthResult.Locked -> {
|
||||
assertNotNull(lockedResult.lockedUntil)
|
||||
}
|
||||
|
||||
else -> fail("Should have been a Locked result")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PasswordChangeResult sealed class should support pattern matching`() = runTest {
|
||||
// Arrange
|
||||
val successResult = AuthenticationService.PasswordChangeResult.Success
|
||||
val failureResult = AuthenticationService.PasswordChangeResult.Failure("Failed")
|
||||
val weakPasswordResult = AuthenticationService.PasswordChangeResult.WeakPassword
|
||||
|
||||
// Act & Assert
|
||||
// Test Success result
|
||||
when (successResult) {
|
||||
is AuthenticationService.PasswordChangeResult.Success -> {
|
||||
// Success case verified
|
||||
}
|
||||
|
||||
else -> fail("Should have been a Success result")
|
||||
}
|
||||
|
||||
// Test Failure result
|
||||
when (failureResult) {
|
||||
is AuthenticationService.PasswordChangeResult.Failure -> {
|
||||
assertEquals("Failed", failureResult.reason)
|
||||
}
|
||||
|
||||
else -> fail("Should have been a Failure result")
|
||||
}
|
||||
|
||||
// Test WeakPassword result
|
||||
when (weakPasswordResult) {
|
||||
is AuthenticationService.PasswordChangeResult.WeakPassword -> {
|
||||
// WeakPassword case verified
|
||||
}
|
||||
|
||||
else -> fail("Should have been a WeakPassword result")
|
||||
}
|
||||
}
|
||||
}
|
||||
-299
@@ -1,299 +0,0 @@
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Extended tests for JwtService focusing on Result-based APIs, edge cases, and security scenarios.
|
||||
*/
|
||||
class JwtServiceExtendedTest {
|
||||
|
||||
private lateinit var jwtService: JwtService
|
||||
private val testSecret = "a-very-long-and-secure-test-secret-that-is-at-least-512-bits-long-for-hmac512"
|
||||
private val testIssuer = "test-issuer"
|
||||
private val testAudience = "test-audience"
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jwtService = JwtService(
|
||||
secret = testSecret,
|
||||
issuer = testIssuer,
|
||||
audience = testAudience,
|
||||
expiration = 60.minutes
|
||||
)
|
||||
}
|
||||
|
||||
// ========== Result API Tests ==========
|
||||
|
||||
@Test
|
||||
fun `validateToken should return Success with true for valid token`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(true, result.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateToken should return Failure for malformed token`() {
|
||||
// Arrange
|
||||
val malformedToken = "this.is.not.a.valid.jwt.token"
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(malformedToken)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateToken should return Failure for token with wrong issuer`() {
|
||||
// Arrange
|
||||
val wrongIssuerService = JwtService(testSecret, "wrong-issuer", testAudience)
|
||||
val token = wrongIssuerService.generateToken("user-123", "test", emptyList())
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateToken should return Failure for token with wrong audience`() {
|
||||
// Arrange
|
||||
val wrongAudienceService = JwtService(testSecret, testIssuer, "wrong-audience")
|
||||
val token = wrongAudienceService.generateToken("user-123", "test", emptyList())
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateToken should return Failure for expired token`() {
|
||||
// Arrange
|
||||
val expiredService = JwtService(testSecret, testIssuer, testAudience, expiration = (-10).seconds)
|
||||
val token = expiredService.generateToken("user-123", "test", emptyList())
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
// ========== getUserIdFromToken Result API Tests ==========
|
||||
|
||||
@Test
|
||||
fun `getUserIdFromToken should return Success with user ID for valid token`() {
|
||||
// Arrange
|
||||
val userId = "user-12345"
|
||||
val token = jwtService.generateToken(userId, "testuser", emptyList())
|
||||
|
||||
// Act
|
||||
val result = jwtService.getUserIdFromToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(userId, result.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserIdFromToken should return Failure for invalid token`() {
|
||||
// Arrange
|
||||
val invalidToken = "invalid.jwt.token"
|
||||
|
||||
// Act
|
||||
val result = jwtService.getUserIdFromToken(invalidToken)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserIdFromToken should handle missing subject claim`() {
|
||||
// Note: This test verifies that empty/blank subject claims are properly rejected for security
|
||||
val token = jwtService.generateToken("", "testuser", emptyList())
|
||||
|
||||
val result = jwtService.getUserIdFromToken(token)
|
||||
|
||||
// Empty subject should be rejected for security reasons
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(IllegalStateException::class.java, result.exceptionOrNull())
|
||||
assertTrue(result.exceptionOrNull()!!.message!!.contains("no subject"))
|
||||
}
|
||||
|
||||
// ========== getPermissionsFromToken Result API Tests ==========
|
||||
|
||||
@Test
|
||||
fun `getPermissionsFromToken should return Success with permissions for valid token`() {
|
||||
// Arrange
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE, BerechtigungE.VEREIN_UPDATE)
|
||||
val token = jwtService.generateToken("user-123", "testuser", permissions)
|
||||
|
||||
// Act
|
||||
val result = jwtService.getPermissionsFromToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
val extractedPermissions = result.getOrNull()!!
|
||||
assertEquals(3, extractedPermissions.size)
|
||||
assertTrue(extractedPermissions.containsAll(permissions))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPermissionsFromToken should return Failure for invalid token`() {
|
||||
// Arrange
|
||||
val invalidToken = "invalid.jwt.token"
|
||||
|
||||
// Act
|
||||
val result = jwtService.getPermissionsFromToken(invalidToken)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPermissionsFromToken should return empty list for token without permissions`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "testuser", emptyList())
|
||||
|
||||
// Act
|
||||
val result = jwtService.getPermissionsFromToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
val permissions = result.getOrNull()!!
|
||||
assertTrue(permissions.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPermissionsFromToken should ignore unknown permissions gracefully`() {
|
||||
// This test simulates a token with permissions that don't exist in the enum
|
||||
// In practice, this would require manually crafting a JWT, so this tests the enum parsing logic
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ)
|
||||
val token = jwtService.generateToken("user-123", "testuser", permissions)
|
||||
|
||||
val result = jwtService.getPermissionsFromToken(token)
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
val extractedPermissions = result.getOrNull()!!
|
||||
assertEquals(1, extractedPermissions.size)
|
||||
assertEquals(BerechtigungE.PERSON_READ, extractedPermissions[0])
|
||||
}
|
||||
|
||||
// ========== Token Generation Tests ==========
|
||||
|
||||
@Test
|
||||
fun `generateToken should create tokens with correct expiration time`() {
|
||||
// Arrange
|
||||
val shortExpirationService = JwtService(testSecret, testIssuer, testAudience, expiration = 5.seconds)
|
||||
val token = shortExpirationService.generateToken("user-123", "test", emptyList())
|
||||
|
||||
// Act - Validate immediately (should be valid)
|
||||
val immediateResult = shortExpirationService.validateToken(token)
|
||||
|
||||
// Wait and validate again (should be expired) - using Thread.sleep is acceptable for this specific test
|
||||
Thread.sleep(6000) // 6 seconds
|
||||
val delayedResult = shortExpirationService.validateToken(token)
|
||||
|
||||
// Assert
|
||||
assertTrue(immediateResult.isSuccess, "Token should be valid immediately after creation")
|
||||
assertTrue(delayedResult.isFailure, "Token should be expired after waiting")
|
||||
}
|
||||
|
||||
// ========== Legacy Method Backward Compatibility Tests ==========
|
||||
|
||||
@Test
|
||||
fun `legacy methods should maintain backward compatibility`() {
|
||||
// Arrange
|
||||
val userId = "user-123"
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE)
|
||||
val token = jwtService.generateToken(userId, "testuser", permissions)
|
||||
|
||||
// Act & Assert - Legacy methods should still work
|
||||
@Suppress("DEPRECATION")
|
||||
assertTrue(jwtService.isValidToken(token))
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
assertEquals(userId, jwtService.getUserId(token))
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val legacyPermissions = jwtService.getPermissions(token)
|
||||
assertEquals(2, legacyPermissions.size)
|
||||
assertTrue(legacyPermissions.containsAll(permissions))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `legacy methods should handle invalid tokens gracefully`() {
|
||||
// Arrange
|
||||
val invalidToken = "invalid.token"
|
||||
|
||||
// Act & Assert - Legacy methods should handle errors gracefully
|
||||
@Suppress("DEPRECATION")
|
||||
assertFalse(jwtService.isValidToken(invalidToken))
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
assertNull(jwtService.getUserId(invalidToken))
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val permissions = jwtService.getPermissions(invalidToken)
|
||||
assertTrue(permissions.isEmpty())
|
||||
}
|
||||
|
||||
// ========== Security Edge Cases ==========
|
||||
|
||||
@Test
|
||||
fun `should reject tokens with tampered signatures`() {
|
||||
// Arrange
|
||||
val validToken = jwtService.generateToken("user-123", "testuser", emptyList())
|
||||
val tamperedToken = validToken.dropLast(5) + "TAMPR"
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(tamperedToken)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle empty token gracefully`() {
|
||||
// Act
|
||||
val result = jwtService.validateToken("")
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertNotNull(result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle null-like values in token validation`() {
|
||||
// Arrange
|
||||
val nullLikeTokens = listOf("null", "undefined", " ", "\t", "\n")
|
||||
|
||||
// Act & Assert
|
||||
nullLikeTokens.forEach { token ->
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isFailure, "Token '$token' should be rejected")
|
||||
}
|
||||
}
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class JwtServiceTest {
|
||||
|
||||
private lateinit var jwtService: JwtService
|
||||
private val testSecret = "a-very-long-and-secure-test-secret-that-is-at-least-512-bits-long-for-hmac512"
|
||||
private val testIssuer = "test-issuer"
|
||||
private val testAudience = "test-audience"
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jwtService = JwtService(
|
||||
secret = testSecret,
|
||||
issuer = testIssuer,
|
||||
audience = testAudience,
|
||||
expiration = 60.seconds // Kurze Lebensdauer für Tests
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generateToken should create a valid JWT with correct claims`() {
|
||||
// Arrange
|
||||
val userId = "user-123"
|
||||
val username = "testuser"
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE)
|
||||
|
||||
// Act
|
||||
val token = jwtService.generateToken(userId, username, permissions)
|
||||
|
||||
// Assert
|
||||
assertNotNull(token)
|
||||
assertTrue(jwtService.validateToken(token).isSuccess)
|
||||
assertEquals(userId, jwtService.getUserIdFromToken(token).getOrNull())
|
||||
|
||||
val extractedPermissions = jwtService.getPermissionsFromToken(token).getOrElse { emptyList() }
|
||||
assertEquals(2, extractedPermissions.size)
|
||||
assertTrue(extractedPermissions.contains(BerechtigungE.PERSON_READ))
|
||||
assertTrue(extractedPermissions.contains(BerechtigungE.PFERD_CREATE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateToken should return false for token with wrong secret`() {
|
||||
// Arrange
|
||||
val otherService = JwtService("a-different-wrong-secret-that-is-long-enough-1234567890", testIssuer, testAudience)
|
||||
val token = otherService.generateToken("user-123", "test", emptyList())
|
||||
|
||||
// Act & Assert
|
||||
assertFalse(jwtService.validateToken(token).isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateToken should return false for expired token`() {
|
||||
// Arrange
|
||||
val expiredService =
|
||||
JwtService(testSecret, testIssuer, testAudience, expiration = (-10).seconds) // bereits abgelaufen
|
||||
val token = expiredService.generateToken("user-123", "test", emptyList())
|
||||
|
||||
// Act & Assert
|
||||
assertFalse(jwtService.validateToken(token).isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPermissionsFromToken should return empty list for invalid token`() {
|
||||
// Arrange
|
||||
val invalidToken = "this.is.not.a.valid.token"
|
||||
|
||||
// Act
|
||||
val permissions = jwtService.getPermissionsFromToken(invalidToken).getOrElse { emptyList() }
|
||||
|
||||
// Assert
|
||||
assertTrue(permissions.isEmpty())
|
||||
}
|
||||
}
|
||||
-331
@@ -1,331 +0,0 @@
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Comprehensive tests for the Result-based APIs in the auth module.
|
||||
* Tests focus on Result type behavior, error handling, and API consistency.
|
||||
*/
|
||||
class ResultApiTest {
|
||||
|
||||
private lateinit var jwtService: JwtService
|
||||
private val testSecret = "a-very-long-and-secure-test-secret-that-is-at-least-512-bits-long-for-hmac512"
|
||||
private val testIssuer = "test-issuer"
|
||||
private val testAudience = "test-audience"
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jwtService = JwtService(
|
||||
secret = testSecret,
|
||||
issuer = testIssuer,
|
||||
audience = testAudience,
|
||||
expiration = 60.minutes
|
||||
)
|
||||
}
|
||||
|
||||
// ========== Result Success Cases Tests ==========
|
||||
|
||||
@Test
|
||||
fun `Result success cases should provide correct values`() {
|
||||
// Arrange
|
||||
val userId = "user-12345"
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE)
|
||||
val token = jwtService.generateToken(userId, "testuser", permissions)
|
||||
|
||||
// Act - Test all Result-based APIs
|
||||
val validateResult = jwtService.validateToken(token)
|
||||
val userIdResult = jwtService.getUserIdFromToken(token)
|
||||
val permissionsResult = jwtService.getPermissionsFromToken(token)
|
||||
|
||||
// Assert - All should be successful
|
||||
assertTrue(validateResult.isSuccess)
|
||||
assertTrue(validateResult.isFailure.not())
|
||||
assertEquals(true, validateResult.getOrNull())
|
||||
assertNull(validateResult.exceptionOrNull())
|
||||
|
||||
assertTrue(userIdResult.isSuccess)
|
||||
assertTrue(userIdResult.isFailure.not())
|
||||
assertEquals(userId, userIdResult.getOrNull())
|
||||
assertNull(userIdResult.exceptionOrNull())
|
||||
|
||||
assertTrue(permissionsResult.isSuccess)
|
||||
assertTrue(permissionsResult.isFailure.not())
|
||||
val extractedPermissions = permissionsResult.getOrNull()!!
|
||||
assertEquals(2, extractedPermissions.size)
|
||||
assertTrue(extractedPermissions.containsAll(permissions))
|
||||
assertNull(permissionsResult.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Result getOrElse should work correctly for success cases`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "test", listOf(BerechtigungE.VEREIN_READ))
|
||||
|
||||
// Act & Assert
|
||||
val isValid = jwtService.validateToken(token).getOrElse { false }
|
||||
assertTrue(isValid)
|
||||
|
||||
val userId = jwtService.getUserIdFromToken(token).getOrElse { "default" }
|
||||
assertEquals("user-123", userId)
|
||||
|
||||
val permissions = jwtService.getPermissionsFromToken(token).getOrElse { emptyList() }
|
||||
assertEquals(1, permissions.size)
|
||||
assertEquals(BerechtigungE.VEREIN_READ, permissions[0])
|
||||
}
|
||||
|
||||
// ========== Result Failure Cases Tests ==========
|
||||
|
||||
@Test
|
||||
fun `Result failure cases should contain meaningful error messages`() {
|
||||
// Arrange
|
||||
val invalidToken = "invalid.jwt.token"
|
||||
|
||||
// Act
|
||||
val validateResult = jwtService.validateToken(invalidToken)
|
||||
val userIdResult = jwtService.getUserIdFromToken(invalidToken)
|
||||
val permissionsResult = jwtService.getPermissionsFromToken(invalidToken)
|
||||
|
||||
// Assert - All should be failures with proper exception types
|
||||
assertTrue(validateResult.isFailure)
|
||||
assertTrue(validateResult.isSuccess.not())
|
||||
assertNull(validateResult.getOrNull())
|
||||
assertInstanceOf(JWTVerificationException::class.java, validateResult.exceptionOrNull())
|
||||
|
||||
assertTrue(userIdResult.isFailure)
|
||||
assertTrue(userIdResult.isSuccess.not())
|
||||
assertNull(userIdResult.getOrNull())
|
||||
assertInstanceOf(JWTVerificationException::class.java, userIdResult.exceptionOrNull())
|
||||
|
||||
assertTrue(permissionsResult.isFailure)
|
||||
assertTrue(permissionsResult.isSuccess.not())
|
||||
assertNull(permissionsResult.getOrNull())
|
||||
assertInstanceOf(JWTVerificationException::class.java, permissionsResult.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Result getOrElse should work correctly for failure cases`() {
|
||||
// Arrange
|
||||
val invalidToken = "invalid.token"
|
||||
|
||||
// Act & Assert
|
||||
val isValid = jwtService.validateToken(invalidToken).getOrElse { false }
|
||||
assertFalse(isValid)
|
||||
|
||||
val userId = jwtService.getUserIdFromToken(invalidToken).getOrElse { "default-user" }
|
||||
assertEquals("default-user", userId)
|
||||
|
||||
val permissions = jwtService.getPermissionsFromToken(invalidToken).getOrElse { emptyList() }
|
||||
assertTrue(permissions.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Result getOrDefault should handle different default types`() {
|
||||
// Arrange
|
||||
val invalidToken = "malformed.jwt"
|
||||
|
||||
// Act & Assert - Test various default value types
|
||||
val defaultBoolean = jwtService.validateToken(invalidToken).getOrElse { true }
|
||||
assertTrue(defaultBoolean)
|
||||
|
||||
val defaultString = jwtService.getUserIdFromToken(invalidToken).getOrElse { "anonymous" }
|
||||
assertEquals("anonymous", defaultString)
|
||||
|
||||
val defaultList = jwtService.getPermissionsFromToken(invalidToken).getOrElse { listOf(BerechtigungE.PERSON_READ) }
|
||||
assertEquals(1, defaultList.size)
|
||||
assertEquals(BerechtigungE.PERSON_READ, defaultList[0])
|
||||
}
|
||||
|
||||
// ========== Result Chaining Tests ==========
|
||||
|
||||
@Test
|
||||
fun `Result chaining should work correctly`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
|
||||
// Act - Chain Result operations
|
||||
val chainedResult = jwtService.validateToken(token)
|
||||
.map { isValid -> if (isValid) "VALID" else "INVALID" }
|
||||
|
||||
val userChainedResult = jwtService.getUserIdFromToken(token)
|
||||
.map { userId -> "User: $userId" }
|
||||
|
||||
val permissionChainedResult = jwtService.getPermissionsFromToken(token)
|
||||
.map { permissions -> permissions.map { it.name } }
|
||||
|
||||
// Assert
|
||||
assertTrue(chainedResult.isSuccess)
|
||||
assertEquals("VALID", chainedResult.getOrNull())
|
||||
|
||||
assertTrue(userChainedResult.isSuccess)
|
||||
assertEquals("User: user-123", userChainedResult.getOrNull())
|
||||
|
||||
assertTrue(permissionChainedResult.isSuccess)
|
||||
val permissionNames = permissionChainedResult.getOrNull()!!
|
||||
assertEquals(1, permissionNames.size)
|
||||
assertEquals("PERSON_READ", permissionNames[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Result chaining should handle failures correctly`() {
|
||||
// Arrange
|
||||
val invalidToken = "bad.token"
|
||||
|
||||
// Act - Chain operations that will fail
|
||||
val chainedResult = jwtService.validateToken(invalidToken)
|
||||
.map { isValid -> "This should not be called" }
|
||||
|
||||
val userChainedResult = jwtService.getUserIdFromToken(invalidToken)
|
||||
.map { userId -> "User: $userId" }
|
||||
|
||||
// Assert - Chained operations should not execute on failure
|
||||
assertTrue(chainedResult.isFailure)
|
||||
assertNull(chainedResult.getOrNull())
|
||||
|
||||
assertTrue(userChainedResult.isFailure)
|
||||
assertNull(userChainedResult.getOrNull())
|
||||
}
|
||||
|
||||
// ========== Exception Handling Consistency Tests ==========
|
||||
|
||||
@Test
|
||||
fun `Exception handling should be consistent across all Result methods`() {
|
||||
// Test various types of invalid tokens
|
||||
val testCases = listOf(
|
||||
"malformed.token",
|
||||
"",
|
||||
"too.short",
|
||||
"way.too.many.parts.in.this.token.structure",
|
||||
"null.claims.signature"
|
||||
)
|
||||
|
||||
testCases.forEach { invalidToken ->
|
||||
// All methods should handle the same invalid input consistently
|
||||
val validateResult = jwtService.validateToken(invalidToken)
|
||||
val userIdResult = jwtService.getUserIdFromToken(invalidToken)
|
||||
val permissionsResult = jwtService.getPermissionsFromToken(invalidToken)
|
||||
|
||||
// All should fail
|
||||
assertTrue(validateResult.isFailure, "validateToken should fail for: $invalidToken")
|
||||
assertTrue(userIdResult.isFailure, "getUserIdFromToken should fail for: $invalidToken")
|
||||
assertTrue(permissionsResult.isFailure, "getPermissionsFromToken should fail for: $invalidToken")
|
||||
|
||||
// All should have non-null exceptions
|
||||
assertNotNull(validateResult.exceptionOrNull(), "validateToken should have exception for: $invalidToken")
|
||||
assertNotNull(userIdResult.exceptionOrNull(), "getUserIdFromToken should have exception for: $invalidToken")
|
||||
assertNotNull(permissionsResult.exceptionOrNull(), "getPermissionsFromToken should have exception for: $invalidToken")
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Special Edge Cases for Result API ==========
|
||||
|
||||
@Test
|
||||
fun `Result API should handle expired tokens consistently`() {
|
||||
// Arrange
|
||||
val expiredService = JwtService(testSecret, testIssuer, testAudience, expiration = (-10).seconds)
|
||||
val expiredToken = expiredService.generateToken("user-123", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
|
||||
// Act
|
||||
val validateResult = jwtService.validateToken(expiredToken)
|
||||
val userIdResult = jwtService.getUserIdFromToken(expiredToken)
|
||||
val permissionsResult = jwtService.getPermissionsFromToken(expiredToken)
|
||||
|
||||
// Assert - All should consistently fail for expired tokens
|
||||
assertTrue(validateResult.isFailure)
|
||||
assertTrue(userIdResult.isFailure)
|
||||
assertTrue(permissionsResult.isFailure)
|
||||
|
||||
// All should have JWT verification exceptions
|
||||
assertInstanceOf(JWTVerificationException::class.java, validateResult.exceptionOrNull())
|
||||
assertInstanceOf(JWTVerificationException::class.java, userIdResult.exceptionOrNull())
|
||||
assertInstanceOf(JWTVerificationException::class.java, permissionsResult.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Result API should handle wrong issuer and audience consistently`() {
|
||||
// Arrange
|
||||
val wrongConfigService = JwtService(testSecret, "wrong-issuer", "wrong-audience")
|
||||
val wrongToken = wrongConfigService.generateToken("user-123", "test", emptyList())
|
||||
|
||||
// Act
|
||||
val validateResult = jwtService.validateToken(wrongToken)
|
||||
val userIdResult = jwtService.getUserIdFromToken(wrongToken)
|
||||
val permissionsResult = jwtService.getPermissionsFromToken(wrongToken)
|
||||
|
||||
// Assert - All should consistently fail
|
||||
assertTrue(validateResult.isFailure)
|
||||
assertTrue(userIdResult.isFailure)
|
||||
assertTrue(permissionsResult.isFailure)
|
||||
|
||||
// All exceptions should be JWT verification exceptions
|
||||
assertInstanceOf(JWTVerificationException::class.java, validateResult.exceptionOrNull())
|
||||
assertInstanceOf(JWTVerificationException::class.java, userIdResult.exceptionOrNull())
|
||||
assertInstanceOf(JWTVerificationException::class.java, permissionsResult.exceptionOrNull())
|
||||
}
|
||||
|
||||
// ========== Result API Interoperability Tests ==========
|
||||
|
||||
@Test
|
||||
fun `Result API should work well with Kotlin standard library`() {
|
||||
// Arrange
|
||||
val validToken = jwtService.generateToken("user-123", "test", listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE))
|
||||
val invalidToken = "invalid.token"
|
||||
|
||||
// Act & Assert - Test integration with Kotlin stdlib
|
||||
|
||||
// Use with let
|
||||
val letResult = jwtService.validateToken(validToken).getOrNull()?.let { "Token is valid: $it" }
|
||||
assertEquals("Token is valid: true", letResult)
|
||||
|
||||
// Use with also
|
||||
var sideEffectCalled = false
|
||||
jwtService.getUserIdFromToken(validToken).also { result ->
|
||||
if (result.isSuccess) sideEffectCalled = true
|
||||
}
|
||||
assertTrue(sideEffectCalled)
|
||||
|
||||
// Use with takeIf
|
||||
val conditionalResult = jwtService.getPermissionsFromToken(validToken)
|
||||
.getOrNull()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
assertNotNull(conditionalResult)
|
||||
assertEquals(2, conditionalResult!!.size)
|
||||
|
||||
// Use with run
|
||||
val runResult = jwtService.validateToken(invalidToken).run {
|
||||
if (isFailure) "Failed as expected" else "Unexpected success"
|
||||
}
|
||||
assertEquals("Failed as expected", runResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Result API should support functional programming patterns`() {
|
||||
// Arrange
|
||||
val token = jwtService.generateToken("user-123", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
|
||||
// Act & Assert - Functional patterns
|
||||
|
||||
// Map transformations
|
||||
val transformedValidation = jwtService.validateToken(token)
|
||||
.map { if (it) 1 else 0 }
|
||||
.getOrElse { -1 }
|
||||
assertEquals(1, transformedValidation)
|
||||
|
||||
// Filter-like behavior
|
||||
val hasReadPermission = jwtService.getPermissionsFromToken(token)
|
||||
.map { permissions -> permissions.contains(BerechtigungE.PERSON_READ) }
|
||||
.getOrElse { false }
|
||||
assertTrue(hasReadPermission)
|
||||
|
||||
// Combine multiple Results
|
||||
val combinedCheck = jwtService.validateToken(token).isSuccess &&
|
||||
jwtService.getUserIdFromToken(token).isSuccess &&
|
||||
jwtService.getPermissionsFromToken(token).isSuccess
|
||||
assertTrue(combinedCheck)
|
||||
}
|
||||
}
|
||||
-390
@@ -1,390 +0,0 @@
|
||||
package at.mocode.infrastructure.auth.client
|
||||
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertTimeoutPreemptively
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
import java.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Security-focused tests for JWT handling.
|
||||
* Tests against common JWT vulnerabilities and security attack vectors.
|
||||
*/
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class SecurityTest {
|
||||
|
||||
private lateinit var jwtService: JwtService
|
||||
private val testSecret = "a-very-long-and-secure-test-secret-that-is-at-least-512-bits-long-for-hmac512"
|
||||
private val testIssuer = "test-issuer"
|
||||
private val testAudience = "test-audience"
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jwtService = JwtService(
|
||||
secret = testSecret,
|
||||
issuer = testIssuer,
|
||||
audience = testAudience,
|
||||
expiration = 60.minutes
|
||||
)
|
||||
}
|
||||
|
||||
// ========== Signature Tampering Tests ==========
|
||||
|
||||
@Test
|
||||
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||
fun `should reject tokens with tampered signatures`() {
|
||||
// Arrange - neue JwtService-Instanz für vollständige Isolation
|
||||
val isolatedJwtService = JwtService(
|
||||
secret = testSecret,
|
||||
issuer = testIssuer,
|
||||
audience = testAudience,
|
||||
expiration = 60.minutes
|
||||
)
|
||||
|
||||
// Arrange
|
||||
val validToken = isolatedJwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
|
||||
val tokenParts = validToken.split(".")
|
||||
|
||||
// Validierung der Token-Struktur
|
||||
assertEquals(3, tokenParts.size, "JWT should have exactly 3 parts")
|
||||
assertTrue(tokenParts[2].isNotEmpty(), "Signature part should not be empty")
|
||||
|
||||
// Tamper with the signature by changing the last character
|
||||
val tamperedSignature = tokenParts[2].dropLast(1) + "X"
|
||||
val tamperedToken = "${tokenParts[0]}.${tokenParts[1]}.$tamperedSignature"
|
||||
|
||||
// Sicherstellen, dass Signatur tatsächlich verändert wurde
|
||||
assertNotEquals(tokenParts[2], tamperedSignature, "Signature should be different after tampering")
|
||||
|
||||
// Act
|
||||
val result = isolatedJwtService.validateToken(tamperedToken)
|
||||
|
||||
// Assert - Erweiterte Validierung
|
||||
assertTrue(result.isFailure, "Tampered token should be rejected")
|
||||
val exception = result.exceptionOrNull()
|
||||
assertNotNull(exception, "Exception should be present for failed validation")
|
||||
assertInstanceOf(
|
||||
JWTVerificationException::class.java, exception,
|
||||
"Exception should be JWTVerificationException, but was: ${exception?.javaClass?.simpleName}"
|
||||
)
|
||||
|
||||
// Zusätzliche Sicherheitsüberprüfung: Original Token sollte noch gültig sein
|
||||
val originalResult = isolatedJwtService.validateToken(validToken)
|
||||
assertTrue(originalResult.isSuccess, "Original valid token should still be valid")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||
fun `should reject tokens with completely different signatures`() {
|
||||
// Isolierte Instanzen verwenden
|
||||
val isolatedJwtService1 = JwtService(testSecret, testIssuer, testAudience, expiration = 60.minutes)
|
||||
val isolatedJwtService2 = JwtService(testSecret, testIssuer, testAudience, expiration = 60.minutes)
|
||||
|
||||
// Arrange
|
||||
val validToken = isolatedJwtService1.generateToken("user-123", "testuser", emptyList())
|
||||
val anotherValidToken = isolatedJwtService2.generateToken("user-456", "anotheruser", emptyList())
|
||||
|
||||
val tokenParts1 = validToken.split(".")
|
||||
val tokenParts2 = anotherValidToken.split(".")
|
||||
|
||||
// Mix signature from different token
|
||||
val mixedToken = "${tokenParts1[0]}.${tokenParts1[1]}.${tokenParts2[2]}"
|
||||
|
||||
// Act
|
||||
val result = isolatedJwtService1.validateToken(mixedToken)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject tokens with extended expiration time`() {
|
||||
// This test simulates an attacker trying to extend the token's validity
|
||||
// by manipulating the payload (even though it will break the signature)
|
||||
|
||||
// Arrange
|
||||
val validToken = jwtService.generateToken("user-123", "testuser", emptyList())
|
||||
val tokenParts = validToken.split(".")
|
||||
|
||||
// Try to use a different payload with extended expiration
|
||||
// (This will fail signature validation, which is the expected behavior)
|
||||
val anotherService = JwtService(testSecret, testIssuer, testAudience, expiration = 24.minutes)
|
||||
val longValidToken = anotherService.generateToken("user-123", "testuser", emptyList())
|
||||
val longValidParts = longValidToken.split(".")
|
||||
|
||||
val tamperedToken = "${longValidParts[0]}.${longValidParts[1]}.${tokenParts[2]}"
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(tamperedToken)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
// ========== Timing Attack Resistance Tests ==========
|
||||
|
||||
@Test
|
||||
fun `token validation should be resistant to timing attacks`() {
|
||||
// Arrange
|
||||
val validToken = jwtService.generateToken("user-123", "testuser", emptyList())
|
||||
val invalidTokens = listOf(
|
||||
"invalid.token.signature",
|
||||
validToken.dropLast(5) + "wrong",
|
||||
"completely.wrong.token",
|
||||
""
|
||||
)
|
||||
|
||||
// Measure validation times for valid and invalid tokens
|
||||
val validationTimes = mutableListOf<Long>()
|
||||
|
||||
// Act - Test multiple times to get consistent timing measurements
|
||||
repeat(10) {
|
||||
// Valid token
|
||||
val start1 = System.nanoTime()
|
||||
jwtService.validateToken(validToken)
|
||||
val end1 = System.nanoTime()
|
||||
validationTimes.add(end1 - start1)
|
||||
|
||||
// Invalid tokens
|
||||
invalidTokens.forEach { invalidToken ->
|
||||
val start2 = System.nanoTime()
|
||||
jwtService.validateToken(invalidToken)
|
||||
val end2 = System.nanoTime()
|
||||
validationTimes.add(end2 - start2)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert - All validation operations should complete reasonably quickly
|
||||
// (This is not a perfect timing attack test but ensures no obvious timing differences)
|
||||
validationTimes.forEach { time ->
|
||||
assertTrue(time < 50_000_000, "Token validation should complete within 50ms (was ${time}ns)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validation should complete under consistent time limits`() {
|
||||
// Arrange
|
||||
val tokens = (1..20).map {
|
||||
jwtService.generateToken("user-$it", "testuser$it", listOf(BerechtigungE.PERSON_READ))
|
||||
}
|
||||
|
||||
// Act & Assert - Each validation should complete within reasonable time
|
||||
tokens.forEach { token ->
|
||||
assertTimeoutPreemptively(Duration.ofMillis(100)) {
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== JWT Vulnerability Tests (Based on Common CVEs) ==========
|
||||
|
||||
@Test
|
||||
fun `should validate against algorithm confusion attack`() {
|
||||
// This test ensures our service doesn't accept tokens with different algorithms
|
||||
// Common attack: changing algorithm from RS256 to HS256 in the header
|
||||
|
||||
// Arrange
|
||||
val validToken = jwtService.generateToken("user-123", "testuser", emptyList())
|
||||
val tokenParts = validToken.split(".")
|
||||
|
||||
// Try to create a token with a manipulated header (algorithm confusion)
|
||||
// In practice, this would require crafting a specific header, but our implementation
|
||||
// should reject any token that doesn't match our configured algorithm
|
||||
val manipulatedHeader = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" // RS256 instead of HS512
|
||||
val manipulatedToken = "$manipulatedHeader.${tokenParts[1]}.${tokenParts[2]}"
|
||||
|
||||
// Act
|
||||
val result = jwtService.validateToken(manipulatedToken)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject tokens without proper structure`() {
|
||||
// Test malformed tokens that don't follow the JWT structure
|
||||
val malformedTokens = listOf(
|
||||
"not.a.jwt",
|
||||
"only.two.parts",
|
||||
"too.many.parts.here.extra",
|
||||
".empty.first.",
|
||||
"first..third",
|
||||
"first.second.",
|
||||
"",
|
||||
"single-string-no-dots"
|
||||
)
|
||||
|
||||
malformedTokens.forEach { malformedToken ->
|
||||
val result = jwtService.validateToken(malformedToken)
|
||||
assertTrue(result.isFailure, "Malformed token '$malformedToken' should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle extremely long tokens without hanging`() {
|
||||
// Test against DoS attacks using extremely long tokens
|
||||
val longString = "a".repeat(10000)
|
||||
val longTokens = listOf(
|
||||
"$longString.valid.token",
|
||||
"valid.$longString.token",
|
||||
"valid.token.$longString",
|
||||
"$longString.$longString.$longString"
|
||||
)
|
||||
|
||||
longTokens.forEach { longToken ->
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(1)) {
|
||||
val result = jwtService.validateToken(longToken)
|
||||
assertTrue(result.isFailure, "Long token should be rejected quickly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Token Replay Attack Tests ==========
|
||||
|
||||
@Test
|
||||
fun `should handle multiple validations of same token consistently`() {
|
||||
// Test that the same token always produces the same validation result
|
||||
// This ensures no state is maintained that could be exploited
|
||||
|
||||
val token = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
|
||||
|
||||
repeat(10) {
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess, "Same token should always validate successfully")
|
||||
|
||||
val userId = jwtService.getUserIdFromToken(token)
|
||||
assertEquals("user-123", userId.getOrNull())
|
||||
|
||||
val permissions = jwtService.getPermissionsFromToken(token)
|
||||
assertEquals(1, permissions.getOrElse { emptyList() }.size)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Input Validation Security Tests ==========
|
||||
|
||||
@Test
|
||||
fun `should handle special characters and injection attempts`() {
|
||||
// Test with various special characters that might cause issues
|
||||
val specialUserIds = listOf(
|
||||
"user'; DROP TABLE users; --",
|
||||
"user<script>alert('xss')</script>",
|
||||
"user\n\r\t",
|
||||
"user\u0000null",
|
||||
"user${'\u0001'}control",
|
||||
"../../../etc/passwd"
|
||||
)
|
||||
|
||||
specialUserIds.forEach { specialUserId ->
|
||||
val token = jwtService.generateToken(specialUserId, "testuser", emptyList())
|
||||
val result = jwtService.getUserIdFromToken(token)
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(
|
||||
specialUserId, result.getOrNull(),
|
||||
"Special characters in user ID should be preserved exactly"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle unicode and international characters`() {
|
||||
// Test with international characters to ensure proper encoding/decoding
|
||||
val internationalUserIds = listOf(
|
||||
"用户123", // Chinese
|
||||
"utilisateur123", // French
|
||||
"пользователь123", // Russian
|
||||
"مستخدم123", // Arabic
|
||||
"🧑💻user123" // Emoji
|
||||
)
|
||||
|
||||
internationalUserIds.forEach { userId ->
|
||||
val token = jwtService.generateToken(userId, "testuser", emptyList())
|
||||
val result = jwtService.getUserIdFromToken(token)
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(
|
||||
userId, result.getOrNull(),
|
||||
"International characters should be handled correctly"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Rate Limiting Simulation Tests ==========
|
||||
|
||||
@Test
|
||||
fun `should handle high frequency validation requests`() {
|
||||
// Simulate high-frequency validation to ensure no memory leaks or performance degradation
|
||||
val token = jwtService.generateToken("user-123", "testuser", emptyList())
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
repeat(1000) {
|
||||
val result = jwtService.validateToken(token)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
val endTime = System.currentTimeMillis()
|
||||
|
||||
// Should complete 1000 validations in a reasonable time (less than 5 seconds)
|
||||
assertTrue(
|
||||
endTime - startTime < 5000,
|
||||
"1000 token validations should complete within 5 seconds"
|
||||
)
|
||||
}
|
||||
|
||||
// ========== Memory Safety Tests ==========
|
||||
|
||||
@Test
|
||||
fun `should not leak sensitive information in error messages`() {
|
||||
// Ensure that error messages don't contain sensitive information
|
||||
val invalidToken = "invalid.token.here"
|
||||
val result = jwtService.validateToken(invalidToken)
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
val exception = result.exceptionOrNull()
|
||||
assertNotNull(exception)
|
||||
|
||||
// Error message should not contain the secret or other sensitive information
|
||||
val errorMessage = exception!!.message ?: ""
|
||||
assertFalse(
|
||||
errorMessage.contains(testSecret),
|
||||
"Error message should not contain the secret"
|
||||
)
|
||||
assertFalse(
|
||||
errorMessage.contains("HMAC"),
|
||||
"Error message should not reveal internal algorithm details"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||
fun `should handle concurrent validation requests safely`() {
|
||||
// Thread-safe JwtService-Instanz
|
||||
val threadSafeJwtService = JwtService(testSecret, testIssuer, testAudience, expiration = 60.minutes)
|
||||
val token = threadSafeJwtService.generateToken("user-123", "testuser", emptyList())
|
||||
val results = mutableListOf<Boolean>()
|
||||
|
||||
|
||||
val threads = (1..10).map { threadIndex ->
|
||||
Thread {
|
||||
repeat(100) {
|
||||
val result = jwtService.validateToken(token)
|
||||
synchronized(results) {
|
||||
results.add(result.isSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
threads.forEach { it.start() }
|
||||
threads.forEach { it.join() }
|
||||
|
||||
// All validations should succeed
|
||||
assertEquals(1000, results.size)
|
||||
assertTrue(results.all { it }, "All concurrent validations should succeed")
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,6 @@ dependencies {
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
// Stellt gemeinsame Abhängigkeiten bereit.
|
||||
implementation(projects.platform.platformDependencies)
|
||||
// Nutzt die Client-Logik für die Kommunikation mit Keycloak.
|
||||
implementation(projects.infrastructure.auth.authClient)
|
||||
// Spring Boot Starter für einen Web-Service.
|
||||
// OPTIMIERUNG: Verwendung des `spring-boot-essentials`-Bundles.
|
||||
implementation(libs.bundles.spring.boot.essentials)
|
||||
|
||||
+4
-47
@@ -1,55 +1,12 @@
|
||||
package at.mocode.infrastructure.auth.config
|
||||
|
||||
import at.mocode.infrastructure.auth.client.JwtService
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.validation.annotation.Validated
|
||||
import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Size
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Spring-Konfiguration für das Auth-Server-Modul.
|
||||
* Stellt die notwendigen Beans und Einstellungen für JWT-Verarbeitung und Authentifizierung bereit.
|
||||
*
|
||||
* Note: JWT handling is now fully delegated to Keycloak via OAuth2 Resource Server.
|
||||
* This auth-server focuses on user management through Keycloak Admin Client.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(AuthServerConfiguration.JwtProperties::class)
|
||||
class AuthServerConfiguration {
|
||||
|
||||
/**
|
||||
* Erstellt einen JwtService-Bean mit Konfiguration aus den Application Properties.
|
||||
*/
|
||||
@Bean
|
||||
fun jwtService(jwtProperties: JwtProperties): JwtService {
|
||||
// Basic safeguard: warn if default secret is used
|
||||
if (jwtProperties.secret == "default-secret-for-development-only-please-change-in-production") {
|
||||
System.err.println("[SECURITY WARNING] Using default JWT secret – DO NOT use this in production!")
|
||||
}
|
||||
return JwtService(
|
||||
secret = jwtProperties.secret,
|
||||
issuer = jwtProperties.issuer,
|
||||
audience = jwtProperties.audience,
|
||||
expiration = jwtProperties.expiration.minutes
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfigurationseigenschaften für JWT-Einstellungen.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "auth.jwt")
|
||||
@Validated
|
||||
data class JwtProperties(
|
||||
@field:NotBlank
|
||||
@field:Size(min = 32, message = "JWT secret must be at least 32 characters for HMAC512")
|
||||
val secret: String = "default-secret-for-development-only-please-change-in-production",
|
||||
@field:NotBlank
|
||||
val issuer: String = "meldestelle-auth-server",
|
||||
@field:NotBlank
|
||||
val audience: String = "meldestelle-services",
|
||||
@field:Min(1)
|
||||
val expiration: Long = 60 // minutes
|
||||
)
|
||||
}
|
||||
class AuthServerConfiguration
|
||||
|
||||
+12
-38
@@ -1,13 +1,16 @@
|
||||
package at.mocode.infrastructure.auth
|
||||
|
||||
import at.mocode.infrastructure.auth.client.JwtService
|
||||
import at.mocode.infrastructure.auth.config.AuthServerConfiguration
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Basic tests for the Auth Server application and configuration.
|
||||
* These tests verify the application structure and basic functionality without requiring full Spring context.
|
||||
* These tests verify the application structure without requiring full Spring context.
|
||||
*
|
||||
* Note: Custom JWT handling has been removed. Authentication is now fully handled
|
||||
* by Keycloak via OAuth2 Resource Server.
|
||||
*/
|
||||
class AuthServerApplicationTest {
|
||||
|
||||
@@ -30,43 +33,14 @@ class AuthServerApplicationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth server configuration should create JWT service bean`() {
|
||||
// Arrange
|
||||
val config = AuthServerConfiguration()
|
||||
val jwtProperties = AuthServerConfiguration.JwtProperties(
|
||||
secret = "test-secret-for-testing-only-at-least-512-bits-long-for-hmac512",
|
||||
issuer = "test-issuer",
|
||||
audience = "test-audience",
|
||||
expiration = 60
|
||||
)
|
||||
|
||||
// Act
|
||||
val jwtService = config.jwtService(jwtProperties)
|
||||
|
||||
// Assert
|
||||
assertNotNull(jwtService)
|
||||
assertInstanceOf(JwtService::class.java, jwtService)
|
||||
|
||||
// Test that the service can generate and validate tokens
|
||||
val token = jwtService.generateToken("test-user", "testuser", emptyList())
|
||||
assertNotNull(token)
|
||||
assertTrue(token.isNotEmpty())
|
||||
|
||||
val validationResult = jwtService.validateToken(token)
|
||||
assertTrue(validationResult.isSuccess)
|
||||
assertEquals(true, validationResult.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JWT properties should have sensible defaults`() {
|
||||
fun `auth server configuration should be present`() {
|
||||
// Arrange & Act
|
||||
val defaultProperties = AuthServerConfiguration.JwtProperties()
|
||||
val config = AuthServerConfiguration()
|
||||
|
||||
// Assert
|
||||
assertNotNull(defaultProperties.secret)
|
||||
assertTrue(defaultProperties.secret.isNotEmpty())
|
||||
assertEquals("meldestelle-auth-server", defaultProperties.issuer)
|
||||
assertEquals("meldestelle-services", defaultProperties.audience)
|
||||
assertEquals(60L, defaultProperties.expiration)
|
||||
assertNotNull(config)
|
||||
assertTrue(config::class.java.isAnnotationPresent(org.springframework.context.annotation.Configuration::class.java)) {
|
||||
"AuthServerConfiguration should be annotated with @Configuration"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-196
@@ -1,10 +1,8 @@
|
||||
package at.mocode.infrastructure.auth
|
||||
|
||||
import at.mocode.infrastructure.auth.client.JwtService
|
||||
import at.mocode.infrastructure.auth.client.model.BerechtigungE
|
||||
import at.mocode.infrastructure.auth.config.AuthServerConfiguration
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
@@ -13,21 +11,16 @@ import org.springframework.test.context.TestPropertySource
|
||||
|
||||
/**
|
||||
* Minimal integration tests for the Auth Server.
|
||||
* Tests essential functionality without full Spring Boot context complexity.
|
||||
* Focuses on core service integration and configuration validation.
|
||||
*
|
||||
* This implements "Option 1: Minimale Integration Tests" focusing on essentials
|
||||
* without vollständige Spring Boot Konfiguration.
|
||||
* Note: Custom JWT handling has been removed. Authentication is now fully handled
|
||||
* by Keycloak via OAuth2 Resource Server. This test verifies the basic Spring
|
||||
* context loads correctly.
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
classes = [AuthServerConfiguration::class]
|
||||
)
|
||||
@TestPropertySource(properties = [
|
||||
"auth.jwt.secret=test-secret-for-testing-only-at-least-512-bits-long-for-hmac512-algorithm",
|
||||
"auth.jwt.issuer=test-issuer",
|
||||
"auth.jwt.audience=test-audience",
|
||||
"auth.jwt.expiration=60",
|
||||
"spring.main.web-application-type=none",
|
||||
"logging.level.org.springframework.security=WARN"
|
||||
])
|
||||
@@ -36,22 +29,6 @@ class AuthServerIntegrationTest {
|
||||
@Autowired
|
||||
private lateinit var applicationContext: ApplicationContext
|
||||
|
||||
@Autowired
|
||||
private lateinit var jwtService: JwtService
|
||||
|
||||
private lateinit var testToken: String
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
testToken = jwtService.generateToken(
|
||||
userId = "test-user-123",
|
||||
username = "testuser",
|
||||
permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.VEREIN_READ)
|
||||
)
|
||||
}
|
||||
|
||||
// ========== Core Service Integration Tests ==========
|
||||
|
||||
@Test
|
||||
fun `application context should load with minimal configuration`() {
|
||||
// Verify that the Spring context loads successfully
|
||||
@@ -63,138 +40,17 @@ class AuthServerIntegrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JwtService should be properly configured as Spring bean`() {
|
||||
// Verify that JwtService is available as a Spring bean
|
||||
assertTrue(applicationContext.containsBean("jwtService"))
|
||||
assertNotNull(jwtService)
|
||||
assertInstanceOf(JwtService::class.java, jwtService)
|
||||
|
||||
println("[DEBUG_LOG] JwtService bean configured successfully")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JWT service should generate valid tokens`() {
|
||||
// Test token generation functionality
|
||||
val token = jwtService.generateToken(
|
||||
userId = "integration-test-user",
|
||||
username = "inttest",
|
||||
permissions = listOf(BerechtigungE.PERSON_CREATE, BerechtigungE.PFERD_READ)
|
||||
)
|
||||
|
||||
assertNotNull(token)
|
||||
assertTrue(token.isNotEmpty())
|
||||
|
||||
// Verify token can be validated
|
||||
val validationResult = jwtService.validateToken(token)
|
||||
assertTrue(validationResult.isSuccess)
|
||||
assertEquals(true, validationResult.getOrNull())
|
||||
|
||||
println("[DEBUG_LOG] Token generated and validated successfully")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JWT service should extract user information correctly`() {
|
||||
// Test user ID extraction
|
||||
val userIdResult = jwtService.getUserIdFromToken(testToken)
|
||||
assertTrue(userIdResult.isSuccess)
|
||||
assertEquals("test-user-123", userIdResult.getOrNull())
|
||||
|
||||
// Test permissions extraction
|
||||
val permissionsResult = jwtService.getPermissionsFromToken(testToken)
|
||||
assertTrue(permissionsResult.isSuccess)
|
||||
val permissions = permissionsResult.getOrNull()!!
|
||||
assertEquals(2, permissions.size)
|
||||
assertTrue(permissions.contains(BerechtigungE.PERSON_READ))
|
||||
assertTrue(permissions.contains(BerechtigungE.VEREIN_READ))
|
||||
|
||||
println("[DEBUG_LOG] User information extracted correctly")
|
||||
println("[DEBUG_LOG] User ID: ${userIdResult.getOrNull()}")
|
||||
println("[DEBUG_LOG] Permissions: $permissions")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JWT service should handle invalid tokens properly`() {
|
||||
val invalidToken = "invalid.jwt.token"
|
||||
|
||||
// Validation should fail
|
||||
val validationResult = jwtService.validateToken(invalidToken)
|
||||
assertTrue(validationResult.isFailure)
|
||||
|
||||
// User ID extraction should fail
|
||||
val userIdResult = jwtService.getUserIdFromToken(invalidToken)
|
||||
assertTrue(userIdResult.isFailure)
|
||||
|
||||
// Permissions extraction should fail
|
||||
val permissionsResult = jwtService.getPermissionsFromToken(invalidToken)
|
||||
assertTrue(permissionsResult.isFailure)
|
||||
|
||||
println("[DEBUG_LOG] Invalid token handling works correctly")
|
||||
}
|
||||
|
||||
// ========== Configuration Validation Tests ==========
|
||||
|
||||
@Test
|
||||
fun `configuration properties should be properly loaded`() {
|
||||
// Test that JWT configuration is loaded correctly
|
||||
val jwtProperties = applicationContext.getBean(AuthServerConfiguration.JwtProperties::class.java)
|
||||
assertNotNull(jwtProperties)
|
||||
assertEquals("test-issuer", jwtProperties.issuer)
|
||||
assertEquals("test-audience", jwtProperties.audience)
|
||||
assertEquals(60L, jwtProperties.expiration)
|
||||
|
||||
println("[DEBUG_LOG] Configuration properties loaded correctly")
|
||||
println("[DEBUG_LOG] Issuer: ${jwtProperties.issuer}")
|
||||
println("[DEBUG_LOG] Audience: ${jwtProperties.audience}")
|
||||
println("[DEBUG_LOG] Expiration: ${jwtProperties.expiration}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `essential beans should be properly configured`() {
|
||||
// Verify that essential beans for auth functionality are available
|
||||
fun `configuration bean should be present`() {
|
||||
// Verify that essential beans are available
|
||||
val beanNames = applicationContext.beanDefinitionNames.toList()
|
||||
|
||||
// Check for JWT service bean
|
||||
assertTrue(applicationContext.containsBean("jwtService")) {
|
||||
"JwtService bean should be configured"
|
||||
}
|
||||
|
||||
// Check for configuration bean
|
||||
assertTrue(beanNames.any { it.contains("authServerConfiguration") }) {
|
||||
"AuthServerConfiguration bean should be configured"
|
||||
}
|
||||
|
||||
println("[DEBUG_LOG] Essential beans configured successfully")
|
||||
println("[DEBUG_LOG] Auth-related beans: ${beanNames.filter { it.contains("jwt") || it.contains("auth") }}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JWT configuration integration should work end-to-end`() {
|
||||
// Test the complete flow from configuration to token operations
|
||||
val userId = "end-to-end-test"
|
||||
val username = "e2etest"
|
||||
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PERSON_CREATE)
|
||||
|
||||
// Generate token
|
||||
val token = jwtService.generateToken(userId, username, permissions)
|
||||
assertNotNull(token)
|
||||
assertTrue(token.isNotEmpty())
|
||||
|
||||
// Validate token
|
||||
val isValid = jwtService.validateToken(token)
|
||||
assertTrue(isValid.isSuccess)
|
||||
|
||||
// Extract and verify data
|
||||
val extractedUserId = jwtService.getUserIdFromToken(token).getOrNull()
|
||||
val extractedPermissions = jwtService.getPermissionsFromToken(token).getOrElse { emptyList() }
|
||||
|
||||
assertEquals(userId, extractedUserId)
|
||||
assertEquals(2, extractedPermissions.size)
|
||||
assertTrue(extractedPermissions.containsAll(permissions))
|
||||
|
||||
println("[DEBUG_LOG] End-to-end test completed successfully")
|
||||
println("[DEBUG_LOG] Token validation: ${isValid.isSuccess}")
|
||||
println("[DEBUG_LOG] Extracted user: $extractedUserId")
|
||||
println("[DEBUG_LOG] Extracted permissions: $extractedPermissions")
|
||||
println("[DEBUG_LOG] Auth-related beans: ${beanNames.filter { it.contains("auth") }}")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -217,48 +73,4 @@ class AuthServerIntegrationTest {
|
||||
println("[DEBUG_LOG] Total bean count: $beanCount")
|
||||
println("[DEBUG_LOG] Web-related beans: $webBeans")
|
||||
}
|
||||
|
||||
// ========== Service Functionality Tests ==========
|
||||
|
||||
@Test
|
||||
fun `JWT service should handle different permission combinations`() {
|
||||
// Test various permission combinations
|
||||
val testCases = listOf(
|
||||
emptyList(),
|
||||
listOf(BerechtigungE.PERSON_READ),
|
||||
listOf(BerechtigungE.PERSON_READ, BerechtigungE.PERSON_CREATE),
|
||||
BerechtigungE.entries
|
||||
)
|
||||
|
||||
testCases.forEach { permissions ->
|
||||
val token = jwtService.generateToken("test-user", "test", permissions)
|
||||
val validationResult = jwtService.validateToken(token)
|
||||
val extractedPermissions = jwtService.getPermissionsFromToken(token).getOrElse { emptyList() }
|
||||
|
||||
assertTrue(validationResult.isSuccess)
|
||||
assertEquals(permissions.size, extractedPermissions.size)
|
||||
assertTrue(extractedPermissions.containsAll(permissions))
|
||||
}
|
||||
|
||||
println("[DEBUG_LOG] Different permission combinations handled correctly")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JWT service should be thread-safe for concurrent access`() {
|
||||
// Test concurrent token operations
|
||||
val threads = (1..5).map { threadIndex ->
|
||||
Thread {
|
||||
repeat(10) { iteration ->
|
||||
val token = jwtService.generateToken("user-$threadIndex-$iteration", "test", listOf(BerechtigungE.PERSON_READ))
|
||||
val isValid = jwtService.validateToken(token).isSuccess
|
||||
assertTrue(isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
threads.forEach { it.start() }
|
||||
threads.forEach { it.join() }
|
||||
|
||||
println("[DEBUG_LOG] Concurrent access test completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
+5
-21
@@ -1,29 +1,13 @@
|
||||
package at.mocode.infrastructure.auth.config
|
||||
|
||||
import at.mocode.infrastructure.auth.client.JwtService
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Primary
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Test configuration for Auth Server integration tests.
|
||||
* Provides minimal bean configuration needed for tests to run.
|
||||
*
|
||||
* Note: Custom JWT handling has been removed. Authentication is now fully handled
|
||||
* by Keycloak via OAuth2 Resource Server. This configuration class is kept as a
|
||||
* placeholder for future test-specific beans if needed.
|
||||
*/
|
||||
@TestConfiguration
|
||||
class AuthServerTestConfiguration {
|
||||
|
||||
/**
|
||||
* Provides a JwtService bean for testing with test-specific configuration.
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
fun testJwtService(): JwtService {
|
||||
return JwtService(
|
||||
secret = "test-secret-for-testing-only-at-least-512-bits-long-for-hmac512-algorithm",
|
||||
issuer = "test-issuer",
|
||||
audience = "test-audience",
|
||||
expiration = 60.minutes
|
||||
)
|
||||
}
|
||||
}
|
||||
class AuthServerTestConfiguration
|
||||
|
||||
@@ -19,7 +19,6 @@ dependencies {
|
||||
// === Core Dependencies ===
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.infrastructure.auth.authClient)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
// === GATEWAY-SPEZIFISCHE ABHÄNGIGKEITEN ===
|
||||
@@ -31,7 +30,6 @@ dependencies {
|
||||
implementation(libs.bundles.logging)
|
||||
implementation(libs.bundles.jackson.kotlin)
|
||||
implementation(project(":infrastructure:event-store:redis-event-store"))
|
||||
implementation(project(":infrastructure:event-store:redis-event-store"))
|
||||
|
||||
// === Test Dependencies ===
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
|
||||
@@ -26,16 +26,3 @@ keycloak:
|
||||
client-id: ${KEYCLOAK_CLIENT_ID:api-gateway}
|
||||
public-client: false
|
||||
bearer-only: true
|
||||
|
||||
# Gateway-spezifische Sicherheitskonfiguration
|
||||
gateway:
|
||||
security:
|
||||
jwt:
|
||||
# Enable JWT validation via Spring Security OAuth2 Resource Server
|
||||
enabled: true
|
||||
keycloak:
|
||||
# Custom JWT filter DISABLED - using Spring Security oauth2ResourceServer instead
|
||||
# This prevents duplicate authentication and ensures proper JWT signature validation
|
||||
enabled: false
|
||||
server-url: ${KEYCLOAK_SERVER_URL:http://keycloak:8080}
|
||||
realm: ${KEYCLOAK_REALM:meldestelle}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import org.springframework.test.context.TestPropertySource
|
||||
* Simplified integration test for Keycloak Gateway integration.
|
||||
* This test verifies that the Spring context can initialize properly with Keycloak configuration
|
||||
* without requiring actual Testcontainers, focusing on resolving the OAuth2 ResourceServer
|
||||
* auto-configuration timing issue.
|
||||
* autoconfiguration timing issue.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("keycloak-integration-test")
|
||||
|
||||
@@ -43,7 +43,6 @@ include(":platform:platform-testing")
|
||||
|
||||
// Infrastructure modules
|
||||
include(":infrastructure:gateway")
|
||||
include(":infrastructure:auth:auth-client")
|
||||
include(":infrastructure:auth:auth-server")
|
||||
include(":infrastructure:messaging:messaging-client")
|
||||
include(":infrastructure:messaging:messaging-config")
|
||||
|
||||
Reference in New Issue
Block a user