-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/recover password #113
base: develop
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic looks good overall. I didn't do a careful review since this is just a draft
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AuthController.kt
Outdated
Show resolved
Hide resolved
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## develop #113 +/- ##
=============================================
- Coverage 88.99% 85.36% -3.64%
+ Complexity 361 234 -127
=============================================
Files 63 43 -20
Lines 827 567 -260
Branches 65 29 -36
=============================================
- Hits 736 484 -252
- Misses 57 62 +5
+ Partials 34 21 -13
☔ View full report in Codecov by Sentry. |
@jamcunha Is this ready for review or do you still have some doubts? We can talk about it in the next meeting or in 1on1 |
src/test/kotlin/pt/up/fe/ni/website/backend/controller/AccountControllerTest.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks good to me. To improve security a bit, we can look into serializing the current password into the jwt to make sure it is only used once: https://stackoverflow.com/a/54865104
0c241bc
to
413af20
Compare
9f89087
to
3f5ae20
Compare
Check the documentation preview: https://649ca04ed8b19e215c607a56--niaefeup-backend-docs.netlify.app |
Check the documentation preview: https://649ca1498b322b02fcdd527a--niaefeup-backend-docs.netlify.app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good job with this implementation! I left some comments with small suggestions and doubts
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AccountController.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AuthController.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/dto/auth/PassRecoveryDto.kt
Outdated
Show resolved
Hide resolved
src/test/kotlin/pt/up/fe/ni/website/backend/controller/AccountControllerTest.kt
Outdated
Show resolved
Hide resolved
src/test/kotlin/pt/up/fe/ni/website/backend/controller/AccountControllerTest.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/dto/auth/PassRecoveryDto.kt
Outdated
Show resolved
Hide resolved
Check the documentation preview: https://64a0e585de74510aa07bd843--niaefeup-backend-docs.netlify.app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good job, I left a small comment but I'm pre-approving. It'd be nice if you could review your commits too, maybe squash a few
src/main/kotlin/pt/up/fe/ni/website/backend/dto/auth/ChangePasswordDto.kt
Outdated
Show resolved
Hide resolved
Check the documentation preview: https://64a1a0168cd57a5fc9cb1bc4--niaefeup-backend-docs.netlify.app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the latest change, where do you check to see if the hash present in the jwt is the same as the one in the db?
nevermind this comment, I was looking at the wrong diff |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you create 1/2 tests for the latest change? Maybe 1 where a user tries to use the same recovery link twice and another one just to check for missing hash (if the last one isn't too hard)
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AccountController.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AuthController.kt
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AuthController.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AuthController.kt
Outdated
Show resolved
Hide resolved
fun generateRecoveryToken(@PathVariable id: Long): Map<String, String> { | ||
val recoveryToken = authService.generateRecoveryToken(id) | ||
// TODO: Change to email service | ||
return mapOf("recovery_url" to "$recoverPasswordPage/$recoveryToken") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For security reasons, users should not be able to know if a email is registered in a website, so the request should always be Ok 200
src/main/kotlin/pt/up/fe/ni/website/backend/dto/auth/ChangePasswordDto.kt
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/dto/auth/PasswordRecoveryDto.kt
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/service/AccountService.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/service/ErrorMessages.kt
Outdated
Show resolved
Hide resolved
Overall looking good, just left some comments to improve the RESTiness of the API and privacy concerning issues 🚀 |
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AccountController.kt
Outdated
Show resolved
Hide resolved
val tokenPasswordHash = jwt.getClaim<String>("passwordHash") | ||
?: throw InvalidBearerTokenException(ErrorMessages.missingHashClaim) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the hash is not in the payload (since we have that option)? You can simply let it be null and skip the next check if that is the case.
Just changed the recovery URL request to be provided with the email instead of the id and removed errors related to invalid tokens, logging them instead.
The tests may be lacking because I didn't want to test more without making these changes. |
Check the documentation preview: https://64a5f916ff15070f4706ee8a--niaefeup-backend-docs.netlify.app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small changes, Im also waiting for @DoStini response on my comment
private val fileUploader: FileUploader | ||
) { | ||
) : Logging { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the logger being used?
src/main/kotlin/pt/up/fe/ni/website/backend/service/AuthService.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/service/AuthService.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/service/AuthService.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/pt/up/fe/ni/website/backend/controller/AuthController.kt
Show resolved
Hide resolved
Check the documentation preview: https://64aae9d16ffd10485fee1ef9--niaefeup-backend-docs.netlify.app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/main/kotlin/pt/up/fe/ni/website/backend/service/AuthService.kt
Outdated
Show resolved
Hide resolved
@@ -57,26 +58,69 @@ class AuthService( | |||
return generateAccessToken(account) | |||
} | |||
|
|||
fun generateRecoveryToken(email: String): String? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fun generateRecoveryToken(email: String): String? { | |
fun generateRecoveryToken(email: String): String { |
Correct me if I'm wrong but I believe it can't be null
src/test/kotlin/pt/up/fe/ni/website/backend/controller/AuthControllerTest.kt
Outdated
Show resolved
Hide resolved
...t/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadAuth.kt
Outdated
Show resolved
Hide resolved
I'm having problems trying to test those two lines because when testing token expiration, I try to create a new token with all the claims of the previous token but modifying the |
Check the documentation preview: https://64c92675c420e6299b59bee0--niaefeup-backend-docs.netlify.app |
For now, I only changed the error handling in the recovery logic so as not to mess up with the tests. After some inputs, I will change the handling for other jwt logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs a rebase but I'm pre-approving
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚀, it looks clean to me, just solve the conflicts below 😅
3e54dda
to
9a9fa6c
Compare
@jamcunha can you fix the conflicts please? |
9850100
to
acac4b3
Compare
Check the documentation preview: https://64efb2c789311e0a7eba43ac--niaefeup-backend-docs.netlify.app |
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email) | ||
?: return emptyMap() | ||
// TODO: Change to email service | ||
return mapOf("recovery_url" to "$recoverPasswordPage/$recoveryToken/confirm") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this return OK all the time and comment on what is supposed to go on an email. If someone wants to test this right away, one can simply print the result. It is safer to leave the correct behavior already implemented.
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email) | |
?: return emptyMap() | |
// TODO: Change to email service | |
return mapOf("recovery_url" to "$recoverPasswordPage/$recoveryToken/confirm") | |
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email) | |
?: return emptyMap() | |
// TODO: Add email service with payload "$recoverPasswordPage/$recoveryToken/ | |
return "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about the return ""
, but it should always be status 200.
Also document on why this is done for security reasons, in order to not leak what are the emails associated with our service
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we have it return the recovery link for now so we can test the feature and remove it when the email service is complete?
If I'm not wrong it always returns status 200.
val jwt = | ||
try { | ||
jwtDecoder.decode(refreshToken) | ||
} catch (e: Exception) { | ||
throw InvalidBearerTokenException(ErrorMessages.invalidRefreshToken) | ||
} | ||
if (jwt.expiresAt?.isBefore(Instant.now()) != false) { | ||
throw InvalidBearerTokenException(ErrorMessages.expiredRefreshToken) | ||
} | ||
val jwt = jwtDecoder.decode(refreshToken) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the changes here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the try catch block and handled the exception in the ErrorController
and the token expiration validation is already handled in the decode()
method.
val account = try { | ||
accountService.getAccountByEmail(email) | ||
} catch (e: Exception) { | ||
return null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the error is not the expected one (account not found), log the error so that developers can more easily find the problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone uses an email that's not related to an account it would trigger the exception. I don't think we should log this.
fun confirmRecoveryToken(recoveryToken: String, dto: PasswordRecoveryConfirmDto): Account { | ||
val jwt = jwtDecoder.decode(recoveryToken) | ||
val account = accountService.getAccountByEmail(jwt.subject) | ||
|
||
val tokenPasswordHash = jwt.getClaim<String>("passwordHash") | ||
?: throw InvalidBearerTokenException(ErrorMessages.invalidToken) | ||
|
||
if (account.password != tokenPasswordHash) { | ||
throw InvalidBearerTokenException(ErrorMessages.invalidToken) | ||
} | ||
|
||
account.password = passwordEncoder.encode(dto.password) | ||
return accountService.updateAccount(account) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function also updates the account password, so I don't think this is the best name. I would either change the function name a little but, or extract the update logic and use this as an auxiliary function to validate the token
fun `should return empty if email is not found`() { | ||
mockMvc.perform( | ||
post("/auth/password/recovery") | ||
.contentType(MediaType.APPLICATION_JSON) | ||
.content(objectMapper.writeValueAsString(mapOf("email" to "[email protected]"))) | ||
) | ||
.andExpectAll( | ||
status().isOk(), | ||
jsonPath("$.recovery_url").doesNotExist() | ||
) | ||
.andDocument( | ||
documentation, | ||
"Recover password", | ||
"This endpoint operation allows the recovery of the password of an account, " + | ||
"sending an email with a link to reset the password.", | ||
documentRequestPayload = true | ||
) | ||
} | ||
|
||
@Test | ||
fun `should return password recovery link`() { | ||
mockMvc.perform( | ||
post("/auth/password/recovery") | ||
.contentType(MediaType.APPLICATION_JSON) | ||
.content(objectMapper.writeValueAsString(mapOf("email" to testAccount.email))) | ||
) | ||
.andExpectAll( | ||
status().isOk(), | ||
jsonPath("$.recovery_url").exists() | ||
).andDocument( | ||
documentation, | ||
"Recover password", | ||
"This endpoint operation allows the recovery of the password of an account, " + | ||
"sending an email with a link to reset the password.", | ||
documentRequestPayload = true | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I explained before, this is not the expected behavior. Both cases will return empty and ok status.
What you should do is test the request returns empty and ok both times, but also to test if the first situation does not call the generate token function. I think the easiest way would be by mocking JwtClaimsSet and verifying if some of those functions do not get called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, this could be as is for now and changed when the email service is implemented.
Can leave a comment to remind that.
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email) | ||
?: return emptyMap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email) | |
?: return emptyMap() | |
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email) | |
Closes #84
Review checklist
docs/openapi.yml