mirror of
https://github.com/ProtonMail/protoncore_android.git
synced 2026-05-15 09:50:41 +00:00
feature(auth): Support token-based login for MDM.
VPNAND-2466
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+32
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -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<AuthenticationApi>().invoke {
|
||||
val request = LoginTokenMdmRequest(token, group, deviceId)
|
||||
val response = performLoginTokenMdm(request)
|
||||
response.toSessionInfo(username = null)
|
||||
}.valueOrThrow
|
||||
}
|
||||
|
||||
private suspend fun getFrameMap(frames: List<ChallengeFrameDetails>): Map<String, ChallengeFrame?> {
|
||||
val name = "${product.framePrefix()}-0"
|
||||
val frame = when {
|
||||
|
||||
+20
@@ -482,6 +482,26 @@ class AuthRepositoryImplTest {
|
||||
repository.performLoginLess()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performLoginTokenMdm return SessionInfo`() = runTest(testDispatcherProvider.Main) {
|
||||
// GIVEN
|
||||
coEvery { apiManager.invoke<SessionInfo>(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<SessionInfo>(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
|
||||
|
||||
@@ -88,6 +88,15 @@ interface AuthRepository {
|
||||
frames: List<ChallengeFrameDetails> = 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].
|
||||
*/
|
||||
|
||||
+85
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+33
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
+87
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user