Skip to content
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

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open

Feature/recover password #113

wants to merge 13 commits into from

Conversation

jamcunha
Copy link
Member

Closes #84

Review checklist

  • Properly documents API changes in docs/openapi.yml
  • Contains enough appropriate tests
  • Behavior is as expected
  • Clean, well structured code

Copy link
Member

@BrunoRosendo BrunoRosendo left a 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

@codecov
Copy link

codecov bot commented May 3, 2023

Codecov Report

Patch coverage: 26.08% and project coverage change: -3.64 ⚠️

Comparison is base (af68fd8) 88.99% compared to head (36812a3) 85.36%.

❗ Current head 36812a3 differs from pull request most recent head 1be6f37. Consider uploading reports for the commit 1be6f37 to get more accurate results

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     
Impacted Files Coverage Δ
...ni/website/backend/controller/AccountController.kt 83.33% <0.00%> (-16.67%) ⬇️
...fe/ni/website/backend/controller/AuthController.kt 83.33% <0.00%> (-16.67%) ⬇️
.../fe/ni/website/backend/dto/auth/PassRecoveryDto.kt 0.00% <0.00%> (ø)
...pt/up/fe/ni/website/backend/service/AuthService.kt 91.48% <0.00%> (-4.07%) ⬇️
.../up/fe/ni/website/backend/service/ErrorMessages.kt 71.42% <ø> (-19.49%) ⬇️
...up/fe/ni/website/backend/service/AccountService.kt 61.53% <28.57%> (-38.47%) ⬇️
...ebsite/backend/config/auth/AuthConfigProperties.kt 100.00% <100.00%> (ø)

... and 51 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@BrunoRosendo
Copy link
Member

@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

Copy link
Member

@bdmendes bdmendes left a 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

@jamcunha jamcunha marked this pull request as ready for review June 28, 2023 16:23
@github-actions
Copy link

Check the documentation preview: https://649ca04ed8b19e215c607a56--niaefeup-backend-docs.netlify.app

@github-actions
Copy link

Check the documentation preview: https://649ca1498b322b02fcdd527a--niaefeup-backend-docs.netlify.app

Copy link
Member

@BrunoRosendo BrunoRosendo left a 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

@github-actions
Copy link

github-actions bot commented Jul 2, 2023

Check the documentation preview: https://64a0e585de74510aa07bd843--niaefeup-backend-docs.netlify.app

Copy link
Member

@BrunoRosendo BrunoRosendo left a 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

@github-actions
Copy link

github-actions bot commented Jul 2, 2023

Check the documentation preview: https://64a1a0168cd57a5fc9cb1bc4--niaefeup-backend-docs.netlify.app

Copy link
Member

@BrunoRosendo BrunoRosendo left a 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?

@BrunoRosendo
Copy link
Member

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

Copy link
Member

@BrunoRosendo BrunoRosendo left a 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)

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")
Copy link
Member

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

@DoStini
Copy link
Member

DoStini commented Jul 3, 2023

Overall looking good, just left some comments to improve the RESTiness of the API and privacy concerning issues 🚀

Comment on lines 85 to 86
val tokenPasswordHash = jwt.getClaim<String>("passwordHash")
?: throw InvalidBearerTokenException(ErrorMessages.missingHashClaim)
Copy link
Member

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.

@jamcunha
Copy link
Member Author

jamcunha commented Jul 4, 2023

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.

Seems weird to me that this is in a different controller.

I suggest either moving everything to auth controller or to account controller.

Besides that, there are two requests with a similar URL recoverPassword.

I suggest the following routes in AuthController: post -> password/recovery with email in body post -> password/recovery/{token}/confirm post -> password/update

The tests may be lacking because I didn't want to test more without making these changes.

@github-actions
Copy link

github-actions bot commented Jul 5, 2023

Check the documentation preview: https://64a5f916ff15070f4706ee8a--niaefeup-backend-docs.netlify.app

Copy link
Member

@BrunoRosendo BrunoRosendo left a 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 {
Copy link
Member

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?

@github-actions
Copy link

github-actions bot commented Jul 9, 2023

Check the documentation preview: https://64aae9d16ffd10485fee1ef9--niaefeup-backend-docs.netlify.app

Copy link
Member

@BrunoRosendo BrunoRosendo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comments, besides the one still unresolved

Also, can you find a way to test the uncovered (red) lines? It'd be awesome if it's not too much trouble

image

@@ -57,26 +58,69 @@ class AuthService(
return generateAccessToken(account)
}

fun generateRecoveryToken(email: String): String? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fun generateRecoveryToken(email: String): String? {
fun generateRecoveryToken(email: String): String {

Correct me if I'm wrong but I believe it can't be null

@jamcunha
Copy link
Member Author

jamcunha commented Aug 1, 2023

Small comments, besides the one still unresolved

Also, can you find a way to test the uncovered (red) lines? It'd be awesome if it's not too much trouble

image

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 expireAt to Instant.now() but it's caught in an exception when trying to decode in confirmRecoveryToken. I think it's the same for passwordHash claim.

@github-actions
Copy link

github-actions bot commented Aug 1, 2023

Check the documentation preview: https://64c92675c420e6299b59bee0--niaefeup-backend-docs.netlify.app

@jamcunha
Copy link
Member Author

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.

Copy link
Member

@BrunoRosendo BrunoRosendo left a 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

@LuisDuarte1 LuisDuarte1 self-requested a review August 23, 2023 15:21
Copy link
Member

@LuisDuarte1 LuisDuarte1 left a 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 😅

@LuisDuarte1
Copy link
Member

@jamcunha can you fix the conflicts please?

@github-actions
Copy link

Check the documentation preview: https://64efb2c789311e0a7eba43ac--niaefeup-backend-docs.netlify.app

Comment on lines +41 to +44
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email)
?: return emptyMap()
// TODO: Change to email service
return mapOf("recovery_url" to "$recoverPasswordPage/$recoveryToken/confirm")
Copy link
Member

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.

Suggested change
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 ""

Copy link
Member

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

Copy link
Member Author

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.

Comment on lines -47 to +48
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the changes here?

Copy link
Member Author

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
Copy link
Member

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

Copy link
Member Author

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.

Comment on lines +66 to +79
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)
}
Copy link
Member

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

Comment on lines +215 to +251
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
)
}
Copy link
Member

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.

Copy link
Member Author

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.

Comment on lines +41 to +42
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email)
?: return emptyMap()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email)
?: return emptyMap()
val recoveryToken = authService.generateRecoveryToken(recoveryRequestDto.email)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

accounts: recover password
5 participants