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()))
}
}
Why always me?