diff --git a/auth/data/src/main/kotlin/me/proton/core/auth/data/api/AuthenticationApi.kt b/auth/data/src/main/kotlin/me/proton/core/auth/data/api/AuthenticationApi.kt index d6c219769..2d056e50c 100644 --- a/auth/data/src/main/kotlin/me/proton/core/auth/data/api/AuthenticationApi.kt +++ b/auth/data/src/main/kotlin/me/proton/core/auth/data/api/AuthenticationApi.kt @@ -24,6 +24,7 @@ import me.proton.core.auth.data.api.request.ForkSessionRequest import me.proton.core.auth.data.api.request.LoginLessRequest import me.proton.core.auth.data.api.request.LoginRequest import me.proton.core.auth.data.api.request.LoginSsoRequest +import me.proton.core.auth.data.api.request.LoginTokenMdmRequest import me.proton.core.auth.data.api.request.PhoneValidationRequest import me.proton.core.auth.data.api.request.RefreshSessionRequest import me.proton.core.auth.data.api.request.RequestSessionRequest @@ -68,6 +69,9 @@ interface AuthenticationApi : BaseRetrofitApi { @POST("auth/v4/credentialless") suspend fun performLoginLess(@Body request: LoginLessRequest): LoginResponse + @POST("vpn/v1/business/mdm-login") + suspend fun performLoginTokenMdm(@Body request: LoginTokenMdmRequest): LoginResponse + @POST("auth/v4/2fa") suspend fun performSecondFactor(@Body request: SecondFactorRequest): SecondFactorResponse diff --git a/auth/data/src/main/kotlin/me/proton/core/auth/data/api/request/LoginTokenMdmRequest.kt b/auth/data/src/main/kotlin/me/proton/core/auth/data/api/request/LoginTokenMdmRequest.kt new file mode 100644 index 000000000..7e134ab82 --- /dev/null +++ b/auth/data/src/main/kotlin/me/proton/core/auth/data/api/request/LoginTokenMdmRequest.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.auth.data.api.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LoginTokenMdmRequest( + @SerialName("MdmToken") + val token: String, + @SerialName("Group") + val group: String, + @SerialName("DeviceUniqId") + val deviceId: String? = null, +) diff --git a/auth/data/src/main/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImpl.kt b/auth/data/src/main/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImpl.kt index e6d1f6e6c..9fa5b8ec1 100644 --- a/auth/data/src/main/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImpl.kt +++ b/auth/data/src/main/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImpl.kt @@ -32,6 +32,7 @@ import me.proton.core.auth.data.api.request.ForkSessionRequest import me.proton.core.auth.data.api.request.LoginLessRequest import me.proton.core.auth.data.api.request.LoginRequest import me.proton.core.auth.data.api.request.LoginSsoRequest +import me.proton.core.auth.data.api.request.LoginTokenMdmRequest import me.proton.core.auth.data.api.request.PhoneValidationRequest import me.proton.core.auth.data.api.request.RefreshSessionRequest import me.proton.core.auth.data.api.request.RequestSessionRequest @@ -142,6 +143,18 @@ class AuthRepositoryImpl @Inject constructor( }.valueOrThrow } + override suspend fun performLoginTokenMdm( + token: String, + group: String, + deviceId: String? + ): SessionInfo = result("performLoginTokenMDM") { + provider.get().invoke { + val request = LoginTokenMdmRequest(token, group, deviceId) + val response = performLoginTokenMdm(request) + response.toSessionInfo(username = null) + }.valueOrThrow + } + private suspend fun getFrameMap(frames: List): Map { val name = "${product.framePrefix()}-0" val frame = when { diff --git a/auth/data/src/test/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImplTest.kt b/auth/data/src/test/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImplTest.kt index 73d32ddda..fa7ddce36 100644 --- a/auth/data/src/test/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImplTest.kt +++ b/auth/data/src/test/kotlin/me/proton/core/auth/data/repository/AuthRepositoryImplTest.kt @@ -482,6 +482,26 @@ class AuthRepositoryImplTest { repository.performLoginLess() } + @Test + fun `performLoginTokenMdm return SessionInfo`() = runTest(testDispatcherProvider.Main) { + // GIVEN + coEvery { apiManager.invoke(any()) } returns ApiResult.Success(mockk()) + // WHEN + repository.performLoginTokenMdm("token", "group", "deviceId") + } + + @Test(expected = ApiException::class) + fun `performLoginTokenMdm return error`() = runTest(testDispatcherProvider.Main) { + // GIVEN + coEvery { apiManager.invoke(any()) } returns ApiResult.Error.Http( + httpCode = 401, + message = "test http error", + proton = ApiResult.Error.ProtonData(1, "test login error") + ) + // WHEN + repository.performLoginTokenMdm("token", "group", "deviceId") + } + @Test fun `requestSession success`() = runTest(testDispatcherProvider.Main) { // GIVEN diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/repository/AuthRepository.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/repository/AuthRepository.kt index 338776fdb..d76331c6e 100644 --- a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/repository/AuthRepository.kt +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/repository/AuthRepository.kt @@ -88,6 +88,15 @@ interface AuthRepository { frames: List = emptyList() ): SessionInfo + /** + * Perform token-based MDM Login to create a session (accessToken, refreshToken, sessionId, ...). + */ + suspend fun performLoginTokenMdm( + token: String, + group: String, + deviceId: String? + ): SessionInfo + /** * Perform Two Factor for the Login process for a given [SessionId]. */ diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/CreateLoginTokenMdmSession.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/CreateLoginTokenMdmSession.kt new file mode 100644 index 000000000..865bc2cbe --- /dev/null +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/CreateLoginTokenMdmSession.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.auth.domain.usecase + +import me.proton.core.account.domain.entity.Account +import me.proton.core.account.domain.entity.AccountDetails +import me.proton.core.account.domain.entity.AccountState +import me.proton.core.account.domain.entity.AccountType +import me.proton.core.account.domain.entity.SessionDetails +import me.proton.core.account.domain.entity.SessionState +import me.proton.core.accountmanager.domain.AccountWorkflowHandler +import me.proton.core.auth.domain.entity.SessionInfo +import me.proton.core.auth.domain.entity.getFido2AuthOptions +import me.proton.core.network.domain.session.Session +import me.proton.core.util.kotlin.serialize +import javax.inject.Inject + +/** + * Create a token-based MDM Session. + * + * Note: Next step should be PostLoginTokenMdmAccountSetup. + */ +class CreateLoginTokenMdmSession @Inject constructor( + private val requiredAccountType: AccountType, + private val accountWorkflow: AccountWorkflowHandler, + private val performLoginTokenMdm: PerformLoginTokenMdm +) { + suspend operator fun invoke(token: String, group: String, deviceId: String?): SessionInfo { + val sessionInfo = performLoginTokenMdm(token, group, deviceId) + handleSessionInfo(sessionInfo) + return sessionInfo + } + + private suspend fun handleSessionInfo(sessionInfo: SessionInfo) { + val sessionState = if (sessionInfo.isSecondFactorNeeded) { + SessionState.SecondFactorNeeded + } else { + SessionState.Authenticated + } + + val account = Account( + userId = sessionInfo.userId, + username = null, + email = null, + sessionId = sessionInfo.sessionId, + state = AccountState.NotReady, + sessionState = sessionState, + details = AccountDetails( + session = SessionDetails( + initialEventId = sessionInfo.eventId, + requiredAccountType = requiredAccountType, + secondFactorEnabled = sessionInfo.isSecondFactorNeeded, + twoPassModeEnabled = sessionInfo.isTwoPassModeNeeded, + passphrase = null, + password = null, + fido2AuthenticationOptionsJson = sessionInfo.getFido2AuthOptions()?.serialize() + ) + ) + ) + val session = Session.Authenticated( + userId = sessionInfo.userId, + sessionId = sessionInfo.sessionId, + accessToken = sessionInfo.accessToken, + refreshToken = sessionInfo.refreshToken, + scopes = sessionInfo.scopes, + ) + accountWorkflow.handleSession(account, session) + } +} diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PerformLoginTokenMdm.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PerformLoginTokenMdm.kt new file mode 100644 index 000000000..eb13b71aa --- /dev/null +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PerformLoginTokenMdm.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.auth.domain.usecase + +import me.proton.core.auth.domain.entity.SessionInfo +import me.proton.core.auth.domain.repository.AuthRepository +import javax.inject.Inject + +/** + * Performs token-based login for MDM. + */ +class PerformLoginTokenMdm @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(token: String, group: String, deviceId: String?): SessionInfo = + authRepository.performLoginTokenMdm(token, group, deviceId) +} diff --git a/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PostLoginTokenMdmAccountSetup.kt b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PostLoginTokenMdmAccountSetup.kt new file mode 100644 index 000000000..4953127f8 --- /dev/null +++ b/auth/domain/src/main/kotlin/me/proton/core/auth/domain/usecase/PostLoginTokenMdmAccountSetup.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.auth.domain.usecase + +import me.proton.core.accountmanager.domain.SessionManager +import me.proton.core.accountmanager.domain.AccountWorkflowHandler +import me.proton.core.domain.entity.UserId +import me.proton.core.network.domain.server.ServerClock +import me.proton.core.user.domain.UserManager +import me.proton.core.user.domain.entity.Type +import me.proton.core.user.domain.entity.User +import me.proton.core.user.domain.extension.USER_SERVICE_MASK_VPN +import javax.inject.Inject + +class PostLoginTokenMdmAccountSetup @Inject constructor( + private val accountWorkflow: AccountWorkflowHandler, + private val userCheck: PostLoginAccountSetup.UserCheck, + private val userManager: UserManager, + private val sessionManager: SessionManager, + private val serverClock: ServerClock, +) { + suspend operator fun invoke( + userId: UserId, + ): PostLoginAccountSetup.UserCheckResult { + // Refresh scopes. + sessionManager.refreshScopes(checkNotNull(sessionManager.getSessionId(userId))) + + // First, create the User to invoke UserCheck. + val user = makeUser(userId) + userManager.addUser(user, emptyList()) + + val userCheckResult = userCheck.invoke(user) + when (userCheckResult) { + is PostLoginAccountSetup.UserCheckResult.Error -> { + // Disable account and prevent login. + accountWorkflow.handleAccountDisabled(userId) + } + is PostLoginAccountSetup.UserCheckResult.Success -> { + // Last step, change account state to Ready. + accountWorkflow.handleAccountReady(userId) + } + } + return userCheckResult + } + + private fun makeUser(userId: UserId): User = User( + userId = userId, + email = null, + name = null, + displayName = null, + currency = "", + credit = 0, + type = Type.CredentialLess, + createdAtUtc = serverClock.getCurrentTimeUTC().toEpochMilli(), + usedSpace = 0, + maxSpace = 0, + maxUpload = 0, + role = null, + private = true, + services = USER_SERVICE_MASK_VPN, + subscribed = 0, + delinquent = null, + recovery = null, + keys = emptyList(), + flags = emptyMap(), + maxBaseSpace = 0, + maxDriveSpace = 0, + usedBaseSpace = 0, + usedDriveSpace = 0, + ) +}