Add remote authentication for users

- add RemoteReactiveAuthenticationManager
- add RemoteAuthentication
- add RemoteUserDetail
- add AccountRegistryService
- add WebClientCalls.kt with accountRegistryAuthenticate
- add Helper.kt with logCall
- add Mapping.kt toAuthenticationRequest
This commit is contained in:
2025-01-17 13:46:52 +01:00
parent 7cf39926d7
commit 9f6d7066b7
7 changed files with 186 additions and 0 deletions

View File

@@ -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<out GrantedAuthority>,
private var authenticated: Boolean = false,
) : Authentication {
override fun getName(): String = "Hlaeja Account Registry"
override fun getAuthorities(): MutableCollection<out GrantedAuthority> = 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
}
}

View File

@@ -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<Authentication> = 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<Claims>,
) = RemoteAuthentication(
makeRemoteUserDetail(claims),
makeSimpleGrantedAuthorities(claims),
true,
)
private fun makeSimpleGrantedAuthorities(
claims: Jws<Claims>,
): MutableList<SimpleGrantedAuthority> = (claims.payload["role"] as String)
.split(",")
.map { SimpleGrantedAuthority(it) }
.toMutableList()
private fun makeRemoteUserDetail(
claims: Jws<Claims>,
): RemoteUserDetail = RemoteUserDetail(
UUID.fromString(claims.payload["id"] as String),
claims.payload["username"] as String,
)
}

View File

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

View File

@@ -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<Authentication.Response> = 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))
}
}
}
}

View File

@@ -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" }

View File

@@ -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,
)

View File

@@ -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<Authentication.Response> = 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)