Skip to content

Commit

Permalink
feat: change sso accounts immediately without confirmation + limit ss…
Browse files Browse the repository at this point in the history
…o domains in server configuration
  • Loading branch information
Anty0 committed Feb 6, 2025
1 parent 3dfa8df commit 501f227
Show file tree
Hide file tree
Showing 15 changed files with 182 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.tolgee.model.enums.ThirdPartyAuthType
import io.tolgee.security.authentication.JwtService
import io.tolgee.security.payload.JwtAuthenticationResponse
import io.tolgee.security.service.thirdParty.ThirdPartyAuthDelegate
import io.tolgee.service.TenantService
import io.tolgee.service.security.AuthProviderChangeService
import io.tolgee.service.security.SignUpService
import io.tolgee.service.security.UserAccountService
Expand All @@ -30,6 +31,7 @@ class GithubOAuthDelegate(
properties: TolgeeProperties,
private val signUpService: SignUpService,
private val authProviderChangeService: AuthProviderChangeService,
private val tenantService: TenantService,
) : ThirdPartyAuthDelegate {
private val githubConfigurationProperties: GithubAuthenticationProperties = properties.authentication.github

Expand Down Expand Up @@ -83,31 +85,9 @@ class GithubOAuthDelegate(
)?.email
?: throw AuthenticationException(Message.THIRD_PARTY_AUTH_NO_EMAIL)

val userAccount =
userAccountService.findByThirdParty(ThirdPartyAuthType.GITHUB, userResponse!!.id!!) ?: let {
userAccountService.findActive(githubEmail)?.let {
authProviderChangeService.initiateProviderChange(
AuthProviderChangeData(
it,
UserAccount.AccountType.THIRD_PARTY,
ThirdPartyAuthType.GITHUB,
userResponse.id,
),
)
throw AuthenticationException(Message.THIRD_PARTY_SWITCH_INITIATED)
}

val newUserAccount = UserAccount()
newUserAccount.username = githubEmail
newUserAccount.name = userResponse.name ?: userResponse.login
newUserAccount.thirdPartyAuthId = userResponse.id
newUserAccount.thirdPartyAuthType = ThirdPartyAuthType.GITHUB
newUserAccount.accountType = UserAccount.AccountType.THIRD_PARTY

signUpService.signUp(newUserAccount, invitationCode, null)

newUserAccount
}
val userAccount = findAccount(githubEmail, userResponse!!, invitationCode)

tenantService.checkSsoNotRequired(userAccount.username)

val jwt = jwtService.emitToken(userAccount.id)
return JwtAuthenticationResponse(jwt)
Expand All @@ -123,6 +103,39 @@ class GithubOAuthDelegate(
throw AuthenticationException(Message.THIRD_PARTY_AUTH_UNKNOWN_ERROR)
}

fun findAccount(
githubEmail: String,
userResponse: GithubUserResponse,
invitationCode: String?,
): UserAccount {
userAccountService.findByThirdParty(ThirdPartyAuthType.GITHUB, userResponse.id!!)?.let {
return it
}

userAccountService.findActive(githubEmail)?.let {
authProviderChangeService.initiateProviderChange(
AuthProviderChangeData(
it,
UserAccount.AccountType.THIRD_PARTY,
ThirdPartyAuthType.GITHUB,
userResponse.id,
),
)
return it
}

val newUserAccount = UserAccount()
newUserAccount.username = githubEmail
newUserAccount.name = userResponse.name ?: userResponse.login
newUserAccount.thirdPartyAuthId = userResponse.id
newUserAccount.thirdPartyAuthType = ThirdPartyAuthType.GITHUB
newUserAccount.accountType = UserAccount.AccountType.THIRD_PARTY

signUpService.signUp(newUserAccount, invitationCode, null)

return newUserAccount
}

class GithubEmailResponse {
var email: String? = null
var primary = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.tolgee.model.enums.ThirdPartyAuthType
import io.tolgee.security.authentication.JwtService
import io.tolgee.security.payload.JwtAuthenticationResponse
import io.tolgee.security.service.thirdParty.ThirdPartyAuthDelegate
import io.tolgee.service.TenantService
import io.tolgee.service.security.AuthProviderChangeService
import io.tolgee.service.security.SignUpService
import io.tolgee.service.security.UserAccountService
Expand All @@ -29,6 +30,7 @@ class GoogleOAuthDelegate(
properties: TolgeeProperties,
private val signUpService: SignUpService,
private val authProviderChangeService: AuthProviderChangeService,
private val tenantService: TenantService,
) : ThirdPartyAuthDelegate {
private val googleConfigurationProperties: GoogleAuthenticationProperties = properties.authentication.google

Expand Down Expand Up @@ -85,33 +87,10 @@ class GoogleOAuthDelegate(
}
}

val googleEmail = userResponse.email ?: throw AuthenticationException(Message.THIRD_PARTY_AUTH_NO_EMAIL)

val userAccount =
userAccountService.findByThirdParty(ThirdPartyAuthType.GOOGLE, userResponse.sub!!) ?: let {
userAccountService.findActive(googleEmail)?.let {
authProviderChangeService.initiateProviderChange(
AuthProviderChangeData(
it,
UserAccount.AccountType.THIRD_PARTY,
ThirdPartyAuthType.GOOGLE,
userResponse.sub,
),
)
throw AuthenticationException(Message.THIRD_PARTY_SWITCH_INITIATED)
}

val newUserAccount = UserAccount()
newUserAccount.username = userResponse.email
?: throw AuthenticationException(Message.THIRD_PARTY_AUTH_NO_EMAIL)
newUserAccount.name = userResponse.name ?: (userResponse.given_name + " " + userResponse.family_name)
newUserAccount.thirdPartyAuthId = userResponse.sub
newUserAccount.thirdPartyAuthType = ThirdPartyAuthType.GOOGLE
newUserAccount.accountType = UserAccount.AccountType.THIRD_PARTY
signUpService.signUp(newUserAccount, invitationCode, null)

newUserAccount
}
val userAccount = findAccount(userResponse, invitationCode)

tenantService.checkSsoNotRequired(userAccount.username)

val jwt = jwtService.emitToken(userAccount.id)
return JwtAuthenticationResponse(jwt)
}
Expand All @@ -128,6 +107,39 @@ class GoogleOAuthDelegate(
}
}

