Authorization

- add SecurityConfiguration
- add JwtAuthenticationManager
- add JwtAuthenticationConverter
- add JwtAuthenticationToken
- add JwtUserDetails
This commit is contained in:
2025-01-01 20:33:09 +01:00
parent 7f87c00dd9
commit 1aee67d51c
5 changed files with 178 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
package ltd.hlaeja.configuration
import ltd.hlaeja.security.JwtAuthenticationConverter
import ltd.hlaeja.security.JwtAuthenticationManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.SecurityWebFiltersOrder.AUTHENTICATION
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec
import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec
import org.springframework.security.config.web.server.ServerHttpSecurity.FormLoginSpec
import org.springframework.security.config.web.server.ServerHttpSecurity.HttpBasicSpec
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.AuthenticationWebFilter
@Configuration
@EnableWebFluxSecurity
class SecurityConfiguration {
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean
fun securityWebFilterChain(
serverHttpSecurity: ServerHttpSecurity,
jwtAuthenticationManager: JwtAuthenticationManager,
jwtAuthenticationConverter: JwtAuthenticationConverter,
): SecurityWebFilterChain = serverHttpSecurity
.authorizeExchange(::authorizeExchange)
.httpBasic(::httpBasic)
.formLogin(::formLogin)
.csrf(::csrf)
.addFilterAt(
AuthenticationWebFilter(jwtAuthenticationManager).apply {
setServerAuthenticationConverter(jwtAuthenticationConverter)
},
AUTHENTICATION,
)
.build()
private fun csrf(
csrf: CsrfSpec,
) = csrf.disable()
private fun formLogin(
formLogin: FormLoginSpec,
) = formLogin.disable()
private fun httpBasic(
httpBasic: HttpBasicSpec,
) = httpBasic.disable()
private fun authorizeExchange(
authorizeExchange: AuthorizeExchangeSpec,
) = authorizeExchange
.pathMatchers("/login").permitAll()
.anyExchange().hasRole("REGISTRY")
}

View File

@@ -0,0 +1,55 @@
package ltd.hlaeja.security
import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.JwtException
import java.util.UUID
import ltd.hlaeja.jwt.service.PublicJwtService
import org.springframework.http.HttpStatus.UNAUTHORIZED
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
import org.springframework.stereotype.Component
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {}
@Component
class JwtAuthenticationConverter(
private val publicJwtService: PublicJwtService,
) : ServerAuthenticationConverter {
companion object {
private const val BEARER = "Bearer "
private const val AUTHORIZATION = "Authorization"
}
override fun convert(
exchange: ServerWebExchange,
): Mono<Authentication> = Mono.justOrEmpty(exchange.request.headers.getFirst(AUTHORIZATION))
.filter { it.startsWith(BEARER) }
.map { it.removePrefix(BEARER) }
.flatMap { token ->
try {
Mono.just(jwtAuthenticationToken(token))
} catch (e: JwtException) {
log.error(e) { "${e.message}" }
Mono.error(ResponseStatusException(UNAUTHORIZED))
}
}
private fun jwtAuthenticationToken(token: String) = publicJwtService.verify(token) { claims ->
JwtAuthenticationToken(
JwtUserDetails(
UUID.fromString(claims.payload["id"] as String),
claims.payload["username"] as String,
),
token,
(claims.payload["role"] as String).split(",")
.map { SimpleGrantedAuthority(it) }
.toMutableList(),
true,
)
}
}

View File

@@ -0,0 +1,27 @@
package ltd.hlaeja.security
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
@Component
class JwtAuthenticationManager : ReactiveAuthenticationManager {
override fun authenticate(
authentication: Authentication,
): Mono<Authentication> = if (authentication is JwtAuthenticationToken) {
handleJwtToken(authentication)
} else {
Mono.error(object : AuthenticationException("Unsupported authentication type") {})
}
private fun handleJwtToken(
token: JwtAuthenticationToken,
): Mono<Authentication> = if (token.isAuthenticated) {
Mono.just(token)
} else {
Mono.error(object : AuthenticationException("Invalid or expired JWT token") {})
}
}

View File

@@ -0,0 +1,28 @@
package ltd.hlaeja.security
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
data class JwtAuthenticationToken(
private val jwtUserDetails: JwtUserDetails,
private val token: String,
private var authorities: MutableCollection<out GrantedAuthority>,
private var authenticated: Boolean = false,
) : Authentication {
override fun getName(): String = "Bearer Token"
override fun getAuthorities(): MutableCollection<out GrantedAuthority> = authorities
override fun getCredentials(): Any = token
override fun getDetails(): Any? = null
override fun getPrincipal(): Any = jwtUserDetails
override fun isAuthenticated(): Boolean = authenticated
override fun setAuthenticated(isAuthenticated: Boolean) {
authenticated = isAuthenticated
}
}

View File

@@ -0,0 +1,8 @@
package ltd.hlaeja.security
import java.util.UUID
data class JwtUserDetails(
val id: UUID,
val username: String,
)