diff --git a/src/main/kotlin/ltd/hlaeja/security/RemoteAuthentication.kt b/src/main/kotlin/ltd/hlaeja/security/RemoteAuthentication.kt new file mode 100644 index 0000000..b37438f --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/security/RemoteAuthentication.kt @@ -0,0 +1,27 @@ +package ltd.hlaeja.security + +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority + +data class RemoteAuthentication( + private val remoteUserDetail: RemoteUserDetail, + private var authorities: MutableCollection, + private var authenticated: Boolean = false, +) : Authentication { + + override fun getName(): String = "Hlaeja Account Registry" + + override fun getAuthorities(): MutableCollection = authorities + + override fun getCredentials(): Any? = null + + override fun getDetails(): Any? = null + + override fun getPrincipal(): Any = remoteUserDetail + + override fun isAuthenticated(): Boolean = authenticated + + override fun setAuthenticated(isAuthenticated: Boolean) { + authenticated = isAuthenticated + } +} diff --git a/src/main/kotlin/ltd/hlaeja/security/RemoteReactiveAuthenticationManager.kt b/src/main/kotlin/ltd/hlaeja/security/RemoteReactiveAuthenticationManager.kt new file mode 100644 index 0000000..d4d08b3 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/security/RemoteReactiveAuthenticationManager.kt @@ -0,0 +1,64 @@ +package ltd.hlaeja.security + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jws +import io.jsonwebtoken.JwtException +import java.util.UUID +import ltd.hlaeja.jwt.service.PublicJwtService +import ltd.hlaeja.library.accountRegistry.Authentication.Response +import ltd.hlaeja.service.AccountRegistryService +import ltd.hlaeja.util.toAuthenticationRequest +import org.springframework.security.authentication.AuthenticationServiceException +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.stereotype.Component +import reactor.core.publisher.Mono + +private val log = KotlinLogging.logger {} + +@Component +class RemoteReactiveAuthenticationManager( + private val accountRegistryService: AccountRegistryService, + private val publicJwtService: PublicJwtService, +) : ReactiveAuthenticationManager { + + override fun authenticate( + authentication: Authentication, + ): Mono = accountRegistryService.authenticate(authentication.toAuthenticationRequest()) + .map(::processToken) + + private fun processToken( + response: Response, + ): Authentication = try { + publicJwtService.verify(response.token) { claims -> makeRemoteAuthentication(claims) } + } catch (e: JwtException) { + "An error occurred while processing token: ${e.message}".let { + log.error(e) { it } + throw AuthenticationServiceException(it, e) + } + } + + private fun makeRemoteAuthentication( + claims: Jws, + ) = RemoteAuthentication( + makeRemoteUserDetail(claims), + makeSimpleGrantedAuthorities(claims), + true, + ) + + private fun makeSimpleGrantedAuthorities( + claims: Jws, + ): MutableList = (claims.payload["role"] as String) + .split(",") + .map { SimpleGrantedAuthority(it) } + .toMutableList() + + private fun makeRemoteUserDetail( + claims: Jws, + ): RemoteUserDetail = RemoteUserDetail( + UUID.fromString(claims.payload["id"] as String), + claims.payload["username"] as String, + ) +} diff --git a/src/main/kotlin/ltd/hlaeja/security/RemoteUserDetail.kt b/src/main/kotlin/ltd/hlaeja/security/RemoteUserDetail.kt new file mode 100644 index 0000000..6089ddf --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/security/RemoteUserDetail.kt @@ -0,0 +1,8 @@ +package ltd.hlaeja.security + +import java.util.UUID + +data class RemoteUserDetail( + val id: UUID, + val username: String, +) diff --git a/src/main/kotlin/ltd/hlaeja/service/AccountRegistryService.kt b/src/main/kotlin/ltd/hlaeja/service/AccountRegistryService.kt new file mode 100644 index 0000000..0840a76 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/service/AccountRegistryService.kt @@ -0,0 +1,46 @@ +package ltd.hlaeja.service + +import io.github.oshai.kotlinlogging.KotlinLogging +import ltd.hlaeja.library.accountRegistry.Authentication +import ltd.hlaeja.property.AccountRegistryProperty +import ltd.hlaeja.util.accountRegistryAuthenticate +import org.springframework.security.authentication.AuthenticationServiceException +import org.springframework.security.core.AuthenticationException +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClientRequestException +import org.springframework.web.reactive.function.client.WebClientResponseException +import reactor.core.publisher.Mono + +private val log = KotlinLogging.logger {} + +@Service +class AccountRegistryService( + private val webClient: WebClient, + private val property: AccountRegistryProperty, +) { + + fun authenticate( + request: Authentication.Request, + ): Mono = webClient.accountRegistryAuthenticate(request, property) + .onErrorResume { error -> + when (error) { + is AuthenticationException -> Mono.error(error) + + is WebClientResponseException -> "WebClient response exception: ${error.message}".let { + log.error(error) { it } + Mono.error(AuthenticationServiceException(it, error)) + } + + is WebClientRequestException -> "An error occurred while making a request: ${error.message}".let { + log.error(error) { it } + Mono.error(AuthenticationServiceException(it, error)) + } + + else -> "An unexpected error occurred: ${error.message}".let { + log.error(error) { it } + Mono.error(AuthenticationServiceException(it, error)) + } + } + } +} diff --git a/src/main/kotlin/ltd/hlaeja/util/Helper.kt b/src/main/kotlin/ltd/hlaeja/util/Helper.kt new file mode 100644 index 0000000..64b7734 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/util/Helper.kt @@ -0,0 +1,7 @@ +package ltd.hlaeja.util + +import io.github.oshai.kotlinlogging.KotlinLogging + +private val log = KotlinLogging.logger {} + +fun logCall(url: String) = log.debug { "calling: $url" } diff --git a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt new file mode 100644 index 0000000..28978a9 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt @@ -0,0 +1,10 @@ +package ltd.hlaeja.util + +import org.springframework.security.core.Authentication as SpringAuthentication + +import ltd.hlaeja.library.accountRegistry.Authentication + +fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Authentication.Request( + principal as String, + credentials as String, +) diff --git a/src/main/kotlin/ltd/hlaeja/util/WebClientCalls.kt b/src/main/kotlin/ltd/hlaeja/util/WebClientCalls.kt new file mode 100644 index 0000000..f6c2963 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/util/WebClientCalls.kt @@ -0,0 +1,24 @@ +package ltd.hlaeja.util + +import ltd.hlaeja.library.accountRegistry.Authentication +import ltd.hlaeja.property.AccountRegistryProperty +import org.springframework.http.HttpStatus.LOCKED +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatus.UNAUTHORIZED +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.authentication.LockedException +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +fun WebClient.accountRegistryAuthenticate( + request: Authentication.Request, + property: AccountRegistryProperty, +): Mono = post() + .uri("${property.url}/authenticate".also(::logCall)) + .bodyValue(request) + .retrieve() + .onStatus(LOCKED::equals) { throw LockedException("Account is locked") } + .onStatus(UNAUTHORIZED::equals) { throw BadCredentialsException("Invalid credentials") } + .onStatus(NOT_FOUND::equals) { throw UsernameNotFoundException("User not found") } + .bodyToMono(Authentication.Response::class.java)