From 4b01c7d9e6e9e959ac877a02219a85b5781cd58b Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Thu, 21 Jul 2022 22:28:23 +0200 Subject: [PATCH] Handle HTTP Gone responses from Hydra This is a common error if a user resubmits a form. --- .../userservice/domain/UserService.kt | 37 +++++++++++++++++-- .../security/RequiredRoleAndScope.kt | 1 + .../hydra/model/RequestWasHandledResponse.kt | 23 ++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/sh/ory/hydra/model/RequestWasHandledResponse.kt diff --git a/src/main/kotlin/com/faforever/userservice/domain/UserService.kt b/src/main/kotlin/com/faforever/userservice/domain/UserService.kt index 2571a770..a7e11bea 100644 --- a/src/main/kotlin/com/faforever/userservice/domain/UserService.kt +++ b/src/main/kotlin/com/faforever/userservice/domain/UserService.kt @@ -4,12 +4,15 @@ import com.faforever.userservice.hydra.HydraService import com.faforever.userservice.security.OAuthScope import io.micronaut.context.annotation.ConfigurationProperties import io.micronaut.context.annotation.Context +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.uri.UriBuilder import jakarta.inject.Singleton import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.security.crypto.password.PasswordEncoder import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.onErrorResume import reactor.kotlin.core.publisher.switchIfEmpty import reactor.kotlin.core.publisher.toMono import reactor.kotlin.core.util.function.component1 @@ -19,6 +22,7 @@ import sh.ory.hydra.model.AcceptLoginRequest import sh.ory.hydra.model.ConsentRequestSession import sh.ory.hydra.model.GenericError import sh.ory.hydra.model.LoginRequest +import sh.ory.hydra.model.RequestWasHandledResponse import java.time.LocalDateTime import java.time.OffsetDateTime import javax.validation.constraints.NotNull @@ -71,7 +75,10 @@ class UserService( fun findUserBySubject(subject: String) = userRepository.findById(subject.toInt()) - private fun checkLoginThrottlingRequired(ip: String) = loginLogRepository.findFailedAttemptsByIpAfterDate(ip, LocalDateTime.now().minusDays(securityProperties.failedLoginDaysToCheck)) + private fun checkLoginThrottlingRequired(ip: String) = loginLogRepository.findFailedAttemptsByIpAfterDate( + ip, + LocalDateTime.now().minusDays(securityProperties.failedLoginDaysToCheck) + ) .map { val accountsAffected = it.accountsAffected ?: 0 val totalFailedAttempts = it.totalAttempts ?: 0 @@ -112,8 +119,12 @@ class UserService( internalLogin(challenge, usernameOrEmail, password, ip, loginRequest) } } - } - .onErrorResume { error -> + }.onErrorResume(HttpClientResponseException::class) { exception -> + handleOryGoneRedirect(exception) { redirectTo -> + LOG.debug("Login challenge $challenge was already solved, following Ory redirect") + LoginResult.SuccessfulLogin(redirectTo) + } + }.onErrorResume { error -> LOG.debug("Login failed with technical error for challenge $challenge", error) LoginResult.TechnicalError.toMono() } @@ -233,5 +244,25 @@ class UserService( } }.map { it.redirectTo + }.onErrorResume(HttpClientResponseException::class) { exception -> + handleOryGoneRedirect(exception) { redirectTo -> + LOG.debug("Consent challenge $challenge was already solved, following Ory redirect") + redirectTo + } } + + private fun handleOryGoneRedirect( + exception: HttpClientResponseException, + redirectMapper: (String) -> T + ): Mono = + if (exception.status == HttpStatus.GONE) { + val mappedResponse = exception.response.getBody(RequestWasHandledResponse::class.java) + .map { redirectMapper(it.redirectTo) } + .orElseThrow() + + Mono.just(mappedResponse) + } else { + // pass through unknown error + Mono.error(exception) + } } diff --git a/src/main/kotlin/com/faforever/userservice/security/RequiredRoleAndScope.kt b/src/main/kotlin/com/faforever/userservice/security/RequiredRoleAndScope.kt index 994e0b7e..ddb9e311 100644 --- a/src/main/kotlin/com/faforever/userservice/security/RequiredRoleAndScope.kt +++ b/src/main/kotlin/com/faforever/userservice/security/RequiredRoleAndScope.kt @@ -32,6 +32,7 @@ class RequiredRoleAndScopeRule : SecurityRule { authentication: Authentication? ): Publisher { if (authentication == null) { + LOG.debug("No authentication available") return SecurityRuleResult.REJECTED.toMono() } diff --git a/src/main/kotlin/sh/ory/hydra/model/RequestWasHandledResponse.kt b/src/main/kotlin/sh/ory/hydra/model/RequestWasHandledResponse.kt new file mode 100644 index 00000000..68106475 --- /dev/null +++ b/src/main/kotlin/sh/ory/hydra/model/RequestWasHandledResponse.kt @@ -0,0 +1,23 @@ +/** +* ORY Hydra +* Welcome to the ORY Hydra HTTP API documentation. You will find documentation for all HTTP APIs here. +* +* The version of the OpenAPI document: latest +* * +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package sh.ory.hydra.model + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * * @param redirectTo Original request URL to which you should redirect the user if request was already handled. + */ + +data class RequestWasHandledResponse( + /* Original request URL to which you should redirect the user if request was already handled. */ + @field:JsonProperty("redirect_to") + val redirectTo: kotlin.String +)