diff --git a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt
index dbeb4bf2..66cd67eb 100644
--- a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt
+++ b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt
@@ -24,8 +24,7 @@ import com.infomaniak.auth.lib.internal.managers.MigrationManager
import com.infomaniak.auth.lib.internal.network.ApiClientProvider
import com.infomaniak.auth.lib.internal.network.ApiRoutes
import com.infomaniak.auth.lib.internal.repositories.AccountsRepository
-import com.infomaniak.auth.lib.internal.repositories.WebAuthnRepository
-import com.infomaniak.auth.lib.internal.requests.AuthenticatorRequest
+import com.infomaniak.auth.lib.internal.requests.WebAuthnRequests
import com.infomaniak.auth.lib.models.migration.user.SharedUserProfile
import com.infomaniak.auth.lib.network.interfaces.AuthenticatorBridge
import com.infomaniak.auth.lib.network.interfaces.CrashReportInterface
@@ -75,27 +74,25 @@ abstract class AuthenticatorFacade internal constructor() {
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
): AuthenticatorFacade {
val routes = ApiRoutes(apiHost)
- val webAuthnRepository = WebAuthnRepository(
- authenticatorRequest = AuthenticatorRequest(
- httpClient = ApiClientProvider(
- scope = scope,
- userAgent = userAgent,
- routes = routes,
- crashReport = crashReport,
- ).httpClient,
+ val webAuthnRequests = WebAuthnRequests(
+ httpClient = ApiClientProvider(
+ scope = scope,
+ userAgent = userAgent,
routes = routes,
- )
+ crashReport = crashReport,
+ ).httpClient,
+ routes = routes,
)
val accountsDatabase = getAccountsRoomDatabase(databaseNameOrPath)
val accountsRepository = AccountsRepository(accountsDatabase)
val authenticatorManager = AuthenticatorManager(
- webAuthnRepository = webAuthnRepository,
+ webAuthnRequests = webAuthnRequests,
accountsRepository = accountsRepository
).also { it.keyPairManager.ensureKeyPairsAreMoved() }
val migrationManager = MigrationManager(
accountsDatabase = accountsDatabase,
authenticatorManager = authenticatorManager,
- webAuthnRepository = webAuthnRepository,
+ webAuthnRequests = webAuthnRequests,
clientId = clientId,
)
return AuthenticatorFacadeImpl(
diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/managers/AccountRestorer.kt b/multiplatform-lib/src/commonMain/kotlin/internal/managers/AccountRestorer.kt
index b2067d4f..98f44cf8 100644
--- a/multiplatform-lib/src/commonMain/kotlin/internal/managers/AccountRestorer.kt
+++ b/multiplatform-lib/src/commonMain/kotlin/internal/managers/AccountRestorer.kt
@@ -21,13 +21,13 @@ import com.infomaniak.auth.lib.internal.KeyPairManager.MatchOn
import com.infomaniak.auth.lib.internal.db.AccountEntity
import com.infomaniak.auth.lib.internal.db.AccountsDatabase
import com.infomaniak.auth.lib.internal.extensions.firstOrElse
-import com.infomaniak.auth.lib.internal.repositories.WebAuthnRepository
+import com.infomaniak.auth.lib.internal.requests.WebAuthnRequests
import com.infomaniak.auth.lib.models.migration.SharedApiToken
internal class AccountRestorer(
accountsDatabase: AccountsDatabase,
private val authenticatorManager: AuthenticatorManager,
- private val webAuthnRepository: WebAuthnRepository,
+ private val webAuthnRequests: WebAuthnRequests,
private val clientId: String,
) {
@@ -51,7 +51,7 @@ internal class AccountRestorer(
val previousRestorationAborted = existingKeyIds.size == 2
if (previousRestorationAborted) {
val newKeyIdToDrop = existingKeyIds.last()
- webAuthnRepository.deletePasskeyIfExists(tokenFromOldPasskey.accessToken, newKeyIdToDrop)
+ webAuthnRequests.deletePasskeyIfExists(tokenFromOldPasskey.accessToken, newKeyIdToDrop)
val _ = keyPairManager.deleteKeysMatching(MatchOn.PasskeyId(newKeyIdToDrop))
}
// Register a new passkey
@@ -72,7 +72,7 @@ internal class AccountRestorer(
persistToken(account.id, tokenWithNewPassKey)
// We can safely delete the old passkey, as the new one is working and the old token won't be valid anymore
oldKeyId?.let { keyId ->
- webAuthnRepository.deletePasskeyIfExists(tokenWithNewPassKey.accessToken, keyId)
+ webAuthnRequests.deletePasskeyIfExists(tokenWithNewPassKey.accessToken, keyId)
val _ = keyPairManager.deleteKeysMatching(MatchOn.PasskeyId(keyId))
}
dao.upsert(account.copy(status = AccountEntity.Status.LoggedIn))
diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt b/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt
index d5250990..4fdc3916 100644
--- a/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt
+++ b/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt
@@ -26,7 +26,7 @@ import com.infomaniak.auth.lib.internal.models.ClientExtensionResults
import com.infomaniak.auth.lib.internal.models.VerifyAuthenticationData
import com.infomaniak.auth.lib.internal.models.VerifyResponse
import com.infomaniak.auth.lib.internal.repositories.AccountsRepository
-import com.infomaniak.auth.lib.internal.repositories.WebAuthnRepository
+import com.infomaniak.auth.lib.internal.requests.WebAuthnRequests
import com.infomaniak.auth.lib.internal.utils.SignUtils
import com.infomaniak.auth.lib.internal.utils.Xor
import com.infomaniak.auth.lib.models.migration.SharedApiToken
@@ -35,7 +35,7 @@ import kotlinx.serialization.json.Json
import okio.ByteString.Companion.toByteString
internal class AuthenticatorManager(
- private val webAuthnRepository: WebAuthnRepository,
+ private val webAuthnRequests: WebAuthnRequests,
private val accountsRepository: AccountsRepository,
) {
@@ -44,10 +44,10 @@ internal class AuthenticatorManager(
private val base64NoPadding get() = cryptoObjectsBuilder.base64UrlSafeNoPadding
- suspend fun getUserProfile(token: String) = webAuthnRepository.getUserProfile(token)
+ suspend fun getUserProfile(token: String) = webAuthnRequests.getUserProfile(token)
suspend fun registerPasskey(token: String, userId: Long): String {
- val passkeysOptions = webAuthnRepository.getPasskeysOptions(token).data
+ val passkeysOptions = webAuthnRequests.getPasskeysOptions(token)
val keyIds = cryptoObjectsBuilder.getKeyIds()
val keyIdAsByteArray = keyIds.first
val keyIdAsString = keyIds.second
@@ -65,7 +65,7 @@ internal class AuthenticatorManager(
id = keyIdAsString,
)
- webAuthnRepository.registerPasskey(token, registerPasskey)
+ webAuthnRequests.registerPasskey(token, registerPasskey)
return keyIdAsString
}
@@ -78,7 +78,7 @@ internal class AuthenticatorManager(
val keyId = keyIdOrDefault ?: keyPairManager.findKeyIdFor(MatchOn.UserId(userId))
?: return Xor.Second(Failure.KeyManagement.KeyNotFound("No key found for user $userId"))
- val authenticationOptions = webAuthnRepository.challenge(clientId)
+ val authenticationOptions = webAuthnRequests.challenge(clientId)
val publicKey = keyPairManager.retrievePublicKey(userId, keyId).firstOrNull()
?: return Xor.Second(Failure.KeyManagement.KeyNotFound("No public key found for $userId"))
val rawAuthenticatorData = cryptoObjectsBuilder.generateAuthenticatorData(
@@ -114,7 +114,7 @@ internal class AuthenticatorManager(
clientExtensionResults = ClientExtensionResults,
authenticatorAttachment = "platform",
)
- val verifyAuthData = webAuthnRepository.verify(verifyAuthenticationData)
+ val verifyAuthData = webAuthnRequests.verify(verifyAuthenticationData)
val apiToken = SharedApiToken(
accessToken = verifyAuthData.accessToken,
tokenType = verifyAuthData.tokenType,
@@ -129,7 +129,7 @@ internal class AuthenticatorManager(
if (passkeyId != null) {
// If we have a passkey for this account, revoke it against the backend and delete it
- webAuthnRepository.deletePasskeyIfExists(token, passkeyId)
+ webAuthnRequests.deletePasskeyIfExists(token, passkeyId)
val _ = keyPairManager.deleteKeysMatching(MatchOn.PasskeyId(passkeyId))
}
diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt b/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt
index a7a604fa..f3c1a255 100644
--- a/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt
+++ b/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt
@@ -32,7 +32,7 @@ import com.infomaniak.auth.lib.internal.otp.deleteLegacyDB
import com.infomaniak.auth.lib.internal.otp.getLegacyAccounts
import com.infomaniak.auth.lib.internal.otp.getSecretFor
import com.infomaniak.auth.lib.internal.otp.needMigration
-import com.infomaniak.auth.lib.internal.repositories.WebAuthnRepository
+import com.infomaniak.auth.lib.internal.requests.WebAuthnRequests
import com.infomaniak.auth.lib.models.migration.SharedApiToken
import com.infomaniak.auth.lib.models.migration.user.SharedUserProfile
import com.infomaniak.auth.lib.network.exceptions.ApiException
@@ -46,7 +46,7 @@ import kotlin.uuid.Uuid
internal class MigrationManager(
private val accountsDatabase: AccountsDatabase,
private val authenticatorManager: AuthenticatorManager,
- private val webAuthnRepository: WebAuthnRepository,
+ private val webAuthnRequests: WebAuthnRequests,
private val clientId: String,
) {
@@ -65,7 +65,7 @@ internal class MigrationManager(
val restorer = AccountRestorer(
accountsDatabase = accountsDatabase,
authenticatorManager = authenticatorManager,
- webAuthnRepository = webAuthnRepository,
+ webAuthnRequests = webAuthnRequests,
clientId = clientId
)
restorer.restore(account, persistToken)
@@ -93,7 +93,7 @@ internal class MigrationManager(
@OptIn(ExperimentalUuidApi::class)
val deviceId = Uuid.random().toHexDashString()
val secret = checkNotNull(getSecretFor(userId)) { "Couldn't find the secret for user $userId" }
- val migrationOptions = webAuthnRepository.getMigrationOptions(
+ val migrationOptions = webAuthnRequests.getMigrationOptions(
deviceId = deviceId,
userId = userId,
)
@@ -110,7 +110,7 @@ internal class MigrationManager(
}
runCatching {
- webAuthnRepository.getTokenForMigration(
+ webAuthnRequests.getTokenForMigration(
sessionId = migrationOptions.session,
otpPayload = OtpPayload(
deviceId = deviceId,
@@ -145,7 +145,7 @@ internal class MigrationManager(
}
persistUser(userProfile)
- webAuthnRepository.completeMigration(
+ webAuthnRequests.completeMigration(
token = apiTokenFromPasskey.accessToken,
sessionId = migrationOptions.session,
deviceId = deviceId
diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/repositories/WebAuthnRepository.kt b/multiplatform-lib/src/commonMain/kotlin/internal/repositories/WebAuthnRepository.kt
deleted file mode 100644
index f2d2779f..00000000
--- a/multiplatform-lib/src/commonMain/kotlin/internal/repositories/WebAuthnRepository.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Infomaniak Authenticator - Android
- * Copyright (C) 2026 Infomaniak Network SA
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-package com.infomaniak.auth.lib.internal.repositories
-
-import com.infomaniak.auth.lib.internal.models.AuthResult
-import com.infomaniak.auth.lib.internal.models.AuthenticationOptions
-import com.infomaniak.auth.lib.internal.models.MigrationOptions
-import com.infomaniak.auth.lib.internal.models.OtpPayload
-import com.infomaniak.auth.lib.internal.models.PasskeysOptions
-import com.infomaniak.auth.lib.internal.models.RegisterPasskey
-import com.infomaniak.auth.lib.internal.models.SuccessfulApiResponse
-import com.infomaniak.auth.lib.internal.models.VerifyAuthenticationData
-import com.infomaniak.auth.lib.internal.requests.AuthenticatorRequest
-import com.infomaniak.auth.lib.network.exceptions.ApiException
-
-internal class WebAuthnRepository(
- private val authenticatorRequest: AuthenticatorRequest,
-) {
-
- //region Passkey
-
- // Generate WebAuthn registration options (authentified)
- suspend fun getPasskeysOptions(token: String): SuccessfulApiResponse {
- return authenticatorRequest.getPasskeysOptions(token)
- }
-
- // Validate WebAuthn registration and save public key (authentified)
- suspend fun registerPasskey(token: String, registerPasskey: RegisterPasskey) {
- authenticatorRequest.registerPasskey(token, registerPasskey)
- }
-
- // Deletion of existing passkey (authentified)
- suspend fun deletePasskeyIfExists(token: String, passkeyId: String) {
- try {
- authenticatorRequest.deletePasskey(token, passkeyId)
- } catch (e: ApiException) {
- if (e.statusCode == 404) return
- throw e
- }
- }
-
- // Authentification challenge (not authentified)
- suspend fun challenge(clientId: String): AuthenticationOptions {
- return authenticatorRequest.challenge(clientId).data
- }
-
- // Authentification verification (not authentified)
- suspend fun verify(verifyAuthenticationData: VerifyAuthenticationData): AuthResult {
- return authenticatorRequest.verify(verifyAuthenticationData).data
- }
-
- //endregion
-
- //region Migration
-
- suspend fun getMigrationOptions(deviceId: String, userId: Long): MigrationOptions {
- return authenticatorRequest.getMigrationOptions(deviceId, userId).data
- }
-
- suspend fun getTokenForMigration(
- sessionId: String,
- otpPayload: OtpPayload,
- ): AuthResult {
- return authenticatorRequest.getTokenForMigration(sessionId, otpPayload).data
- }
-
- suspend fun completeMigration(token: String, sessionId: String, deviceId: String) {
- return authenticatorRequest.completeMigration(token, sessionId, deviceId)
- }
-
- suspend fun getUserProfile(token: String) = authenticatorRequest.getUserProfile(token = token).data
-
- //endregion
-}
diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/requests/AuthenticatorRequest.kt b/multiplatform-lib/src/commonMain/kotlin/internal/requests/WebAuthnRequests.kt
similarity index 82%
rename from multiplatform-lib/src/commonMain/kotlin/internal/requests/AuthenticatorRequest.kt
rename to multiplatform-lib/src/commonMain/kotlin/internal/requests/WebAuthnRequests.kt
index 55a17cb7..540eca4f 100644
--- a/multiplatform-lib/src/commonMain/kotlin/internal/requests/AuthenticatorRequest.kt
+++ b/multiplatform-lib/src/commonMain/kotlin/internal/requests/WebAuthnRequests.kt
@@ -28,6 +28,7 @@ import com.infomaniak.auth.lib.internal.models.VerifyAuthenticationData
import com.infomaniak.auth.lib.internal.network.ApiRoutes
import com.infomaniak.auth.lib.internal.network.utils.decode
import com.infomaniak.auth.lib.models.migration.user.SharedUserProfile
+import com.infomaniak.auth.lib.network.exceptions.ApiException
import io.ktor.client.HttpClient
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete
@@ -36,18 +37,20 @@ import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.client.request.setBody
-internal class AuthenticatorRequest(
+internal class WebAuthnRequests(
private val httpClient: suspend () -> HttpClient,
private val routes: ApiRoutes,
) {
+ //region Passkey
+
/**
* Retrieves options (including a challenge) prior to registering a public key credential with [registerPasskey].
*/
- suspend fun getPasskeysOptions(token: String): SuccessfulApiResponse {
+ suspend fun getPasskeysOptions(token: String): PasskeysOptions {
return httpClient().get(routes.passkeysOptions()) {
addAuthenticationHeader(token)
- }.decode()
+ }.decode>().data
}
/**
@@ -64,10 +67,10 @@ internal class AuthenticatorRequest(
/**
* Retrieves the backend-generated challenge, prior to authenticating with [verify].
*/
- suspend fun challenge(clientId: String): SuccessfulApiResponse {
+ suspend fun challenge(clientId: String): AuthenticationOptions {
return httpClient().post(routes.challenge()) {
setBody(mapOf("client_id" to clientId))
- }.decode()
+ }.decode>().data
}
/**
@@ -77,10 +80,10 @@ internal class AuthenticatorRequest(
*
* @return An [AuthResult] that includes an access token.
*/
- suspend fun verify(verifyAuthenticationData: VerifyAuthenticationData): SuccessfulApiResponse {
+ suspend fun verify(verifyAuthenticationData: VerifyAuthenticationData): AuthResult {
return httpClient().post(routes.verify()) {
setBody(verifyAuthenticationData)
- }.decode()
+ }.decode>().data
}
/**
@@ -89,22 +92,35 @@ internal class AuthenticatorRequest(
* @param token The access token of the user.
* @param passkeyId The id of the passkey to delete.
*/
- suspend fun deletePasskey(token: String, passkeyId: String) {
+ suspend fun deletePasskeyIfExists(token: String, passkeyId: String) {
+ try {
+ deletePasskey(token, passkeyId)
+ } catch (e: ApiException) {
+ if (e.statusCode == 404) return
+ throw e
+ }
+ }
+
+ private suspend fun deletePasskey(token: String, passkeyId: String) {
httpClient().delete(routes.delete(passkeyId)) {
addAuthenticationHeader(token)
}
}
+ //endregion
+
+ //region Migration
+
/**
* Get migration options (see [MigrationOptions]), prior to migration from kAuth.
*
* @param deviceId The id of the device.
* @param userId The id of the user.
*/
- suspend fun getMigrationOptions(deviceId: String, userId: Long): SuccessfulApiResponse {
+ suspend fun getMigrationOptions(deviceId: String, userId: Long): MigrationOptions {
return httpClient().post(routes.migrationsOptions()) {
setBody(mapOf("device" to deviceId, "id" to userId.toString()))
- }.decode()
+ }.decode>().data
}
/**
@@ -121,10 +137,10 @@ internal class AuthenticatorRequest(
suspend fun getTokenForMigration(
sessionId: String,
otpPayload: OtpPayload,
- ): SuccessfulApiResponse {
+ ): AuthResult {
return httpClient().post(routes.verifyMigration(sessionId)) {
setBody(otpPayload)
- }.decode()
+ }.decode>().data
}
/**
@@ -140,14 +156,16 @@ internal class AuthenticatorRequest(
}
}
+ //endregion
+
suspend fun getUserProfile(
token: String,
- ): SuccessfulApiResponse {
+ ): SharedUserProfile {
val url = "${routes.userProfile()}&with=security"
return httpClient().get(url) {
addAuthenticationHeader(token)
- }.decode()
+ }.decode>().data
}
private fun HttpRequestBuilder.addAuthenticationHeader(token: String) {