refactor(infra-auth): Relocate auth models and add JWT tests

This commit resolves the build failures caused by the refactoring of the `core` module and significantly improves the quality of the `auth-client` module.

### Architectural Refinements

- **Relocated Auth Enums:** The `RolleE` and `BerechtigungE` enums have been moved from the `core` module to their correct logical home within `:infrastructure:auth:auth-client`. The `auth-client` is now the single source of truth for authorization models, making it a self-contained and more coherent module.
- **Improved Type Safety:** The `AuthenticationService` interface and its DTOs now use the type-safe `BerechtigungE` enum instead of raw `List<String>`, improving consistency and reducing the risk of runtime errors.
- The `JwtService` now uses `kotlin.time.Duration` for token expiration, aligning it with project-wide best practices.

### Testing Enhancements

- **Added JWT Service Tests:** Introduced a comprehensive `JwtServiceTest` suite.
- The tests cover token generation, validation (including successful, invalid secret, and expired token scenarios), and the correct extraction of claims like user ID and permissions.
- This ensures the reliability and security of our core authentication mechanism.
This commit is contained in:
2025-08-09 19:35:53 +02:00
parent 2896f1f752
commit f9927066a2
5 changed files with 168 additions and 54 deletions
@@ -1,5 +1,6 @@
package at.mocode.infrastructure.auth.client
import at.mocode.infrastructure.auth.client.model.BerechtigungE
import com.benasher44.uuid.Uuid
import java.time.LocalDateTime
@@ -58,7 +59,7 @@ interface AuthenticationService {
*/
sealed class PasswordChangeResult {
/**
* Password change was successful.
* The password change was successful.
*/
object Success : PasswordChangeResult()
@@ -83,6 +84,6 @@ interface AuthenticationService {
val personId: Uuid,
val username: String,
val email: String,
val permissions: List<String>
val permissions: List<BerechtigungE>
)
}
@@ -1,9 +1,11 @@
package at.mocode.infrastructure.auth.client
import at.mocode.core.domain.model.BerechtigungE
import at.mocode.infrastructure.auth.client.model.BerechtigungE
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import java.util.*
import java.util.Date
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
/**
* Service for JWT token generation and validation.
@@ -12,16 +14,14 @@ class JwtService(
private val secret: String,
private val issuer: String,
private val audience: String,
private val expirationInMinutes: Long = 60
private val expiration: Duration = 60.minutes
) {
/**
* Generates a JWT token for the given user.
*
* @param userId The user ID
* @param username The username
* @param permissions The user's permissions
* @return The generated JWT token
*/
private val algorithm = Algorithm.HMAC512(secret)
private val verifier = JWT.require(algorithm)
.withIssuer(issuer)
.withAudience(audience)
.build()
fun generateToken(
userId: String,
username: String,
@@ -33,8 +33,8 @@ class JwtService(
.withAudience(audience)
.withClaim("username", username)
.withArrayClaim("permissions", permissions.map { it.name }.toTypedArray())
.withExpiresAt(Date(System.currentTimeMillis() + expirationInMinutes * 60 * 1000))
.sign(Algorithm.HMAC512(secret))
.withExpiresAt(Date(System.currentTimeMillis() + expiration.inWholeMilliseconds))
.sign(algorithm)
}
/**
@@ -45,13 +45,9 @@ class JwtService(
*/
fun validateToken(token: String): Boolean {
return try {
JWT.require(Algorithm.HMAC512(secret))
.withIssuer(issuer)
.withAudience(audience)
.build()
.verify(token)
verifier.verify(token)
true
} catch (e: Exception) {
} catch (_: Exception) {
false
}
}
@@ -64,13 +60,8 @@ class JwtService(
*/
fun getUserIdFromToken(token: String): String? {
return try {
JWT.require(Algorithm.HMAC512(secret))
.withIssuer(issuer)
.withAudience(audience)
.build()
.verify(token)
.subject
} catch (e: Exception) {
verifier.verify(token).subject
} catch (_: Exception) {
null
}
}
@@ -83,21 +74,16 @@ class JwtService(
*/
fun getPermissionsFromToken(token: String): List<BerechtigungE> {
return try {
val decodedJWT = JWT.require(Algorithm.HMAC512(secret))
.withIssuer(issuer)
.withAudience(audience)
.build()
.verify(token)
val decodedJWT = verifier.verify(token)
val permissionStrings = decodedJWT.getClaim("permissions").asArray(String::class.java)
permissionStrings.mapNotNull {
permissionStrings?.mapNotNull {
try {
BerechtigungE.valueOf(it)
} catch (e: Exception) {
null
}
}
} catch (e: Exception) {
} ?: emptyList()
} catch (_: Exception) {
emptyList()
}
}
@@ -0,0 +1,49 @@
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
}
@@ -0,0 +1,81 @@
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))
assertEquals(userId, jwtService.getUserIdFromToken(token))
val extractedPermissions = jwtService.getPermissionsFromToken(token)
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", testIssuer, testAudience)
val token = otherService.generateToken("user-123", "test", emptyList())
// Act & Assert
assertFalse(jwtService.validateToken(token))
}
@Test
fun `validateToken should return false for expired token`() {
// Arrange
val expiredService =
JwtService(testSecret, testIssuer, testAudience, expiration = (-1).seconds) // läuft sofort ab
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))
}
@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)
// Assert
assertTrue(permissions.isEmpty())
}
}