refactoring(infra-auth)

This commit is contained in:
2025-08-14 21:21:53 +02:00
parent fde93093b9
commit fa04c16ece
21 changed files with 3031 additions and 44 deletions
@@ -61,7 +61,7 @@ interface AuthenticationService {
/**
* The password change was successful.
*/
object Success : PasswordChangeResult()
data object Success : PasswordChangeResult()
/**
* Password change failed.
@@ -73,7 +73,7 @@ interface AuthenticationService {
/**
* The new password is too weak.
*/
object WeakPassword : PasswordChangeResult()
data object WeakPassword : PasswordChangeResult()
}
/**
@@ -3,6 +3,8 @@ 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 mu.KotlinLogging
import java.util.Date
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
@@ -16,6 +18,8 @@ class JwtService(
private val audience: String,
private val expiration: Duration = 60.minutes
) {
private val logger = KotlinLogging.logger {}
private val algorithm = Algorithm.HMAC512(secret)
private val verifier = JWT.require(algorithm)
.withIssuer(issuer)
@@ -41,50 +45,107 @@ class JwtService(
* Validates a JWT token.
*
* @param token The JWT token to validate
* @return True if the token is valid, false otherwise
* @return Result with true if the token is valid, or failure with error details
*/
fun validateToken(token: String): Boolean {
fun validateToken(token: String): Result<Boolean> {
return try {
verifier.verify(token)
true
} catch (_: Exception) {
false
logger.debug { "JWT token validation successful" }
Result.success(true)
} catch (e: JWTVerificationException) {
logger.warn { "JWT token validation failed: ${e.message}" }
Result.failure(e)
} catch (e: Exception) {
logger.error(e) { "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
*/
fun getUserIdFromToken(token: String): String? {
return try {
verifier.verify(token).subject
} catch (_: Exception) {
null
}
@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 The permissions, or an empty list if the token is invalid
* @return Result with the permissions, or failure with error details
*/
fun getPermissionsFromToken(token: String): List<BerechtigungE> {
fun getPermissionsFromToken(token: String): Result<List<BerechtigungE>> {
return try {
val decodedJWT = verifier.verify(token)
val permissionStrings = decodedJWT.getClaim("permissions").asArray(String::class.java)
permissionStrings?.mapNotNull {
val permissions = permissionStrings?.mapNotNull { permissionString ->
try {
BerechtigungE.valueOf(it)
} catch (_: Exception) {
BerechtigungE.valueOf(permissionString)
} catch (_: IllegalArgumentException) {
logger.warn { "Unknown permission in JWT token: $permissionString" }
null
}
} ?: emptyList()
} catch (_: Exception) {
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() }
}
}
@@ -0,0 +1,379 @@
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 org.junit.jupiter.api.assertTimeoutPreemptively
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.
*/
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 10ms`() {
// Arrange
val token = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
// Act & Assert - Single validation should be very fast
repeat(100) {
val timeMs = measureTimeMillis {
val result = jwtService.validateToken(token)
assertTrue(result.isSuccess)
}
assertTrue(timeMs < 10, "JWT validation should complete under 10ms (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
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 * 0.5,
"Performance should be consistent (max deviation: ${maxDeviation}ms, avg: ${avgTime}ms)")
}
// ========== Token Generation Performance Tests ==========
@Test
fun `token generation should complete under 5ms`() {
// Arrange
val permissions = listOf(BerechtigungE.PERSON_READ, BerechtigungE.PFERD_CREATE, BerechtigungE.VEREIN_UPDATE)
// Act & Assert
repeat(100) {
val timeMs = measureTimeMillis {
val token = jwtService.generateToken("user-$it", "testuser$it", permissions)
assertNotNull(token)
assertTrue(token.isNotEmpty())
}
assertTrue(timeMs < 5, "Token generation should complete under 5ms (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 ==========
@Test
fun `should handle large permission sets efficiently`() {
// Arrange - Create a token with all available permissions
val allPermissions = BerechtigungE.entries
// Act & Assert - Generation should still be fast
val generationTime = measureTimeMillis {
val token = jwtService.generateToken("admin-user", "admin", allPermissions)
assertNotNull(token)
}
assertTrue(generationTime < 100, "Generation with all permissions should be under 100ms")
// 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 < 20, "Validation with all permissions should be under 20ms")
}
// ========== 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")
}
}
@@ -0,0 +1,339 @@
package at.mocode.infrastructure.auth.client
import at.mocode.infrastructure.auth.client.model.BerechtigungE
import com.benasher44.uuid.uuid4
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
/**
* 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 = uuid4()
private val testPersonId = uuid4()
@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 = uuid4()
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
when (successResult) {
is AuthenticationService.AuthResult.Success -> {
assertNotNull(successResult.token)
assertNotNull(successResult.user)
}
is AuthenticationService.AuthResult.Failure -> fail("Should not be failure")
is AuthenticationService.AuthResult.Locked -> fail("Should not be locked")
}
when (failureResult) {
is AuthenticationService.AuthResult.Success -> fail("Should not be success")
is AuthenticationService.AuthResult.Failure -> {
assertEquals("Failed", failureResult.reason)
}
is AuthenticationService.AuthResult.Locked -> fail("Should not be locked")
}
when (lockedResult) {
is AuthenticationService.AuthResult.Success -> fail("Should not be success")
is AuthenticationService.AuthResult.Failure -> fail("Should not be failure")
is AuthenticationService.AuthResult.Locked -> {
assertNotNull(lockedResult.lockedUntil)
}
}
}
@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
when (successResult) {
is AuthenticationService.PasswordChangeResult.Success -> {
// Success case - no additional data
assertTrue(true)
}
is AuthenticationService.PasswordChangeResult.Failure -> fail("Should not be failure")
is AuthenticationService.PasswordChangeResult.WeakPassword -> fail("Should not be weak password")
}
when (failureResult) {
is AuthenticationService.PasswordChangeResult.Success -> fail("Should not be success")
is AuthenticationService.PasswordChangeResult.Failure -> {
assertEquals("Failed", failureResult.reason)
}
is AuthenticationService.PasswordChangeResult.WeakPassword -> fail("Should not be weak password")
}
when (weakPasswordResult) {
is AuthenticationService.PasswordChangeResult.Success -> fail("Should not be success")
is AuthenticationService.PasswordChangeResult.Failure -> fail("Should not be failure")
is AuthenticationService.PasswordChangeResult.WeakPassword -> {
// Weak password case - no additional data
assertTrue(true)
}
}
}
}
@@ -0,0 +1,299 @@
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")
}
}
}
@@ -35,10 +35,10 @@ class JwtServiceTest {
// Assert
assertNotNull(token)
assertTrue(jwtService.validateToken(token))
assertEquals(userId, jwtService.getUserIdFromToken(token))
assertTrue(jwtService.validateToken(token).isSuccess)
assertEquals(userId, jwtService.getUserIdFromToken(token).getOrNull())
val extractedPermissions = jwtService.getPermissionsFromToken(token)
val extractedPermissions = jwtService.getPermissionsFromToken(token).getOrElse { emptyList() }
assertEquals(2, extractedPermissions.size)
assertTrue(extractedPermissions.contains(BerechtigungE.PERSON_READ))
assertTrue(extractedPermissions.contains(BerechtigungE.PFERD_CREATE))
@@ -51,20 +51,18 @@ class JwtServiceTest {
val token = otherService.generateToken("user-123", "test", emptyList())
// Act & Assert
assertFalse(jwtService.validateToken(token))
assertFalse(jwtService.validateToken(token).isSuccess)
}
@Test
fun `validateToken should return false for expired token`() {
// Arrange
val expiredService =
JwtService(testSecret, testIssuer, testAudience, expiration = (-1).seconds) // läuft sofort ab
JwtService(testSecret, testIssuer, testAudience, expiration = (-10).seconds) // bereits abgelaufen
val token = expiredService.generateToken("user-123", "test", emptyList())
// Act & Assert
// möglicherweise ist eine kleine Verzögerung nötig, um sicherzustellen, dass die Zeitstempel unterschiedlich sind
Thread.sleep(10)
assertFalse(jwtService.validateToken(token))
assertFalse(jwtService.validateToken(token).isSuccess)
}
@Test
@@ -73,7 +71,7 @@ class JwtServiceTest {
val invalidToken = "this.is.not.a.valid.token"
// Act
val permissions = jwtService.getPermissionsFromToken(invalidToken)
val permissions = jwtService.getPermissionsFromToken(invalidToken).getOrElse { emptyList() }
// Assert
assertTrue(permissions.isEmpty())
@@ -0,0 +1,331 @@
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)
}
}
@@ -0,0 +1,345 @@
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 java.time.Duration
import kotlin.time.Duration.Companion.minutes
/**
* Security-focused tests for JWT handling.
* Tests against common JWT vulnerabilities and security attack vectors.
*/
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
fun `should reject tokens with tampered signatures`() {
// Arrange
val validToken = jwtService.generateToken("user-123", "testuser", listOf(BerechtigungE.PERSON_READ))
val tokenParts = validToken.split(".")
// Tamper with the signature by changing the last character
val tamperedSignature = tokenParts[2].dropLast(1) + "X"
val tamperedToken = "${tokenParts[0]}.${tokenParts[1]}.$tamperedSignature"
// Act
val result = jwtService.validateToken(tamperedToken)
// Assert
assertTrue(result.isFailure)
assertInstanceOf(JWTVerificationException::class.java, result.exceptionOrNull())
}
@Test
fun `should reject tokens with completely different signatures`() {
// Arrange
val validToken = jwtService.generateToken("user-123", "testuser", emptyList())
val anotherValidToken = jwtService.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 = jwtService.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 < 10_000_000, "Token validation should complete within 10ms (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
fun `should handle concurrent validation requests safely`() {
// Test thread safety of JWT validation
val token = jwtService.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")
}
}