diff --git a/backend/api/src/main/kotlin/io/tolgee/controllers/PublicController.kt b/backend/api/src/main/kotlin/io/tolgee/controllers/PublicController.kt index 7d82bfff79..e1261dd9a2 100644 --- a/backend/api/src/main/kotlin/io/tolgee/controllers/PublicController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/controllers/PublicController.kt @@ -80,6 +80,27 @@ class PublicController( request: ResetPasswordRequest, ) { val userAccount = userAccountService.findActive(request.email!!) ?: return + if (userAccount.accountType === UserAccount.AccountType.MANAGED) { + val params = + EmailParams( + to = request.email!!, + subject = "Password Reset Request - SSO Managed Account", + text = + """ + Hello! 👋

+ 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 for login.

+ To access your account, please use the "SSO Login" button on the Tolgee login page. No password reset is needed.

+ If you did not make this request, you may safely ignore this email.

+ + Regards,
+ Tolgee + """.trimIndent(), + ) + + tolgeeEmailSender.sendEmail(params) + return + } + val code = RandomStringUtils.randomAlphabetic(50) userAccountService.setResetPasswordCode(userAccount, code) @@ -124,6 +145,9 @@ class PublicController( request: ResetPassword, ) { val userAccount = validateEmailCode(request.code!!, request.email!!) + if (userAccount.accountType === UserAccount.AccountType.MANAGED) { + throw BadRequestException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE) + } if (userAccount.accountType === UserAccount.AccountType.THIRD_PARTY) { userAccountService.setAccountType(userAccount, UserAccount.AccountType.LOCAL) } diff --git a/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuth2Delegate.kt b/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuth2Delegate.kt index 3daeb993a4..ab3d210332 100644 --- a/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuth2Delegate.kt +++ b/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuth2Delegate.kt @@ -4,6 +4,7 @@ import io.tolgee.configuration.tolgee.OAuth2AuthenticationProperties import io.tolgee.configuration.tolgee.TolgeeProperties import io.tolgee.constants.Message import io.tolgee.exceptions.AuthenticationException +import io.tolgee.model.UserAccount import io.tolgee.model.enums.ThirdPartyAuthType import io.tolgee.security.authentication.JwtService import io.tolgee.security.payload.JwtAuthenticationResponse @@ -96,7 +97,13 @@ class OAuth2Delegate( familyName = userResponse.family_name, email = email, ) - val user = oAuthUserHandler.findOrCreateUser(userData, invitationCode, ThirdPartyAuthType.OAUTH2) + val user = + oAuthUserHandler.findOrCreateUser( + userData, + invitationCode, + ThirdPartyAuthType.OAUTH2, + UserAccount.AccountType.THIRD_PARTY, + ) val jwt = jwtService.emitToken(user.id) return JwtAuthenticationResponse(jwt) diff --git a/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuthUserHandler.kt b/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuthUserHandler.kt index 3642b9dfbd..a78faf9dd8 100644 --- a/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuthUserHandler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/security/thirdParty/OAuthUserHandler.kt @@ -27,6 +27,7 @@ class OAuthUserHandler( userResponse: OAuthUserDetails, invitationCode: String?, thirdPartyAuthType: ThirdPartyAuthType, + accountType: UserAccount.AccountType, ): UserAccount { val tenant = userResponse.tenant @@ -73,8 +74,9 @@ class OAuthUserHandler( } newUserAccount.thirdPartyAuthType = thirdPartyAuthType newUserAccount.ssoRefreshToken = userResponse.refreshToken - newUserAccount.accountType = UserAccount.AccountType.THIRD_PARTY + newUserAccount.accountType = accountType newUserAccount.ssoSessionExpiry = currentDateProvider.date.addMinutes(SSO_SESSION_EXPIRATION_MINUTES) + signUpService.signUp(newUserAccount, invitationCode, null) // grant role to user only if request is not from oauth2 delegate diff --git a/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt b/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt index ac44a31196..d45435213d 100644 --- a/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt +++ b/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt @@ -247,7 +247,6 @@ enum class Message { SSO_USER_CANNOT_CREATE_ORGANIZATION, SSO_CANT_VERIFY_USER, SSO_USER_CANT_LOGIN_WITH_NATIVE, - SSO_USER_OPERATION_UNAVAILABLE, SSO_GLOBAL_CONFIG_MISSING_PROPERTIES, SSO_DOMAIN_NOT_FOUND_OR_DISABLED, NATIVE_AUTHENTICATION_DISABLED, diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt index 64cdf42dd8..25ae6c6651 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt @@ -400,10 +400,6 @@ class UserAccountService( throw BadRequestException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE) } - if (userAccount.thirdPartyAuthType == ThirdPartyAuthType.SSO) { - throw BadRequestException(Message.SSO_USER_OPERATION_UNAVAILABLE) - } - val matches = passwordEncoder.matches(dto.currentPassword, userAccount.password) if (!matches) throw PermissionException(Message.WRONG_CURRENT_PASSWORD) diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/UserCredentialsService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/UserCredentialsService.kt index 2bf719cf35..678938895e 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/security/UserCredentialsService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/security/UserCredentialsService.kt @@ -3,7 +3,6 @@ package io.tolgee.service.security import io.tolgee.constants.Message import io.tolgee.exceptions.AuthenticationException import io.tolgee.model.UserAccount -import io.tolgee.model.enums.ThirdPartyAuthType import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service @@ -27,10 +26,6 @@ class UserCredentialsService( throw AuthenticationException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE) } - if (userAccount.thirdPartyAuthType == ThirdPartyAuthType.SSO) { - throw AuthenticationException(Message.SSO_USER_OPERATION_UNAVAILABLE) - } - checkNativeUserCredentials(userAccount, password) return userAccount } diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/OAuth2CallbackController.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/OAuth2CallbackController.kt index df119f5cfb..4356809ac8 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/OAuth2CallbackController.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/OAuth2CallbackController.kt @@ -1,6 +1,9 @@ package io.tolgee.ee.api.v2.controllers import io.tolgee.component.FrontendUrlProvider +import io.tolgee.component.enabledFeaturesProvider.EnabledFeaturesProvider +import io.tolgee.constants.Feature +import io.tolgee.constants.Message import io.tolgee.ee.data.DomainRequest import io.tolgee.ee.data.SsoUrlResponse import io.tolgee.ee.service.OAuthService @@ -21,6 +24,7 @@ class OAuth2CallbackController( private val userAccountService: UserAccountService, private val jwtService: JwtService, private val frontendUrlProvider: FrontendUrlProvider, + private val enabledFeaturesProvider: EnabledFeaturesProvider, ) { @PostMapping("/get-authentication-url") fun getAuthenticationUrl( @@ -28,6 +32,10 @@ class OAuth2CallbackController( ): SsoUrlResponse { val registrationId = request.domain val tenant = tenantService.getEnabledByDomain(registrationId) + enabledFeaturesProvider.checkFeatureEnabled( + organizationId = tenant.organization?.id, + Feature.SSO, + ) val redirectUrl = buildAuthUrl(tenant, state = request.state) return SsoUrlResponse(redirectUrl) diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/OAuthService.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/OAuthService.kt index c50265a64f..ae8bae703f 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/OAuthService.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/OAuthService.kt @@ -9,12 +9,15 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import com.nimbusds.jwt.proc.ConfigurableJWTProcessor import io.tolgee.component.CurrentDateProvider +import io.tolgee.component.enabledFeaturesProvider.EnabledFeaturesProvider +import io.tolgee.constants.Feature import io.tolgee.constants.Message import io.tolgee.ee.data.GenericUserResponse import io.tolgee.ee.data.OAuth2TokenResponse import io.tolgee.ee.exceptions.OAuthAuthorizationException import io.tolgee.exceptions.AuthenticationException import io.tolgee.model.SsoTenant +import io.tolgee.model.UserAccount import io.tolgee.model.enums.ThirdPartyAuthType import io.tolgee.security.authentication.JwtService import io.tolgee.security.payload.JwtAuthenticationResponse @@ -27,7 +30,7 @@ import org.springframework.http.* import org.springframework.stereotype.Service import org.springframework.util.LinkedMultiValueMap import org.springframework.util.MultiValueMap -import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestClientException import org.springframework.web.client.RestTemplate import java.net.URL import java.util.* @@ -40,6 +43,7 @@ class OAuthService( private val tenantService: TenantService, private val oAuthUserHandler: OAuthUserHandler, private val currentDateProvider: CurrentDateProvider, + private val enabledFeaturesProvider: EnabledFeaturesProvider, ) : OAuthServiceEe, Logging { fun handleOAuthCallback( @@ -59,6 +63,10 @@ class OAuthService( } val tenant = tenantService.getEnabledByDomain(registrationId) + enabledFeaturesProvider.checkFeatureEnabled( + organizationId = tenant.organization?.id, + Feature.SSO, + ) val tokenResponse = exchangeCodeForToken(tenant, code, redirectUrl) @@ -71,7 +79,7 @@ class OAuthService( return register(userInfo, tenant, invitationCode, tokenResponse.refresh_token) } - fun exchangeCodeForToken( + private fun exchangeCodeForToken( tenant: SsoTenant, code: String, redirectUrl: String, @@ -99,13 +107,13 @@ class OAuthService( OAuth2TokenResponse::class.java, ) response.body - } catch (e: HttpClientErrorException) { + } catch (e: RestClientException) { logger.info("Failed to exchange code for token: ${e.message}") null } } - fun verifyAndDecodeIdToken( + private fun verifyAndDecodeIdToken( idToken: String, jwkSetUri: String, ): GenericUserResponse { @@ -158,7 +166,13 @@ class OAuthService( refreshToken = refreshToken, tenant = tenant, ) - val user = oAuthUserHandler.findOrCreateUser(userData, invitationCode, ThirdPartyAuthType.SSO) + val user = + oAuthUserHandler.findOrCreateUser( + userData, + invitationCode, + ThirdPartyAuthType.SSO, + UserAccount.AccountType.MANAGED, + ) val jwt = jwtService.emitToken(user.id) return JwtAuthenticationResponse(jwt) } @@ -183,6 +197,10 @@ class OAuthService( } val tenant = tenantService.getEnabledByDomain(ssoDomain) + enabledFeaturesProvider.checkFeatureEnabled( + organizationId = tenant.organization?.id, + Feature.SSO, + ) val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_FORM_URLENCODED @@ -210,7 +228,7 @@ class OAuthService( return true } false - } catch (e: HttpClientErrorException) { + } catch (e: RestClientException) { logger.info("Failed to refresh token: ${e.message}") false }