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:
+3
-2
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+21
-35
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+49
@@ -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
|
||||
}
|
||||
+81
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user