1. JWT Authentication Service

Create a new package com.endlessuphill.regent.service. Inside, create AuthService.kt:

// src/main/kotlin/com/example/regent/security/JwtSefvice.kt
package com.endlessuphill.regent.security
 
import com.endlessuphill.regent.config.JwtProperties
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import jakarta.annotation.PostConstruct
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import javax.crypto.SecretKey
import java.time.Instant
import java.util.Date
import java.util.UUID
 
@Service
class JwtService(private val jwtProperties: JwtProperties) {
 
    private val log = LoggerFactory.getLogger(JwtService::class.java)
    private lateinit var signingKey: SecretKey
 
    @PostConstruct
    fun init() {
        // Decode the base64 secret key AFTER properties are loaded
        try {
            val keyBytes = java.util.Base64.getDecoder().decode(jwtProperties.secret)
            this.signingKey = Keys.hmacShaKeyFor(keyBytes)
            log.info("JWT Signing Key initialized successfully.")
        } catch (e: IllegalArgumentException) {
            log.error("Invalid Base64 encoding for JWT secret key!", e)
            // Fail fast during startup if the key is invalid
            throw IllegalStateException("Invalid JWT secret key configuration", e)
        }
    }
 
    fun generateToken(userId: UUID, username: String): String {
        val now = Instant.now()
        val expirationTime = now.plus(jwtProperties.expiration)
 
        return Jwts.builder()
            .subject(username) // Standard claim for username
            .claim("userId", userId.toString()) // Custom claim for user ID
            .issuer(jwtProperties.issuer)
            .issuedAt(Date.from(now))
            .expiration(Date.from(expirationTime))
            .signWith(signingKey)
            .compact()
    }
 
    fun validateTokenAndGetClaims(token: String): Claims? {
        return try {
            Jwts.parser()
                .verifyWith(signingKey) // Use verifyWith for modern jjwt versions
                .requireIssuer(jwtProperties.issuer) // Validate issuer
                .build()
                .parseSignedClaims(token) // Renamed from parseClaimsJws
                .payload // Use payload to get Claims
        } catch (ex: ExpiredJwtException) {
            log.warn("JWT token is expired: {}", ex.message)
            null
        } catch (ex: JwtException) { // Catch broader JwtException for other issues (malformed, signature etc.)
            log.warn("JWT token validation failed: {}", ex.message)
            null
        } catch (ex: IllegalArgumentException) {
            log.error("JWT token validation error: {}", ex.message)
            null
        }
    }
 
    fun extractUserId(claims: Claims): UUID? {
        return try {
            UUID.fromString(claims.get("userId", String::class.java))
        } catch (e: Exception) { // Handle potential missing claim or parsing error
            log.error("Could not extract/parse userId claim: {}", e.message)
            null
        }
    }
 
    fun extractUsername(claims: Claims): String? {
        return claims.subject
    }
 
    // Expiration check is implicitly done by parseSignedClaims, but can be useful separately
    fun isTokenExpired(claims: Claims): Boolean {
        return claims.expiration.before(Date.from(Instant.now()))
    }
}

Next Step