Skip to content

Commit

Permalink
fix: Reset password callback from frontend URL (#2872)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanCizmar authored Jan 27, 2025
1 parent 75216b6 commit 1e99ce2
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.tolgee.controllers

import io.swagger.v3.oas.annotations.tags.Tag

/** This tag is used to mark controllers handling authentication */
@Tag(name = "Authentication")
annotation class AuthenticationTag
120 changes: 1 addition & 119 deletions backend/api/src/main/kotlin/io/tolgee/controllers/PublicController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ package io.tolgee.controllers

import com.fasterxml.jackson.databind.node.TextNode
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.component.email.TolgeeEmailSender
import io.tolgee.configuration.tolgee.AuthenticationProperties
import io.tolgee.configuration.tolgee.TolgeeProperties
import io.tolgee.constants.Message
import io.tolgee.dtos.misc.EmailParams
import io.tolgee.dtos.request.auth.ResetPassword
import io.tolgee.dtos.request.auth.ResetPasswordRequest
import io.tolgee.dtos.request.auth.SignUpDto
import io.tolgee.dtos.security.LoginRequest
import io.tolgee.exceptions.AuthenticationException
Expand All @@ -35,7 +30,6 @@ import io.tolgee.service.security.UserCredentialsService
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import org.apache.commons.lang3.RandomStringUtils
import org.springframework.http.MediaType
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.GetMapping
Expand All @@ -49,12 +43,11 @@ import java.util.*

@RestController
@RequestMapping("/api/public")
@Tag(name = "Authentication")
@AuthenticationTag
class PublicController(
private val jwtService: JwtService,
private val properties: TolgeeProperties,
private val userAccountService: UserAccountService,
private val tolgeeEmailSender: TolgeeEmailSender,
private val emailVerificationService: EmailVerificationService,
private val reCaptchaValidationService: ReCaptchaValidationService,
private val signUpService: SignUpService,
Expand Down Expand Up @@ -84,105 +77,6 @@ class PublicController(
return JwtAuthenticationResponse(jwt)
}

@Operation(summary = "Request password reset")
@PostMapping("/reset_password_request")
@OpenApiHideFromPublicDocs
@RateLimited(limit = 2, refillDurationInMs = 300000)
fun resetPasswordRequest(
@RequestBody @Valid
request: ResetPasswordRequest,
) {
if (!authProperties.nativeEnabled) {
throw DisabledFunctionalityException(Message.NATIVE_AUTHENTICATION_DISABLED)
}

val userAccount = userAccountService.findActive(request.email!!) ?: return

if (!emailVerificationService.isVerified(userAccount)) {
throw BadRequestException(Message.EMAIL_NOT_VERIFIED)
}

if (userAccount.accountType === UserAccount.AccountType.MANAGED) {
val params =
EmailParams(
to = request.email!!,
subject = "Password reset - SSO managed account",
text =
"""
Hello! 👋<br/><br/>
We received a request to reset the password for your account. However, your account is managed by your organization and uses a single sign-on (SSO) service to log in.<br/><br/>
To access your account, please use the "SSO Login" button on the Tolgee login page. No password reset is needed.<br/><br/>
If you did not make this request, you may safely ignore this email.<br/><br/>
Regards,<br/>
Tolgee
""".trimIndent(),
)

tolgeeEmailSender.sendEmail(params)
return
}

val code = RandomStringUtils.randomAlphabetic(50)
userAccountService.setResetPasswordCode(userAccount, code)

val callbackString = code + "," + request.email
val url = request.callbackUrl + "/" + Base64.getEncoder().encodeToString(callbackString.toByteArray())
val isInitial = userAccount.accountType == UserAccount.AccountType.THIRD_PARTY

val params =
EmailParams(
to = request.email!!,
subject = if (isInitial) "Initial password configuration" else "Password reset",
text =
"""
Hello! 👋<br/><br/>
${if (isInitial) "To set a password for your account, <b>follow this link</b>:<br/>" else "To reset your password, <b>follow this link</b>:<br/>"}
<a href="$url">$url</a><br/><br/>
If you have not requested this e-mail, please ignore it.<br/><br/>
Regards,<br/>
Tolgee
""".trimIndent(),
)

tolgeeEmailSender.sendEmail(params)
}

@GetMapping("/reset_password_validate/{email}/{code}")
@Operation(summary = "Validate password-resetting key")
@OpenApiHideFromPublicDocs
fun resetPasswordValidate(
@PathVariable("code") code: String,
@PathVariable("email") email: String,
) {
if (!authProperties.nativeEnabled) {
throw DisabledFunctionalityException(Message.NATIVE_AUTHENTICATION_DISABLED)
}
validateEmailCode(code, email)
}

@PostMapping("/reset_password_set")
@Operation(summary = "Set a new password", description = "Checks the password reset code from e-mail")
@OpenApiHideFromPublicDocs
fun resetPasswordSet(
@RequestBody @Valid
request: ResetPassword,
) {
if (!authProperties.nativeEnabled) {
throw DisabledFunctionalityException(Message.NATIVE_AUTHENTICATION_DISABLED)
}
val userAccount = validateEmailCode(request.code!!, request.email!!)
if (userAccount.accountType === UserAccount.AccountType.MANAGED) {
throw AuthenticationException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE)
}
if (userAccount.accountType === UserAccount.AccountType.THIRD_PARTY) {
userAccountService.setAccountType(userAccount, UserAccount.AccountType.LOCAL)
}
userAccountService.setUserPassword(userAccount, request.password)
userAccountService.removeResetCode(userAccount)
}

@PostMapping("/sign_up")
@Transactional
@OpenApiHideFromPublicDocs
Expand Down Expand Up @@ -272,16 +166,4 @@ class PublicController(
}
return user
}

