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

feat: sso - user privileges and licence handling #2665

Merged
merged 7 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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! 👋<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 for login.<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)

Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class OAuthUserHandler(
userResponse: OAuthUserDetails,
invitationCode: String?,
thirdPartyAuthType: ThirdPartyAuthType,
accountType: UserAccount.AccountType,
): UserAccount {
val tenant = userResponse.tenant

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,13 +24,18 @@ class OAuth2CallbackController(
private val userAccountService: UserAccountService,
private val jwtService: JwtService,
private val frontendUrlProvider: FrontendUrlProvider,
private val enabledFeaturesProvider: EnabledFeaturesProvider,
) {
@PostMapping("/get-authentication-url")
fun getAuthenticationUrl(
@RequestBody request: DomainRequest,
): 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.*
Expand All @@ -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(
Expand All @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -210,7 +228,7 @@ class OAuthService(
return true
}
false
} catch (e: HttpClientErrorException) {
} catch (e: RestClientException) {
logger.info("Failed to refresh token: ${e.message}")
false
}
Expand Down
Loading