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,
+ )
+}