private fun findAccount(
userResponse: GoogleUserResponse,
invitationCode: String?,
): UserAccount {
userAccountService.findByThirdParty(ThirdPartyAuthType.GOOGLE, userResponse.sub!!)?.let {
return it
}

val googleEmail = userResponse.email ?: throw AuthenticationException(Message.THIRD_PARTY_AUTH_NO_EMAIL)
userAccountService.findActive(googleEmail)?.let {
authProviderChangeService.initiateProviderChange(
AuthProviderChangeData(
it,
UserAccount.AccountType.THIRD_PARTY,
ThirdPartyAuthType.GOOGLE,
userResponse.sub,
),
)
return it
}

val newUserAccount = UserAccount()
newUserAccount.username = userResponse.email
?: throw AuthenticationException(Message.THIRD_PARTY_AUTH_NO_EMAIL)
newUserAccount.name = userResponse.name ?: (userResponse.given_name + " " + userResponse.family_name)
newUserAccount.thirdPartyAuthId = userResponse.sub
newUserAccount.thirdPartyAuthType = ThirdPartyAuthType.GOOGLE
newUserAccount.accountType = UserAccount.AccountType.THIRD_PARTY
signUpService.signUp(newUserAccount, invitationCode, null)

return newUserAccount
}

@Suppress("PropertyName")
class GoogleUserResponse {
var sub: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.tolgee.security.authentication.JwtService
import io.tolgee.security.payload.JwtAuthenticationResponse
import io.tolgee.security.service.thirdParty.ThirdPartyAuthDelegate
import io.tolgee.security.thirdParty.data.OAuthUserDetails
import io.tolgee.service.TenantService
import org.slf4j.LoggerFactory
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
Expand All @@ -28,6 +29,7 @@ class OAuth2Delegate(
private val restTemplate: RestTemplate,
properties: TolgeeProperties,
private val oAuthUserHandler: OAuthUserHandler,
private val tenantService: TenantService,
) : ThirdPartyAuthDelegate {
private val oauth2ConfigurationProperties: OAuth2AuthenticationProperties = properties.authentication.oauth2
private val logger = LoggerFactory.getLogger(this::class.java)
Expand Down Expand Up @@ -110,6 +112,8 @@ class OAuth2Delegate(
UserAccount.AccountType.THIRD_PARTY,
)

tenantService.checkSsoNotRequired(user.username)

val jwt = jwtService.emitToken(user.id)
return JwtAuthenticationResponse(jwt)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class OAuthUserHandler(
thirdPartyAuthType: ThirdPartyAuthType,
accountType: UserAccount.AccountType,
): UserAccount {
val existingUserAccount =
var existingUserAccount =
userAccountService.findActive(userResponse.email)
if (existingUserAccount != null) {
authProviderChangeService.initiateProviderChange(
Expand All @@ -76,7 +76,7 @@ class OAuthUserHandler(
ssoExpiration = userAccountService.getCurrentSsoExpiration(thirdPartyAuthType),
),
)
throw AuthenticationException(Message.THIRD_PARTY_SWITCH_INITIATED)
return existingUserAccount
}

val newUserAccount = UserAccount()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,19 @@ class SsoOrganizationsProperties {
" able to access the server after the account has been disabled or deleted in the SSO provider.",
)
var sessionExpirationMinutes: Int = 10

@DocProperty(
description =
"Only allow listed domains to be used for SSO configuration.",
)
var allowedDomains: List<String>? = emptyList()

fun isAllowed(domain: String): Boolean {
val allowed = allowedDomains
if (allowed == null) {
return true
}

return domain in allowed
}
}
2 changes: 2 additions & 0 deletions backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ enum class Message {
CANNOT_SET_SSO_PROVIDER_MISSING_FIELDS,
NAMESPACES_CANNOT_BE_DISABLED_WHEN_NAMESPACE_EXISTS,
NAMESPACE_CANNOT_BE_USED_WHEN_FEATURE_IS_DISABLED,
SSO_DOMAIN_NOT_ALLOWED,
SSO_LOGIN_FORCED_FOR_THIS_ACCOUNT,
;

val code: String
Expand Down
14 changes: 14 additions & 0 deletions backend/data/src/main/kotlin/io/tolgee/service/TenantService.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package io.tolgee.service

import io.tolgee.constants.Message
import io.tolgee.dtos.sso.SsoTenantConfig
import io.tolgee.dtos.sso.SsoTenantDto
import io.tolgee.exceptions.AuthenticationException
import io.tolgee.model.Organization
import io.tolgee.model.SsoTenant

interface TenantService {
fun getEnabledConfigByDomainOrNull(domain: String): SsoTenantConfig?

fun getEnabledConfigByDomain(domain: String): SsoTenantConfig

fun save(tenant: SsoTenant): SsoTenant
Expand All @@ -22,4 +26,14 @@ interface TenantService {
request: SsoTenantDto,
organization: Organization,
): SsoTenant

fun checkSsoNotRequired(username: String) {
val domain = username.takeIf { it.count { it == '@' } == 1 }?.split('@')?.get(1)
if (domain != null) {
val tenant = getEnabledConfigByDomainOrNull(domain)
if (tenant != null) {
throw AuthenticationException(Message.SSO_LOGIN_FORCED_FOR_THIS_ACCOUNT, listOf(domain))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import org.springframework.stereotype.Service

@Service
class TenantServiceOssStub : TenantService {
override fun getEnabledConfigByDomainOrNull(domain: String): SsoTenantConfig? {
return null
}

override fun getEnabledConfigByDomain(domain: String): SsoTenantConfig {
throw NotFoundException(Message.SSO_DOMAIN_NOT_FOUND_OR_DISABLED)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import io.tolgee.service.TenantService
import io.tolgee.service.organization.OrganizationRoleService
import io.tolgee.util.addMinutes
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional

@Service
class AuthProviderChangeService(
Expand All @@ -33,19 +31,39 @@ class AuthProviderChangeService(
return getActiveAuthProviderChangeRequest(userAccount)?.asAuthProviderDto()
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
// @Transactional(propagation = Propagation.REQUIRES_NEW)
// fun initiateProviderChange(data: AuthProviderChangeData) {
// authProviderChangeRequestRepository.deleteByUserAccountId(data.userAccount.id)
// val expirationDate = currentDateProvider.date.addMinutes(30)
// authProviderChangeRequestRepository.save(data.asAuthProviderChangeRequest(expirationDate))
// throw AuthenticationException(Message.THIRD_PARTY_SWITCH_INITIATED)
// }

fun initiateProviderChange(data: AuthProviderChangeData) {
authProviderChangeRequestRepository.deleteByUserAccountId(data.userAccount.id)
// This version of the function is only temporary solution and
// supports only SSO account auth provider change for now.
// It will be replaced with the version above once full
// support for auth provider change is implemented.
if (data.accountType != UserAccount.AccountType.MANAGED) {
throw AuthenticationException(Message.USERNAME_ALREADY_EXISTS)
}
val expirationDate = currentDateProvider.date.addMinutes(30)
authProviderChangeRequestRepository.save(data.asAuthProviderChangeRequest(expirationDate))
val change = data.asAuthProviderChangeRequest(expirationDate)
acceptProviderChange(change)
}

fun acceptProviderChange(userAccount: UserAccount) {
val req = getActiveAuthProviderChangeRequest(userAccount) ?: return
acceptProviderChange(req)
authProviderChangeRequestRepository.delete(req)
}

fun acceptProviderChange(req: AuthProviderChangeRequest) {
val userAccount = req.userAccount ?: return throw NotFoundException()
if (userAccount.accountType === UserAccount.AccountType.MANAGED) {
throw AuthenticationException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE)
}

val req = getActiveAuthProviderChangeRequest(userAccount) ?: return
userAccount.apply {
accountType = req.accountType
thirdPartyAuthType = req.authType
Expand All @@ -66,7 +84,6 @@ class AuthProviderChangeService(
}
organizationRoleService.setManaged(userAccount, organization, true)
}
authProviderChangeRequestRepository.delete(req)
}

fun rejectProviderChange(userAccount: UserAccount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.tolgee.security.authentication.JwtService
import io.tolgee.security.payload.JwtAuthenticationResponse
import io.tolgee.service.EmailVerificationService
import io.tolgee.service.QuickStartService
import io.tolgee.service.TenantService
import io.tolgee.service.invitation.InvitationService
import io.tolgee.service.organization.OrganizationRoleService
import io.tolgee.service.organization.OrganizationService
Expand All @@ -31,13 +32,16 @@ class SignUpService(
private val organizationRoleService: OrganizationRoleService,
private val quickStartService: QuickStartService,
private val passwordEncoder: PasswordEncoder,
private val tenantService: TenantService,
) {
@Transactional
fun signUp(dto: SignUpDto): JwtAuthenticationResponse? {
userAccountService.findActive(dto.email)?.let {
throw BadRequestException(Message.USERNAME_ALREADY_EXISTS)
}

tenantService.checkSsoNotRequired(dto.email)

val user = dtoToEntity(dto)
signUp(user, dto.invitationCode, dto.organizationName, dto.userSource)

Expand Down
Loading

0 comments on commit 501f227

Please sign in to comment.