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:
infrastructure/auth/
├── auth-client/ # Wiederverwendbare Bibliothek für die JWT-Validierung
└── auth-server/ # Eigenständiger Service für Benutzerverwaltung & Token-Austausch
├── auth-client/ # Wiederverwendbare Bibliothek für die JWT-Validierung
└── auth-server/ # Eigenständiger Service für Benutzerverwaltung & Token-Austausch
### `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.
**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.
* **Rechte-Extraktion:** Extrahiert die Rollen und Berechtigungen des Benutzers aus dem validierten Token.
* **Security Context:** Befüllt den `SecurityContextHolder` von Spring, sodass in den Controllern einfach auf den authentifizierten Benutzer zugegriffen werden kann (z.B. mit `@AuthenticationPrincipal`).
* **JWT-Management:** Stellt einen `JwtService` zur Erstellung und Validierung von JSON Web Tokens bereit.
* **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.
* **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.
@@ -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.
**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.
* **Token-Endpunkte:** Kann Endpunkte für den Austausch oder die Erneuerung von Tokens bereitstellen (Token Exchange).
* **Zentraler Login-Punkt (Optional):** Kann als zentraler Punkt für Login-Weiterleitungen im OAuth2/OIDC-Flow dienen.
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`.
* **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:** Ist verantwortlich für das Ausstellen von Tokens nach einer erfolgreichen Authentifizierung.
* **Implementierung der `AuthenticationService`-Schnittstelle:** Enthält die konkrete Logik, die gegen Keycloak prüft, ob ein Benutzername und ein Passwort korrekt sind.
## Zusammenspiel im System
1. Ein **Benutzer** meldet sich über eine Client-Anwendung an und erhält ein JWT von **Keycloak**.
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.
3. Der **Microservice**, der `auth-client` als Abhängigkeit hat, validiert das Token automatisch.
4. Wenn der Microservice Benutzerdaten ändern muss, ruft er nicht direkt Keycloak auf, sondern die sichere REST-API des **`auth-server`**.
1. Ein **Benutzer** meldet sich über eine Client-Anwendung am **`auth-server`** an.
2. Der **`auth-server`** validiert die Anmeldedaten gegen **Keycloak**.
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. 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.
---
**Letzte Aktualisierung**: 31. Juli 2025
**Letzte Aktualisierung**: 9. August 2025
@@ -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())
}
}