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
+14 -17
View File
@@ -10,10 +10,9 @@ Als Identity Provider wird **Keycloak** verwendet. Dieses Modul kapselt die gesa
Das Auth-Modul ist in zwei spezialisierte Komponenten aufgeteilt, um eine klare Trennung der Verantwortlichkeiten zu gewährleisten: Das Auth-Modul ist in zwei spezialisierte Komponenten aufgeteilt, um eine klare Trennung der Verantwortlichkeiten zu gewährleisten:
infrastructure/auth/ infrastructure/auth/
├── auth-client/ # Wiederverwendbare Bibliothek für die JWT-Validierung ├── auth-client/ # Wiederverwendbare Bibliothek für die JWT-Validierung
└── auth-server/ # Eigenständiger Service für Benutzerverwaltung & Token-Austausch └── auth-server/ # Eigenständiger Service für Benutzerverwaltung & Token-Austausch
### `auth-client` ### `auth-client`
@@ -21,9 +20,9 @@ infrastructure/auth/
Dieses Modul ist eine **wiederverwendbare Bibliothek** und kein eigenständiger Service. Es enthält die gesamte Logik, die andere Microservices (wie `masterdata-service`, `members-service` etc.) benötigen, um ihre Endpunkte abzusichern. Dieses Modul ist eine **wiederverwendbare Bibliothek** und kein eigenständiger Service. Es enthält die gesamte Logik, die andere Microservices (wie `masterdata-service`, `members-service` etc.) benötigen, um ihre Endpunkte abzusichern.
**Hauptaufgaben:** **Hauptaufgaben:**
* **JWT-Validierung:** Stellt Spring Security Konfigurationen bereit, um eingehende JWTs (ausgestellt von Keycloak) zu validieren. Es prüft die Signatur, den Aussteller (Issuer) und die Gültigkeitsdauer des Tokens. * **JWT-Management:** Stellt einen `JwtService` zur Erstellung und Validierung von JSON Web Tokens bereit.
* **Rechte-Extraktion:** Extrahiert die Rollen und Berechtigungen des Benutzers aus dem validierten Token. * **Modell-Definition:** Definiert die **Quelle der Wahrheit** für sicherheitsrelevante Konzepte wie `RolleE` und `BerechtigungE` als typsichere Kotlin-Enums. Dies stellt sicher, dass alle Services dieselbe "Sprache" für Berechtigungen sprechen.
* **Security Context:** Befüllt den `SecurityContextHolder` von Spring, sodass in den Controllern einfach auf den authentifizierten Benutzer zugegriffen werden kann (z.B. mit `@AuthenticationPrincipal`). * **Schnittstellen:** Bietet saubere Schnittstellen wie `AuthenticationService` an, die von der konkreten Implementierung (z.B. Keycloak) abstrahieren.
Jeder Microservice, der geschützte Endpunkte anbietet, bindet dieses Modul als Abhängigkeit ein. Jeder Microservice, der geschützte Endpunkte anbietet, bindet dieses Modul als Abhängigkeit ein.
@@ -32,20 +31,18 @@ Jeder Microservice, der geschützte Endpunkte anbietet, bindet dieses Modul als
Dies ist ein **eigenständiger Spring Boot Microservice**, der als Brücke zwischen dem Meldestelle-System und Keycloak agiert. Dies ist ein **eigenständiger Spring Boot Microservice**, der als Brücke zwischen dem Meldestelle-System und Keycloak agiert.
**Hauptaufgaben:** **Hauptaufgaben:**
* **Benutzer-API:** Stellt eine REST-API zur Verfügung, um Benutzer zu verwalten (z.B. Registrierung, Profil-Updates). Diese API kommuniziert im Hintergrund über den `keycloak-admin-client` mit Keycloak. * **Benutzer-API:** Stellt eine REST-API zur Verfügung, um Benutzer zu verwalten (z.B. Registrierung). Diese API kommuniziert im Hintergrund über den `keycloak-admin-client` mit Keycloak.
* **Token-Endpunkte:** Kann Endpunkte für den Austausch oder die Erneuerung von Tokens bereitstellen (Token Exchange). * **Token-Endpunkte:** Ist verantwortlich für das Ausstellen von Tokens nach einer erfolgreichen Authentifizierung.
* **Zentraler Login-Punkt (Optional):** Kann als zentraler Punkt für Login-Weiterleitungen im OAuth2/OIDC-Flow dienen. * **Implementierung der `AuthenticationService`-Schnittstelle:** Enthält die konkrete Logik, die gegen Keycloak prüft, ob ein Benutzername und ein Passwort korrekt sind.
Durch die Kapselung der Keycloak-spezifischen Logik im `auth-server` müssen die anderen Fach-Services nichts über die Interna der Benutzerverwaltung wissen. Sie interagieren nur mit der sauberen API des `auth-server`.
## Zusammenspiel im System ## Zusammenspiel im System
1. Ein **Benutzer** meldet sich über eine Client-Anwendung an und erhält ein JWT von **Keycloak**. 1. Ein **Benutzer** meldet sich über eine Client-Anwendung am **`auth-server`** an.
2. Die **Client-Anwendung** sendet eine Anfrage an einen Microservice (z.B. `masterdata-service`) und fügt das JWT als Bearer-Token in den `Authorization`-Header ein. 2. Der **`auth-server`** validiert die Anmeldedaten gegen **Keycloak**.
3. Der **Microservice**, der `auth-client` als Abhängigkeit hat, validiert das Token automatisch. 3. Bei Erfolg erstellt der `auth-server` mit dem `JwtService` aus dem `auth-client` ein JWT, das die Berechtigungen des Benutzers enthält, und sendet es an den Client zurück.
4. Wenn der Microservice Benutzerdaten ändern muss, ruft er nicht direkt Keycloak auf, sondern die sichere REST-API des **`auth-server`**. 4. Der **Client** sendet eine Anfrage an einen anderen Microservice (z.B. `members-service`) und fügt das JWT als Bearer-Token in den Header ein.
5. Der **`members-service`**, der ebenfalls den `auth-client` als Abhängigkeit hat, nutzt den `JwtService`, um das Token zu validieren und die Berechtigungen typsicher auszulesen.
Diese Architektur entkoppelt die Fach-Services von der Komplexität der Identitätsverwaltung und schafft eine robuste, zentrale Sicherheitsinfrastruktur. Diese Architektur entkoppelt die Fach-Services von der Komplexität der Identitätsverwaltung und schafft eine robuste, zentrale Sicherheitsinfrastruktur.
--- ---
**Letzte Aktualisierung**: 31. Juli 2025 **Letzte Aktualisierung**: 9. August 2025
@@ -1,5 +1,6 @@
package at.mocode.infrastructure.auth.client package at.mocode.infrastructure.auth.client
import at.mocode.infrastructure.auth.client.model.BerechtigungE
import com.benasher44.uuid.Uuid import com.benasher44.uuid.Uuid
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -58,7 +59,7 @@ interface AuthenticationService {
*/ */
sealed class PasswordChangeResult { sealed class PasswordChangeResult {
/** /**
* Password change was successful. * The password change was successful.
*/ */
object Success : PasswordChangeResult() object Success : PasswordChangeResult()
@@ -83,6 +84,6 @@ interface AuthenticationService {
val personId: Uuid, val personId: Uuid,
val username: String, val username: String,
val email: String, val email: String,
val permissions: List<String> val permissions: List<BerechtigungE>
) )
} }
@@ -1,9 +1,11 @@
package at.mocode.infrastructure.auth.client 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.JWT
import com.auth0.jwt.algorithms.Algorithm 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. * Service for JWT token generation and validation.
@@ -12,16 +14,14 @@ class JwtService(
private val secret: String, private val secret: String,
private val issuer: String, private val issuer: String,
private val audience: String, private val audience: String,
private val expirationInMinutes: Long = 60 private val expiration: Duration = 60.minutes
) { ) {
/** private val algorithm = Algorithm.HMAC512(secret)
* Generates a JWT token for the given user. private val verifier = JWT.require(algorithm)
* .withIssuer(issuer)
* @param userId The user ID .withAudience(audience)
* @param username The username .build()
* @param permissions The user's permissions
* @return The generated JWT token
*/
fun generateToken( fun generateToken(
userId: String, userId: String,
username: String, username: String,
@@ -33,8 +33,8 @@ class JwtService(
.withAudience(audience) .withAudience(audience)
.withClaim("username", username) .withClaim("username", username)
.withArrayClaim("permissions", permissions.map { it.name }.toTypedArray()) .withArrayClaim("permissions", permissions.map { it.name }.toTypedArray())
.withExpiresAt(Date(System.currentTimeMillis() + expirationInMinutes * 60 * 1000)) .withExpiresAt(Date(System.currentTimeMillis() + expiration.inWholeMilliseconds))
.sign(Algorithm.HMAC512(secret)) .sign(algorithm)
} }
/** /**
@@ -45,13 +45,9 @@ class JwtService(
*/ */
fun validateToken(token: String): Boolean { fun validateToken(token: String): Boolean {
return try { return try {
JWT.require(Algorithm.HMAC512(secret)) verifier.verify(token)
.withIssuer(issuer)
.withAudience(audience)
.build()
.verify(token)
true true
} catch (e: Exception) { } catch (_: Exception) {
false false
} }
} }
@@ -64,13 +60,8 @@ class JwtService(
*/ */
fun getUserIdFromToken(token: String): String? { fun getUserIdFromToken(token: String): String? {
return try { return try {
JWT.require(Algorithm.HMAC512(secret)) verifier.verify(token).subject
.withIssuer(issuer) } catch (_: Exception) {
.withAudience(audience)
.build()
.verify(token)
.subject
} catch (e: Exception) {
null null
} }
} }
@@ -83,21 +74,16 @@ class JwtService(
*/ */
fun getPermissionsFromToken(token: String): List<BerechtigungE> { fun getPermissionsFromToken(token: String): List<BerechtigungE> {
return try { return try {
val decodedJWT = JWT.require(Algorithm.HMAC512(secret)) val decodedJWT = verifier.verify(token)
.withIssuer(issuer)
.withAudience(audience)
.build()
.verify(token)
val permissionStrings = decodedJWT.getClaim("permissions").asArray(String::class.java) val permissionStrings = decodedJWT.getClaim("permissions").asArray(String::class.java)
permissionStrings.mapNotNull { permissionStrings?.mapNotNull {
try { try {
BerechtigungE.valueOf(it) BerechtigungE.valueOf(it)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} } ?: emptyList()
} catch (e: Exception) { } catch (_: Exception) {
emptyList() 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())
}
}