private fun validateEmailCode(
code: String,
email: String,
): UserAccount {
val userAccount = userAccountService.findActive(email) ?: throw BadRequestException(Message.BAD_CREDENTIALS)
val resetCodeValid = userAccountService.isResetCodeValid(userAccount, code)
if (!resetCodeValid) {
throw BadRequestException(Message.BAD_CREDENTIALS)
}
return userAccount
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.tolgee.controllers.resetPassword

import io.swagger.v3.oas.annotations.Operation
import io.tolgee.configuration.tolgee.AuthenticationProperties
import io.tolgee.constants.Message
import io.tolgee.controllers.AuthenticationTag
import io.tolgee.dtos.request.auth.ResetPassword
import io.tolgee.dtos.request.auth.ResetPasswordRequest
import io.tolgee.exceptions.AuthenticationException
import io.tolgee.exceptions.BadRequestException
import io.tolgee.exceptions.DisabledFunctionalityException
import io.tolgee.model.UserAccount
import io.tolgee.openApiDocs.OpenApiHideFromPublicDocs
import io.tolgee.security.ratelimit.RateLimited
import io.tolgee.service.security.UserAccountService
import jakarta.validation.Valid
import org.springframework.context.ApplicationContext
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/public")
@AuthenticationTag
class PasswordResetController(
private val userAccountService: UserAccountService,
private val applicationContext: ApplicationContext,
private val authProperties: AuthenticationProperties,
) {
@Operation(summary = "Request password reset")
@PostMapping("/reset_password_request")
@OpenApiHideFromPublicDocs
@RateLimited(limit = 2, refillDurationInMs = 300000)
fun resetPasswordRequest(
@RequestBody @Valid
request: ResetPasswordRequest,
) {
ResetPasswordRequestHandler(applicationContext, request).handle()
}

@GetMapping("/reset_password_validate/{email}/{code}")
@Operation(summary = "Validate password-resetting key")
@OpenApiHideFromPublicDocs
fun resetPasswordValidate(
@PathVariable("code") code: String,
@PathVariable("email") email: String,
) {
if (!authProperties.nativeEnabled) {
throw DisabledFunctionalityException(Message.NATIVE_AUTHENTICATION_DISABLED)
}
validateEmailCode(code, email)
}

@PostMapping("/reset_password_set")
@Operation(summary = "Set a new password", description = "Checks the password reset code from e-mail")
@OpenApiHideFromPublicDocs
fun resetPasswordSet(
@RequestBody @Valid
request: ResetPassword,
) {
if (!authProperties.nativeEnabled) {
throw DisabledFunctionalityException(Message.NATIVE_AUTHENTICATION_DISABLED)
}
val userAccount = validateEmailCode(request.code!!, request.email!!)
if (userAccount.accountType === UserAccount.AccountType.MANAGED) {
throw AuthenticationException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE)
}
if (userAccount.accountType === UserAccount.AccountType.THIRD_PARTY) {
userAccountService.setAccountType(userAccount, UserAccount.AccountType.LOCAL)
}
userAccountService.setUserPassword(userAccount, request.password)
userAccountService.removeResetCode(userAccount)
}

private fun validateEmailCode(
code: String,
email: String,
): UserAccount {
val userAccount = userAccountService.findActive(email) ?: throw BadRequestException(Message.BAD_CREDENTIALS)
val resetCodeValid = userAccountService.isResetCodeValid(userAccount, code)
if (!resetCodeValid) {
throw BadRequestException(Message.BAD_CREDENTIALS)
}
return userAccount
}
}
Loading

0 comments on commit 1e99ce2

Please sign in to comment.