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
}