diff --git a/Core b/Core index 308bf87..cc66df0 160000 --- a/Core +++ b/Core @@ -1 +1 @@ -Subproject commit 308bf878381e5c1ce989f7d4ad1d0aead844ca44 +Subproject commit cc66df014eee602c935a48719ba14c328e421810 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e807906..e0f7b8e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ kotlin = "2.3.0" kotlinxCollectionsImmutable = "0.4.0" ksp = "2.3.4" +ktor = "3.3.0" lifecycleViewmodelNav3 = "2.10.0" nav3Core = "1.0.0" room = "2.8.4" @@ -16,6 +17,13 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } +ktor-client-content-negociation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" } +ktor-client-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } infomaniak-core-auth = { module = "com.infomaniak.core:Auth" } infomaniak-core-common = { module = "com.infomaniak.core:Common" } diff --git a/multiplatform-lib/build.gradle.kts b/multiplatform-lib/build.gradle.kts index f32c2e9..179e531 100644 --- a/multiplatform-lib/build.gradle.kts +++ b/multiplatform-lib/build.gradle.kts @@ -52,20 +52,32 @@ kotlin { commonMain { dependencies { implementation(core.kotlinx.coroutines.core) + implementation(core.kotlinx.serialization.cbor) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.content.negociation) + implementation(libs.ktor.client.json) + implementation(libs.ktor.client.encoding) } } commonTest { dependencies { implementation(kotlin("test")) implementation(core.kotlinx.coroutines.test) + implementation(libs.ktor.client.mock) } } androidMain { dependencies { + implementation(libs.ktor.client.okhttp) implementation(core.splitties.appctx) implementation(core.splitties.bitflags) } } + iosMain { + dependencies { + implementation(libs.ktor.client.darwin) + } + } val androidDeviceTest by getting { dependencies { implementation(core.androidx.junit) diff --git a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorInjection.kt b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorInjection.kt new file mode 100644 index 0000000..0e7ebb4 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorInjection.kt @@ -0,0 +1,50 @@ +/* + * 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 . + */ +import com.infomaniak.auth.lib.network.ApiClientProvider +import com.infomaniak.auth.lib.network.interfaces.CrashReportInterface +import network.repositories.WebAuthnRepository +import network.utils.ApiEnvironment + +/* + 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 . + */ + +class AuthenticatorInjection( + private val environment: ApiEnvironment, + private val userAgent: String, + private val databaseRootDirectory: String? = null, + private val crashReport: CrashReportInterface, +) { + private val apiClientProvider by lazy { ApiClientProvider(userAgent, crashReport) } + + private val webAuthnRepository by lazy { WebAuthnRepository(apiClientProvider, environment) } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/ApiClientProvider.kt b/multiplatform-lib/src/commonMain/kotlin/network/ApiClientProvider.kt new file mode 100644 index 0000000..d1be0a8 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/ApiClientProvider.kt @@ -0,0 +1,164 @@ +/* + * 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.network + +import com.infomaniak.auth.lib.network.interfaces.BreadcrumbType +import com.infomaniak.auth.lib.network.interfaces.CrashReportInterface +import com.infomaniak.auth.lib.network.interfaces.CrashReportLevel +import com.infomaniak.auth.lib.network.models.ApiError +import com.infomaniak.auth.lib.network.utils.getRequestContextId +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.HttpResponseValidator +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.request +import io.ktor.http.contentLength +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.CancellationException +import kotlinx.io.IOException +import kotlinx.serialization.json.Json +import network.exceptions.ApiException +import network.exceptions.ApiException.ApiErrorException +import network.exceptions.ApiException.UnexpectedApiErrorFormatException +import network.exceptions.NetworkException +import kotlin.time.Duration.Companion.seconds + +class ApiClientProvider internal constructor( + engine: HttpClientEngine? = null, + // When you don't use AuthenticatorInjection, you don't have an userAgent, so we're currently setting a default value. + // See later how to improve it. + private val userAgent: String = "Ktor client", + private val crashReport: CrashReportInterface? = null, +) { + + constructor() : this(null) + constructor(userAgent: String, crashReport: CrashReportInterface) : this( + engine = null, + userAgent = userAgent, + crashReport = crashReport, + ) + + val json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + isLenient = true + useAlternativeNames = false + } + + val httpClient = createHttpClient(engine) + + fun createHttpClient(engine: HttpClientEngine?): HttpClient { + val block: HttpClientConfig<*>.() -> Unit = { + install(UserAgent) { + agent = userAgent + } + install(ContentNegotiation) { + json(this@ApiClientProvider.json) + } + install(ContentEncoding) { + gzip() + } + install(HttpTimeout) { + // Each value can be fine-tuned independently, hence the value not being shared. + requestTimeoutMillis = 10.seconds.inWholeMilliseconds + connectTimeoutMillis = 10.seconds.inWholeMilliseconds + socketTimeoutMillis = 10.seconds.inWholeMilliseconds + } + install(HttpRequestRetry) { + retryOnExceptionIf(maxRetries = MAX_RETRY) { _, cause -> + cause.isNetworkException() + } + delayMillis { retry -> + retry * 500L + } + } + HttpResponseValidator { + validateResponse { response: HttpResponse -> + val requestContextId = response.getRequestContextId() + val statusCode = response.status.value + + addSentryUrlBreadcrumb(response, statusCode, requestContextId) + + if (statusCode >= 300) { + val bodyResponse = response.bodyAsText() + val apiError = runCatching { + json.decodeFromString(bodyResponse) + }.getOrElse { + throw UnexpectedApiErrorFormatException(statusCode, bodyResponse, null, requestContextId) + } + throw ApiErrorException(apiError.errorCode, apiError.message, requestContextId) + } + } + handleResponseExceptionWithRequest { cause, request -> + when (cause) { + is IOException -> throw NetworkException("Network error: ${cause.message}") + is ApiException, is CancellationException -> throw cause + else -> { + val response = runCatching { request.call.response }.getOrNull() + val requestContextId = response?.getRequestContextId() ?: "" + val bodyResponse = response?.bodyAsText() ?: cause.message ?: "" + val statusCode = response?.status?.value ?: -1 + throw UnexpectedApiErrorFormatException( + statusCode, + bodyResponse, + cause, + requestContextId + ) + } + } + } + } + } + + return if (engine != null) HttpClient(engine, block) else HttpClient(block) + } + + private fun addSentryUrlBreadcrumb(response: HttpResponse, statusCode: Int, requestContextId: String) { + val requestUrl = response.request.url + val data = buildMap { + put("url", "${requestUrl.protocol.name}://${requestUrl.host}${requestUrl.encodedPath}") + put("method", response.request.method.value) + put("status_code", "$statusCode") + if (requestUrl.encodedQuery.isNotEmpty()) put("http.query", requestUrl.encodedQuery) + put("request_id", requestContextId) + put("http.start_timestamp", "${response.requestTime.timestamp}") + put("http.end_timestamp", "${response.responseTime.timestamp}") + response.contentLength()?.let { put("response_content_length", "$it") } + } + crashReport?.addBreadcrumb( + message = "", + category = "http", + level = CrashReportLevel.INFO, + type = BreadcrumbType.HTTP, + data = data + ) + } + + private fun Throwable.isNetworkException() = this is IOException + + companion object { + private const val MAX_RETRY = 3 + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/exceptions/ApiException.kt b/multiplatform-lib/src/commonMain/kotlin/network/exceptions/ApiException.kt new file mode 100644 index 0000000..c00ec4e --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/exceptions/ApiException.kt @@ -0,0 +1,69 @@ +/* + * 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 network.exceptions + +/** + * Parent class of API calls exception. + * + * This exception is used to represent errors returned by an API, with an associated [requestContextId] + * and message describing the problem. + * + * @param errorMessage The detailed error message explaining the cause of the failure. + * @param cause The cause of the exception if exists otherwise null + * @property requestContextId The request context id used to track what happened during calls session by the backend + */ +sealed class ApiException( + errorMessage: String, + cause: Throwable?, + val requestContextId: String, +) : Exception(errorMessage, cause) { + + /** + * Thrown when an API call fails due to an error identified by a specific error code. + * + * This exception is used to represent errors returned by an API, with an associated error code + * and message describing the problem. + * + * @property errorCode The specific error code returned by the API. + * @property errorMessage The detailed error message explaining the cause of the failure. + * @param requestContextId The request context id send by the backend to track the call + */ + open class ApiErrorException( + val errorCode: Int, + val errorMessage: String, + requestContextId: String, + ) : ApiException(errorMessage, null, requestContextId) + + /** + * Thrown when an API call returns an error in an unexpected format that cannot be parsed. + * + * This exception indicates that the API response format is different from what was expected, + * preventing proper parsing of the error details. + * + * @property statusCode The HTTP status code returned by the API. + * @property bodyResponse The raw response body from the API that could not be parsed. + * @param cause The cause of the exception if exists otherwise null + * @param requestContextId The request context id send by the backend to track the call + */ + class UnexpectedApiErrorFormatException( + val statusCode: Int, + val bodyResponse: String, + cause: Throwable?, + requestContextId: String, + ) : ApiException(bodyResponse, cause, requestContextId) +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/exceptions/NetworkException.kt b/multiplatform-lib/src/commonMain/kotlin/network/exceptions/NetworkException.kt new file mode 100644 index 0000000..b78ac85 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/exceptions/NetworkException.kt @@ -0,0 +1,25 @@ +/* + * 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 network.exceptions + +/** + * Thrown when a network-related error occurs, such as connectivity issues or timeouts. + * + * @param message A detailed message describing the network error. + */ +class NetworkException(message: String) : Exception(message) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/exceptions/UnknownException.kt b/multiplatform-lib/src/commonMain/kotlin/network/exceptions/UnknownException.kt new file mode 100644 index 0000000..8d4e81a --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/exceptions/UnknownException.kt @@ -0,0 +1,34 @@ +/* + * 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.network.exceptions + +/** + * Represents an unknown exception that can occur during the execution of the application. + * + * This exception is used to encapsulate unexpected or unknown errors that are not covered + * by other specific exception types. + * + * @constructor Creates an instance of `UnknownException` with a detailed error message and an optional cause. + * + * @param cause The underlying exception that caused this exception. + * + * @property message The detailed message describing the error. + */ +class UnknownException(cause: Throwable) : Exception(cause) { + override val message: String = cause.message ?: cause.toString() +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/interfaces/CrashReportInterface.kt b/multiplatform-lib/src/commonMain/kotlin/network/interfaces/CrashReportInterface.kt new file mode 100644 index 0000000..d8c1803 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/interfaces/CrashReportInterface.kt @@ -0,0 +1,86 @@ +/* + * 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.network.interfaces + +enum class CrashReportLevel { + DEBUG, + INFO, + WARNING, + ERROR, + FATAL +} + +/** + * Breadcrumb types for Sentry. Controls how the icon/color is displayed in the UI on Sentry's webpage. + * + * @param value The value used in the JSON payload. + * @see Sentry Documentation + */ +enum class BreadcrumbType(val value: String) { + Default("default"), + HTTP("http"), +} + +interface CrashReportInterface { + /** + * Adds a breadcrumb to the crash reporting system to provide contextual information + * leading up to a potential crash. + * + * @param message A descriptive message for the breadcrumb, explaining the event or action. + * @param category A category string to group related breadcrumbs (e.g., "UI", "Network"). + * @param level The severity level of the breadcrumb (e.g., info, warning, error). + * @param type Sentry internal attribute that controls how breadcrumbs are categorized. + * @param data Optional additional data providing more context about the event. + */ + fun addBreadcrumb( + message: String, + category: String, + level: CrashReportLevel, + type: BreadcrumbType = BreadcrumbType.Default, + data: Map? = null, + ) + + /** + * Captures and reports an error to the crash reporting system with optional context + * and additional metadata. + * + * @param message A custom message to be reported (e.g., an error message or event description). + * @param error The [Throwable] to be reported. + * @param data Optional contextual data to provide more insight into the environment or state when the error occurred. + */ + fun capture( + message: String, + error: Throwable, + data: Map? = null, + ) + + /** + * Captures a custom message and reports it to the crash reporting system with optional context, + * severity level, and additional metadata. + * + * @param message The custom message to be reported (e.g., an error message or event description). + * @param data Optional contextual data that provides additional information about the environment + * or state when the message was logged. + * @param level The severity level of the message (e.g., `info`, `warning`, `error`). + */ + fun capture( + message: String, + data: Map? = null, + level: CrashReportLevel? = null + ) +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/AllowCredential.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/AllowCredential.kt new file mode 100644 index 0000000..1f8fa26 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/AllowCredential.kt @@ -0,0 +1,27 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class AllowCredential( + val type: String, + val id: String, + val transports: List, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/ApiError.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/ApiError.kt new file mode 100644 index 0000000..076dd41 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/ApiError.kt @@ -0,0 +1,26 @@ +/* + * 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.network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ApiError( + val errorCode: Int, + val message: String, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/AuthResult.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthResult.kt new file mode 100644 index 0000000..55622c6 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthResult.kt @@ -0,0 +1,32 @@ +/* + * 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 network.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AuthResult( + @SerialName("token_type") + val tokenType: String, + val scope: String, + @SerialName("user_id") + val userId: Long, + @SerialName("access_token") + val accessToken: String, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/AuthentificationOptions.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthentificationOptions.kt new file mode 100644 index 0000000..ca06ef0 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/AuthentificationOptions.kt @@ -0,0 +1,29 @@ +/* + * 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 network.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AuthenticationOptions( + val challenge: String, + @SerialName("rpId") + val relyingPartyId: String, + val allowCredentials: List, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/ClientExtensionResults.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/ClientExtensionResults.kt new file mode 100644 index 0000000..4da21af --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/ClientExtensionResults.kt @@ -0,0 +1,23 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data object ClientExtensionResults // TODO Don't know what's inside this diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/ExcludeCredential.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/ExcludeCredential.kt new file mode 100644 index 0000000..cd34ec8 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/ExcludeCredential.kt @@ -0,0 +1,26 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ExcludeCredential( + val id: String, + val type: String, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/PasskeysOptions.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/PasskeysOptions.kt new file mode 100644 index 0000000..53e1807 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/PasskeysOptions.kt @@ -0,0 +1,31 @@ +/* + * 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 network.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PasskeysOptions( + val challenge: String, + @SerialName("rp") + val relyingParty: RelyingParty, + val user: User, + val pubKeyCredParams: List, + val excludeCredentials: List, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/PubKeyCredParam.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/PubKeyCredParam.kt new file mode 100644 index 0000000..bcdb5ca --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/PubKeyCredParam.kt @@ -0,0 +1,28 @@ +/* + * 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 network.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PubKeyCredParam( + val type: String, + @SerialName("alg") + val algorithm: Int, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskey.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskey.kt new file mode 100644 index 0000000..8fadffa --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskey.kt @@ -0,0 +1,33 @@ +/* + * 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 network.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RegisterPasskey( + val device: String, + val id: String, + val rawId: String, + @SerialName("response") + val registerPasskeyResponse: RegisterPasskeyResponse, + val type: String, + val clientExtensionResults: ClientExtensionResults, + val authenticatorAttachment: String, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskeyResponse.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskeyResponse.kt new file mode 100644 index 0000000..e7d1e38 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/RegisterPasskeyResponse.kt @@ -0,0 +1,30 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class RegisterPasskeyResponse( + val attestationObject: String, + val clientDataJSON: String, + val transports: List, + val publicKeyAlgorithm: Int, + val publicKey: String, + val authenticatorData: String, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/RelyingParty.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/RelyingParty.kt new file mode 100644 index 0000000..3b05599 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/RelyingParty.kt @@ -0,0 +1,27 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class RelyingParty( + val id: String, + val name: String, + val icon: String?, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/User.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/User.kt new file mode 100644 index 0000000..77e7b3c --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/User.kt @@ -0,0 +1,27 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class User( + val id: String, + val name: String, + val displayName: String?, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyAuthenticationData.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyAuthenticationData.kt new file mode 100644 index 0000000..f8d8810 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyAuthenticationData.kt @@ -0,0 +1,31 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class VerifyAuthenticationData( + val identity: Long, + val id: String, + val rawId: String, + val response: VerifyResponse, + val type: String, + val clientExtensionResults: ClientExtensionResults, + val authenticatorAttachment: String, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyResponse.kt b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyResponse.kt new file mode 100644 index 0000000..e69d8b3 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/models/VerifyResponse.kt @@ -0,0 +1,28 @@ +/* + * 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 network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class VerifyResponse( + val authenticatorData: String, + val clientDataJSON: String, + val signature: String, + val userHandle: String, +) diff --git a/multiplatform-lib/src/commonMain/kotlin/network/repositories/WebAuthnRepository.kt b/multiplatform-lib/src/commonMain/kotlin/network/repositories/WebAuthnRepository.kt new file mode 100644 index 0000000..0a5977b --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/repositories/WebAuthnRepository.kt @@ -0,0 +1,84 @@ +/* + * 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 network.repositories + +import com.infomaniak.auth.lib.network.ApiClientProvider +import io.ktor.client.HttpClient +import kotlinx.serialization.json.Json +import network.models.AuthResult +import network.models.AuthenticationOptions +import network.models.ClientExtensionResults +import network.models.PasskeysOptions +import network.models.RegisterPasskey +import network.models.VerifyAuthenticationData +import network.models.VerifyResponse +import network.requests.AuthenticatorRequest +import network.utils.ApiEnvironment + +class WebAuthnRepository internal constructor(private val authenticatorRequest: AuthenticatorRequest) { + + constructor(environment: ApiEnvironment) : this(ApiClientProvider(), environment) + constructor(apiClientProvider: ApiClientProvider = ApiClientProvider(), environment: ApiEnvironment) : this( + environment = environment, + json = apiClientProvider.json, + httpClient = apiClientProvider.httpClient, + ) + + internal constructor(environment: ApiEnvironment, json: Json, httpClient: HttpClient) : + this(AuthenticatorRequest(environment, json, httpClient)) + + // Authentification challenge (not authentified) + suspend fun getAuthenticationOptions(identity: Long): AuthenticationOptions { + return authenticatorRequest.challenge(identity) + } + + // Authentification verification (not authentified) + suspend fun verifyAuthentication( + identity: Long, + id: String, + rawId: String, + verifyResponse: VerifyResponse, + type: String, + clientExtensionResult: ClientExtensionResults, + authenticatorAttachment: String, + ): AuthResult { + return authenticatorRequest.verify( + VerifyAuthenticationData( + identity, + id, + rawId, + verifyResponse, + type, + clientExtensionResult, + authenticatorAttachment, + ) + ) + } + + // Generate WebAuthn registration options (authentified) + suspend fun getRegistrationOptions(): PasskeysOptions { + //TODO where to get the bearer ? + return authenticatorRequest.getPasskeysOptions() + } + + // Validate WebAuthn registration and save public key (authentified) + suspend fun registerCredential(registerPasskey: RegisterPasskey): Result { + //TODO where to get the bearer ? + return authenticatorRequest.registerPasskey(registerPasskey) + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/requests/AuthenticatorRequest.kt b/multiplatform-lib/src/commonMain/kotlin/network/requests/AuthenticatorRequest.kt new file mode 100644 index 0000000..dc49f90 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/requests/AuthenticatorRequest.kt @@ -0,0 +1,51 @@ +/* + * 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 network.requests + +import io.ktor.client.HttpClient +import kotlinx.serialization.json.Json +import network.models.AuthResult +import network.models.AuthenticationOptions +import network.models.PasskeysOptions +import network.models.RegisterPasskey +import network.models.VerifyAuthenticationData +import network.utils.ApiEnvironment +import network.utils.ApiRoutes + +internal class AuthenticatorRequest( + environment: ApiEnvironment, + json: Json, + httpClient: HttpClient, +) : BaseRequest(environment, json, httpClient) { + + suspend fun getPasskeysOptions(): PasskeysOptions { + return get(createUrl(ApiRoutes.getPasskeysOptions)) + } + + suspend fun registerPasskey(registerPasskey: RegisterPasskey): Result { + return post(createUrl(ApiRoutes.registerPasskey), registerPasskey) + } + + suspend fun challenge(identity: Long): AuthenticationOptions { + return post(createUrl(ApiRoutes.challenge), mapOf("identity" to identity)) + } + + suspend fun verify(verifyAuthenticationData: VerifyAuthenticationData): AuthResult { + return post(createUrl(ApiRoutes.verify), verifyAuthenticationData) + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/requests/BaseRequest.kt b/multiplatform-lib/src/commonMain/kotlin/network/requests/BaseRequest.kt new file mode 100644 index 0000000..2cc4c9b --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/requests/BaseRequest.kt @@ -0,0 +1,82 @@ +/* + * 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 network.requests + +import com.infomaniak.auth.lib.network.utils.decode +import io.ktor.client.HttpClient +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.client.request.post +import io.ktor.client.request.put +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HeadersBuilder +import io.ktor.http.URLBuilder +import io.ktor.http.Url +import io.ktor.http.contentType +import kotlinx.serialization.json.Json +import network.utils.ApiEnvironment +import network.utils.ApiRoutes + +internal open class BaseRequest( + protected val environment: ApiEnvironment, + protected val json: Json, + protected val httpClient: HttpClient, +) { + + protected fun createUrl(path: String, vararg queries: Pair): Url { + val baseUrl = Url(ApiRoutes.apiBaseUrl(environment) + path) + return URLBuilder(baseUrl).apply { + queries.forEach { parameters.append(it.first, it.second) } + }.build() + } + + protected suspend inline fun get( + url: Url, + crossinline appendHeaders: HeadersBuilder.() -> Unit = {}, + httpClient: HttpClient = this.httpClient, + ): R { + return httpClient.get(url) { + headers { appendHeaders() } + }.decode() + } + + protected suspend inline fun post( + url: Url, data: Any?, + crossinline appendHeaders: HeadersBuilder.() -> Unit = {}, + httpClient: HttpClient = this.httpClient, + ): R { + return httpClient.post(url) { + contentType(ContentType.Application.Json) + headers { appendHeaders() } + setBody(data) + }.decode() + } + + protected suspend inline fun put(url: Url, data: Any?, httpClient: HttpClient = this.httpClient): R { + return httpClient.put(url) { + contentType(ContentType.Application.Json) + setBody(data) + }.decode() + } + + protected suspend inline fun delete(url: Url, httpClient: HttpClient = this.httpClient): R { + return httpClient.delete(url) {}.decode() + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiEnvironment.kt b/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiEnvironment.kt new file mode 100644 index 0000000..ec6c709 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiEnvironment.kt @@ -0,0 +1,27 @@ +/* + * 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 network.utils + +sealed class ApiEnvironment(val baseUrl: String) { + // Those urls are duplicated with the ones we have in Android so don't forget to change them also in Android + data object Preprod : + ApiEnvironment("https://authenticator.preprod.dev.infomaniak.ch") //TODO Change this to the final baseUrl + + data object Prod : ApiEnvironment("https://www.authenticator.com") //TODO Change this to the final baseUrl + data class Custom(private val url: String) : ApiEnvironment(url) +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiExt.kt b/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiExt.kt new file mode 100644 index 0000000..e1276bb --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiExt.kt @@ -0,0 +1,51 @@ +/* + * 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.network.utils + +import com.infomaniak.auth.lib.network.exceptions.UnknownException +import io.ktor.client.call.body +import io.ktor.client.plugins.timeout +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.statement.HttpResponse +import io.ktor.utils.io.CancellationException +import network.exceptions.ApiException +import network.exceptions.NetworkException +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +const val CONTENT_REQUEST_ID_HEADER = "x-request-id" + +internal fun HttpRequestBuilder.longTimeout() { + timeout { + requestTimeoutMillis = 1.hours.inWholeMilliseconds // Be permissive for DSL or slow mobile connections. + connectTimeoutMillis = 10.seconds.inWholeMilliseconds // Don't let the user wait too long for connection. + socketTimeoutMillis = 1.minutes.inWholeMilliseconds // Be permissive if packets are slow to be delivered. + } +} + +internal fun HttpResponse.getRequestContextId() = headers[CONTENT_REQUEST_ID_HEADER] ?: "" + +internal suspend inline fun HttpResponse.decode(): R = runCatching { + body() +}.getOrElse { exception -> + when (exception) { + is CancellationException, is NetworkException, is ApiException -> throw exception + else -> throw UnknownException(exception) + } +} diff --git a/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiRoutes.kt b/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiRoutes.kt new file mode 100644 index 0000000..2b81539 --- /dev/null +++ b/multiplatform-lib/src/commonMain/kotlin/network/utils/ApiRoutes.kt @@ -0,0 +1,28 @@ +/* + * 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 network.utils + +internal object ApiRoutes { + + fun apiBaseUrl(environment: ApiEnvironment) = "${environment.baseUrl}/api/authenticator/" + + const val getPasskeysOptions = "passkeys/options" + const val registerPasskey = "passkey" + const val challenge = "challenge" + const val verify = "verify" +}