Skip to content

Commit

Permalink
Merge pull request #2665 from tolgee/jirikuchynka/sso
Browse files Browse the repository at this point in the history
feat: sso - user privileges and licence handling
  • Loading branch information
Anty0 authored Nov 4, 2024
2 parents 5eb67cd + 563a09d commit daf0bfb
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 18 deletions.
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

0 comments on commit daf0bfb

Please sign in to comment.