Skip to content

Commit 5ee0509

Browse files
committed
Refactor flow to reduce exceptions
1 parent 7cef09c commit 5ee0509

File tree

8 files changed

+105
-90
lines changed

8 files changed

+105
-90
lines changed

src/main/bundles/dev.bundle

-2.08 MB
Binary file not shown.

src/main/kotlin/com/faforever/userservice/backend/account/RecoveryService.kt

+39-19
Original file line numberDiff line numberDiff line change
@@ -58,52 +58,72 @@ class RecoveryService(
5858
fafProperties.account().passwordReset().passwordResetUrlFormat().format("STEAM"),
5959
)
6060

61-
fun parseRecoveryHttpRequest(parameters: Map<String, List<String>>): Pair<Type, User?> {
61+
sealed interface ParsingResult {
62+
data class ExtractedUser(val type: Type, val user: User) : ParsingResult
63+
data class ValidNoUser(val type: Type) : ParsingResult
64+
data class Invalid(val cause: Exception) : ParsingResult
65+
}
66+
67+
fun parseRecoveryHttpRequest(parameters: Map<String, List<String>>): ParsingResult {
6268
// At first glance it may seem strange that a service is parsing http request parameters,
6369
// but the parameters of the request are determined by this service itself in the request reset phase!
64-
val token = parameters["token"]?.first()
65-
LOG.debug("Extracted token: {}", token)
6670

67-
val steamId = steamService.parseSteamIdFromRequestParameters(parameters)
68-
LOG.debug("Extracted Steam id: {}", steamId)
69-
70-
return when {
71-
steamId != null -> Type.STEAM to steamService.findUserBySteamId(steamId).also { user ->
72-
if (user == null) metricHelper.incrementPasswordResetViaSteamFailedCounter()
73-
}
74-
75-
token != null -> Type.EMAIL to extractUserFromEmailRecoveryToken(token)
76-
else -> {
71+
val token = parameters["token"]?.firstOrNull()
72+
LOG.debug("Extracted token: {}", token)
73+
return when (token) {
74+
null -> {
7775
metricHelper.incrementPasswordResetViaEmailFailedCounter()
78-
throw InvalidRecoveryException("Could not extract recovery type or user from HTTP request")
76+
ParsingResult.Invalid(InvalidRecoveryException("Could not extract token"))
7977
}
78+
"STEAM" -> when (val result = steamService.parseSteamIdFromRequestParameters(parameters)) {
79+
is SteamService.ParsingResult.NoSteamIdPresent,
80+
is SteamService.ParsingResult.InvalidRedirect,
81+
-> {
82+
metricHelper.incrementPasswordResetViaSteamFailedCounter()
83+
ParsingResult.Invalid(
84+
InvalidRecoveryException("Steam based recovery attempt is invalid"),
85+
)
86+
}
87+
is SteamService.ParsingResult.ExtractedId -> {
88+
val user = steamService.findUserBySteamId(result.steamId)
89+
if (user == null) {
90+
metricHelper.incrementPasswordResetViaSteamFailedCounter()
91+
ParsingResult.ValidNoUser(Type.STEAM)
92+
} else {
93+
ParsingResult.ExtractedUser(Type.STEAM, user)
94+
}
95+
}
96+
}
97+
98+
// Email
99+
else -> extractUserFromEmailRecoveryToken(token)
80100
}
81101
}
82102

83-
private fun extractUserFromEmailRecoveryToken(emailRecoveryToken: String): User {
103+
private fun extractUserFromEmailRecoveryToken(emailRecoveryToken: String): ParsingResult {
84104
val claims = try {
85105
fafTokenService.getTokenClaims(FafTokenType.PASSWORD_RESET, emailRecoveryToken)
86106
} catch (exception: Exception) {
87107
metricHelper.incrementPasswordResetViaEmailFailedCounter()
88108
LOG.error("Unable to extract claims", exception)
89-
throw InvalidRecoveryException("Unable to extract claims from token")
109+
return ParsingResult.Invalid(InvalidRecoveryException("Unable to extract claims from token"))
90110
}
91111

92112
val userId = claims[KEY_USER_ID]
93113

94114
if (userId.isNullOrBlank()) {
95115
metricHelper.incrementPasswordResetViaEmailFailedCounter()
96-
throw InvalidRecoveryException("No user id found in token claims")
116+
return ParsingResult.Invalid(InvalidRecoveryException("No user id found in token claims"))
97117
}
98118

99119
val user = userRepository.findById(userId.toInt())
100120

101121
if (user == null) {
102122
metricHelper.incrementPasswordResetViaEmailFailedCounter()
103-
throw InvalidRecoveryException("User with id $userId not found")
123+
return ParsingResult.Invalid(InvalidRecoveryException("User with id $userId not found"))
104124
}
105125

106-
return user
126+
return ParsingResult.ExtractedUser(Type.EMAIL, user)
107127
}
108128

109129
fun resetPassword(type: Type, userId: Int, newPassword: String) {

src/main/kotlin/com/faforever/userservice/backend/steam/SteamService.kt

+26-15
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,31 @@ class SteamService(
3535
.build().toString()
3636
}
3737

38-
fun parseSteamIdFromRequestParameters(parameters: Map<String, List<String>>): String? {
39-
if (!parameters.containsKey(OPENID_IDENTITY_KEY)) {
40-
LOG.debug("No OpenID identity key present")
41-
return null
42-
}
43-
44-
validateSteamRedirect(parameters)
45-
46-
LOG.trace("Parsing steam id from request parameters: {}", parameters)
47-
return parameters[OPENID_IDENTITY_KEY]?.get(0)
48-
?.let { identityUrl -> identityUrl.substring(identityUrl.lastIndexOf("/") + 1) }
38+
sealed interface ParsingResult {
39+
data object NoSteamIdPresent : ParsingResult
40+
data object InvalidRedirect : ParsingResult
41+
data class ExtractedId(val steamId: String) : ParsingResult
4942
}
5043

51-
private fun validateSteamRedirect(parameters: Map<String, List<String>>) {
44+
fun parseSteamIdFromRequestParameters(parameters: Map<String, List<String>>): ParsingResult =
45+
when {
46+
!isValidSteamRedirect(parameters) -> ParsingResult.InvalidRedirect
47+
else -> {
48+
LOG.trace("Parsing steam id from request parameters: {}", parameters)
49+
parameters[OPENID_IDENTITY_KEY]?.firstOrNull()
50+
?.let { identityUrl -> identityUrl.substring(identityUrl.lastIndexOf("/") + 1) }
51+
?.let { steamId ->
52+
ParsingResult.ExtractedId(steamId).also {
53+
LOG.debug("Extracted Steam id: {}", steamId)
54+
}
55+
}
56+
?: ParsingResult.NoSteamIdPresent.also {
57+
LOG.debug("No OpenID identity key present")
58+
}
59+
}
60+
}
61+
62+
private fun isValidSteamRedirect(parameters: Map<String, List<String>>): Boolean {
5263
LOG.debug("Checking valid OpenID 2.0 redirect against Steam API, parameters: {}", parameters)
5364

5465
val uriBuilder = UriBuilder.fromUri(fafProperties.steam().loginUrlFormat())
@@ -68,11 +79,11 @@ class SteamService(
6879
val result = response.body()
6980

7081
if (result == null || !result.contains("is_valid:true")) {
71-
throw InvalidSteamRedirectException(
72-
"Could not verify steam redirect for identity: ${parameters[OPENID_IDENTITY_KEY]}",
73-
)
82+
LOG.debug("Could not verify steam redirect for identity: {}", parameters[OPENID_IDENTITY_KEY])
83+
return false
7484
} else {
7585
LOG.debug("Steam response successfully validated.")
86+
return true
7687
}
7788
}
7889

src/main/kotlin/com/faforever/userservice/ui/view/recovery/RecoverAccountView.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class RecoverAccountView :
2020

2121
private val emailSection = VerticalLayout(
2222
Button(getTranslation("recovery.selectMethod.email.link")) {
23-
getUI().ifPresent{ ui -> ui.navigate(RecoverViaEmailView::class.java) }
23+
getUI().ifPresent { ui -> ui.navigate(RecoverViaEmailView::class.java) }
2424
}.apply {
2525
addThemeVariants(ButtonVariant.LUMO_PRIMARY)
2626
},
@@ -33,7 +33,7 @@ class RecoverAccountView :
3333

3434
private val steamSection = VerticalLayout(
3535
Button(getTranslation("recovery.selectMethod.steam.link")) {
36-
getUI().ifPresent{ ui -> ui.navigate(RecoverViaSteamView::class.java) }
36+
getUI().ifPresent { ui -> ui.navigate(RecoverViaSteamView::class.java) }
3737
}.apply {
3838
addThemeVariants(ButtonVariant.LUMO_PRIMARY)
3939
},

src/main/kotlin/com/faforever/userservice/ui/view/recovery/RecoverSetPasswordView.kt

+11-19
Original file line numberDiff line numberDiff line change
@@ -102,27 +102,19 @@ class RecoverSetPasswordView(
102102
override fun beforeEnter(event: BeforeEnterEvent?) {
103103
val parameters = event?.location?.queryParameters?.parameters ?: emptyMap()
104104

105-
val (recoveryType, user) = try {
106-
recoveryService.parseRecoveryHttpRequest(parameters)
107-
} catch (e: Exception) {
108-
showDialog("recovery.setPassword.failed.title", "recovery.setPassword.invalidToken")
109-
return
110-
}
111-
112-
if (user == null) {
113-
when (recoveryType) {
114-
RecoveryService.Type.EMAIL ->
115-
showDialog("recovery.setPassword.failed.title", "recovery.setPassword.invalidToken")
116-
RecoveryService.Type.STEAM ->
117-
showDialog("recovery.setPassword.failed.title", "recovery.steam.unknownUser")
105+
when (val result = recoveryService.parseRecoveryHttpRequest(parameters)) {
106+
is RecoveryService.ParsingResult.Invalid ->
107+
showDialog("recovery.setPassword.failed.title", "recovery.setPassword.invalidToken")
108+
is RecoveryService.ParsingResult.ValidNoUser ->
109+
showDialog("recovery.setPassword.failed.title", "recovery.setPassword.unknownUser")
110+
is RecoveryService.ParsingResult.ExtractedUser -> {
111+
this.recoveryType = result.type
112+
this.user = result.user
113+
114+
usernameInRecovery.element.text =
115+
getTranslation("recovery.setPassword.usernameInRecovery", user?.username ?: "")
118116
}
119-
} else {
120-
usernameInRecovery.element.text =
121-
getTranslation("recovery.setPassword.usernameInRecovery", user.username ?: "")
122117
}
123-
124-
this.recoveryType = recoveryType
125-
this.user = user
126118
}
127119

128120
private fun showDialog(titleKey: String, messageKey: String?) {

src/main/kotlin/com/faforever/userservice/ui/view/recovery/RecoverViaSteamView.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,6 @@ class RecoverViaSteamView(
6969

7070
private fun redirectToSteam() {
7171
val steamUrl = recoveryService.buildSteamLoginUrl()
72-
getUI().ifPresent{ ui -> ui.page.setLocation(steamUrl)}
72+
getUI().ifPresent { ui -> ui.page.setLocation(steamUrl) }
7373
}
7474
}

src/main/resources/i18n/messages.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ recovery.email.sent.title=Recovery email sent
100100
recovery.email.sent.hint=Don't forget to also check your spam folder! Disclaimer: Due to data privacy regulations we cannot tell you if the username or email actually exists in our system and an email was actually sent.
101101
recovery.steam.title=Reset password via Steam
102102
recovery.steam.disclaimer=You will be forwarded to the Steam website to login with your Steam username and password. FAForever can not see your Steam password, nor do we get access to your Steam account in any way.
103-
recovery.steam.unknownUser=There is no user associated with your Steam account.
103+
recovery.setPassword.unknownUser=There is no user associated with your Steam account.
104104
recovery.setPassword.title=Set new password
105105
recovery.setPassword.usernameInRecovery=You are recovering user ''{0}''.
106106
recovery.setPassword.submit=Set new password

src/test/kotlin/com/faforever/userservice/backend/account/RecoveryServiceTest.kt

+25-33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.faforever.userservice.backend.account
22

3+
import com.faforever.userservice.backend.account.RecoveryService.ParsingResult
34
import com.faforever.userservice.backend.domain.User
45
import com.faforever.userservice.backend.domain.UserRepository
56
import com.faforever.userservice.backend.email.EmailService
@@ -16,10 +17,9 @@ import jakarta.inject.Inject
1617
import org.hamcrest.MatcherAssert.assertThat
1718
import org.hamcrest.Matchers.containsString
1819
import org.hamcrest.Matchers.equalTo
19-
import org.hamcrest.Matchers.nullValue
20+
import org.hamcrest.Matchers.instanceOf
2021
import org.hamcrest.Matchers.startsWith
2122
import org.junit.jupiter.api.Test
22-
import org.junit.jupiter.api.assertThrows
2323
import org.mockito.kotlin.any
2424
import org.mockito.kotlin.eq
2525
import org.mockito.kotlin.verify
@@ -113,69 +113,64 @@ class RecoveryServiceTest {
113113
@Test
114114
fun testParseRecoveryHttpRequestWithEmptyParameters() {
115115
// Execute
116-
assertThrows<InvalidRecoveryException> {
117-
recoveryService.parseRecoveryHttpRequest(emptyMap())
118-
}
116+
val result = recoveryService.parseRecoveryHttpRequest(emptyMap())
119117

120118
// Verify
119+
assertThat(result, instanceOf(ParsingResult.Invalid::class.java))
120+
121121
verify(metricHelper).incrementPasswordResetViaEmailFailedCounter()
122122
}
123123

124124
@Test
125125
fun testParseRecoveryHttpRequestWithUnknownSteamId() {
126126
// Prepare
127-
val parameters = mapOf("some" to listOf("fake", "values"))
127+
val parameters = mapOf("token" to listOf("STEAM"))
128128

129129
whenever(steamService.parseSteamIdFromRequestParameters(parameters))
130-
.thenReturn("someSteamId")
130+
.thenReturn(SteamService.ParsingResult.ExtractedId("someSteamId"))
131131
whenever(steamService.findUserBySteamId("someSteamId"))
132132
.thenReturn(null)
133133

134134
// Execute
135-
val (type, user) = recoveryService.parseRecoveryHttpRequest(parameters)
135+
val result = recoveryService.parseRecoveryHttpRequest(parameters) as ParsingResult.ValidNoUser
136136

137137
// Verify
138-
assertThat(type, equalTo(RecoveryService.Type.STEAM))
139-
assertThat(user, nullValue())
140-
138+
assertThat(result.type, equalTo(RecoveryService.Type.STEAM))
141139
verify(metricHelper).incrementPasswordResetViaSteamFailedCounter()
142140
}
143141

144142
@Test
145143
fun testParseRecoveryHttpRequestWithKnownSteamId() {
146144
// Prepare
147-
val parameters = mapOf("some" to listOf("fake", "values"))
145+
val parameters = mapOf("token" to listOf("STEAM"))
148146
val testUser = buildTestUser()
149147

150148
whenever(steamService.parseSteamIdFromRequestParameters(parameters))
151-
.thenReturn("someSteamId")
149+
.thenReturn(SteamService.ParsingResult.ExtractedId("someSteamId"))
152150
whenever(steamService.findUserBySteamId("someSteamId"))
153151
.thenReturn(testUser)
154152

155153
// Execute
156-
val (type, user) = recoveryService.parseRecoveryHttpRequest(parameters)
154+
val result = recoveryService.parseRecoveryHttpRequest(parameters) as ParsingResult.ExtractedUser
157155

158156
// Verify
159-
assertThat(type, equalTo(RecoveryService.Type.STEAM))
160-
assertThat(user, equalTo(testUser))
157+
assertThat(result.type, equalTo(RecoveryService.Type.STEAM))
158+
assertThat(result.user, equalTo(testUser))
161159
}
162160

163161
@Test
164162
fun testParseRecoveryHttpRequestWithInvalidTokenClaims() {
165163
// Prepare
166164
val parameters = mapOf("token" to listOf("tokenValue"))
167165

168-
whenever(steamService.parseSteamIdFromRequestParameters(parameters))
169-
.thenReturn(null)
170166
whenever(fafTokenService.getTokenClaims(PASSWORD_RESET, "tokenValue"))
171167
.thenThrow(RuntimeException("invalid token claim"))
172168

173169
// Execute
174-
assertThrows<InvalidRecoveryException> {
175-
recoveryService.parseRecoveryHttpRequest(parameters)
176-
}
170+
val result = recoveryService.parseRecoveryHttpRequest(parameters)
177171

178172
// Verify
173+
assertThat(result, instanceOf(ParsingResult.Invalid::class.java))
179174
verify(metricHelper).incrementPasswordResetViaEmailFailedCounter()
180175
}
181176

@@ -185,16 +180,15 @@ class RecoveryServiceTest {
185180
val parameters = mapOf("token" to listOf("tokenValue"))
186181

187182
whenever(steamService.parseSteamIdFromRequestParameters(parameters))
188-
.thenReturn(null)
183+
.thenReturn(SteamService.ParsingResult.NoSteamIdPresent)
189184
whenever(fafTokenService.getTokenClaims(PASSWORD_RESET, "tokenValue"))
190185
.thenReturn(emptyMap())
191186

192187
// Execute
193-
assertThrows<InvalidRecoveryException> {
194-
recoveryService.parseRecoveryHttpRequest(parameters)
195-
}
188+
val result = recoveryService.parseRecoveryHttpRequest(parameters)
196189

197190
// Verify
191+
assertThat(result, instanceOf(ParsingResult.Invalid::class.java))
198192
verify(metricHelper).incrementPasswordResetViaEmailFailedCounter()
199193
}
200194

@@ -203,18 +197,16 @@ class RecoveryServiceTest {
203197
// Prepare
204198
val parameters = mapOf("token" to listOf("tokenValue"))
205199

206-
whenever(steamService.parseSteamIdFromRequestParameters(parameters))
207-
.thenReturn(null)
208200
whenever(fafTokenService.getTokenClaims(PASSWORD_RESET, "tokenValue"))
209201
.thenReturn(mapOf("id" to "12345"))
210202
whenever(userRepository.findById(12345)).thenReturn(null)
211203

212204
// Execute
213-
assertThrows<InvalidRecoveryException> {
214-
recoveryService.parseRecoveryHttpRequest(parameters)
215-
}
205+
val result = recoveryService.parseRecoveryHttpRequest(parameters)
216206

217207
// Verify
208+
assertThat(result, instanceOf(ParsingResult.Invalid::class.java))
209+
218210
verify(metricHelper).incrementPasswordResetViaEmailFailedCounter()
219211
}
220212

@@ -231,11 +223,11 @@ class RecoveryServiceTest {
231223
whenever(userRepository.findById(12345)).thenReturn(testUser)
232224

233225
// Execute
234-
val (type, user) = recoveryService.parseRecoveryHttpRequest(parameters)
226+
val result = recoveryService.parseRecoveryHttpRequest(parameters) as ParsingResult.ExtractedUser
235227

236228
// Verify
237-
assertThat(type, equalTo(RecoveryService.Type.EMAIL))
238-
assertThat(user, equalTo(testUser))
229+
assertThat(result.type, equalTo(RecoveryService.Type.EMAIL))
230+
assertThat(result.user, equalTo(testUser))
239231
}
240232

241233
@Test

0 commit comments

Comments
 (0)