Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f55a959
chore: Move DerivedTokenGenerator to the Auth module
LouisCAD Jul 7, 2026
8e0d9e8
feat: Introduce the RestoreFromBackupManager interface
LouisCAD Jul 7, 2026
168a96c
chore: Add suspending allUsers() in UserDao
LouisCAD Jul 8, 2026
a65d485
feat: Add WIP RestoreFromBackupManagerImpl
LouisCAD Jul 8, 2026
833d251
chore: Forward issues from DerivedTokenGenerator in RestoreFromBackup…
LouisCAD Jul 9, 2026
621be18
chore: Store user token to device association in the db instead of a …
LouisCAD Jul 9, 2026
4524930
chore: Move ensureRestorationIsHandled from impl to the sealed class
LouisCAD Jul 21, 2026
890d8e0
chore: Add `shouldShowRestorationScreen` helper in RestoreFromBackupM…
LouisCAD Jul 21, 2026
ff0c5b2
feat: Auto retry post-restoration token derivation if appropriate
LouisCAD Jul 21, 2026
8f41981
fix: Use a getter to get the up-to-date userDataCleanableList
LouisCAD Jul 21, 2026
4c04e37
feat: Remove relevant users when the user gives up backup restoration
LouisCAD Jul 21, 2026
f88fd22
docs: Update CrossAppLogin README.md
LouisCAD Jul 21, 2026
5062360
fix: Fix crashes and race condition
LouisCAD Jul 21, 2026
9ccb9e2
feat: Add barebones RestoringFromBackupFailedScreen
LouisCAD Jul 21, 2026
19cfd5e
feat: Move landscape UI of backup restoration failed to the end
LouisCAD Jul 29, 2026
a9ff1f0
fix: Fix AccountUtils tests
LouisCAD Jul 29, 2026
392d7bc
fix: Ensure attestation tokens cannot be reused
LouisCAD Jul 30, 2026
42efec6
feat: Add strings
Elouan1411 Jul 31, 2026
eac4ad3
fix: Update UI
Elouan1411 Jul 31, 2026
b818fa1
chore: Extract nested when
LouisCAD Aug 3, 2026
c98e234
fix: Don't auto-retry if we received http 401
LouisCAD Aug 3, 2026
3b6b759
chore: Add braces to if branch
LouisCAD Aug 3, 2026
ce3de05
chore: Add fast-path for not set up app
LouisCAD Aug 3, 2026
2bb2848
feat: Support the Authenticator app
LouisCAD Aug 11, 2026
20da144
chore: Fix compilation by adding a default parameter value
LouisCAD Aug 11, 2026
66a859f
chore: Ensure we don't auto-retry http 400 requests
LouisCAD Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ dependencies {

implementation(platform(core.compose.bom))
implementation(core.compose.runtime)
implementation(core.compose.ui)
implementation(core.compose.material3)
implementation(core.activity.compose)
implementation(core.compose.ui.tooling.preview)
debugImplementation(core.compose.ui.tooling)

implementation(core.appcompat)
implementation(core.androidx.core.ktx)
Expand Down
203 changes: 203 additions & 0 deletions Auth/schemas/com.infomaniak.core.auth.room.UserDatabase/9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "5c16489c4961bd978d19b0d0c0bacb8d",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `firstname` TEXT NOT NULL, `lastname` TEXT NOT NULL, `email` TEXT NOT NULL, `avatar` TEXT, `login` TEXT NOT NULL, `isStaff` INTEGER NOT NULL DEFAULT false, `organizations` TEXT NOT NULL, `preferences_security_score` INTEGER DEFAULT 0, `preferences_security_dateLastChangedPassword` INTEGER DEFAULT 0, `preferences_organizationPreference_currentOrganizationId` INTEGER NOT NULL DEFAULT 0, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `tokenType` TEXT NOT NULL, `expiresIn` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `scope` TEXT, `expiresAt` INTEGER, `isTemporary` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "firstname",
"columnName": "firstname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastname",
"columnName": "lastname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT"
},
{
"fieldPath": "login",
"columnName": "login",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isStaff",
"columnName": "isStaff",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "organizations",
"columnName": "organizations",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "preferences.security.score",
"columnName": "preferences_security_score",
"affinity": "INTEGER",
"defaultValue": "0"
},
{
"fieldPath": "preferences.security.dateLastChangedPassword",
"columnName": "preferences_security_dateLastChangedPassword",
"affinity": "INTEGER",
"defaultValue": "0"
},
{
"fieldPath": "preferences.organizationPreference.currentOrganizationId",
"columnName": "preferences_organizationPreference_currentOrganizationId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "apiToken.accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiToken.refreshToken",
"columnName": "refreshToken",
"affinity": "TEXT"
},
{
"fieldPath": "apiToken.tokenType",
"columnName": "tokenType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiToken.expiresIn",
"columnName": "expiresIn",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "apiToken.userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "apiToken.scope",
"columnName": "scope",
"affinity": "TEXT"
},
{
"fieldPath": "apiToken.expiresAt",
"columnName": "expiresAt",
"affinity": "INTEGER"
},
{
"fieldPath": "apiToken.isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "CurrentUserId",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `uniqueKey` TEXT NOT NULL, PRIMARY KEY(`uniqueKey`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
},
{
"fieldPath": "uniqueKey",
"columnName": "uniqueKey",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uniqueKey"
]
}
},
{
"tableName": "TokenDeviceBinding",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `androidId` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `User`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "androidId",
"columnName": "androidId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId"
]
},
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5c16489c4961bd978d19b0d0c0bacb8d')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import androidx.annotation.CallSuper
import androidx.room.withTransaction
import com.infomaniak.core.auth.backup.RestoreFromBackupManager
import com.infomaniak.core.auth.models.user.User
import com.infomaniak.core.auth.room.UserDatabase
import com.infomaniak.core.common.AssociatedUserDataCleanable
Expand All @@ -38,9 +39,10 @@ import kotlinx.coroutines.flow.flowOf
*/
abstract class AbstractCurrentUserAccountUtils(
appContext: Context,
userDataCleanableList: List<AssociatedUserDataCleanable> = emptyList(),
userDataCleanableList: () -> List<AssociatedUserDataCleanable> = { emptyList() },
Comment thread
LouisCAD marked this conversation as resolved.
userDatabase: UserDatabase = UserDatabase.instantiateDataBase(appContext),
) : UserAccountUtils(appContext, userDataCleanableList, userDatabase) {
restoreFromBackupManager: RestoreFromBackupManager = RestoreFromBackupManager.instance,
) : UserAccountUtils(appContext, userDataCleanableList, userDatabase, restoreFromBackupManager) {

/**
* If you need a live [User] instead of just its id, use [currentUserFlow]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import androidx.annotation.CallSuper
import androidx.collection.ArrayMap
import com.infomaniak.core.auth.models.user.User
import com.infomaniak.core.auth.room.UserDatabase
import com.infomaniak.core.network.networking.HttpClientConfig
import com.infomaniak.core.login.ApiToken
import com.infomaniak.core.network.networking.HttpClientConfig
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Cache
Expand Down Expand Up @@ -115,7 +115,7 @@ abstract class BaseCredentialManager : UserExistenceChecker {

private suspend fun getDefaultTokenInterceptorListener(userId: Int): TokenInterceptorListener {
var user = userDatabase.userDao().findById(userId)
return object : TokenInterceptorListener {
return object : TokenInterceptorListener(dedicatedUserId = userId) {
override suspend fun onRefreshTokenSuccess(apiToken: ApiToken) {
setUserToken(user, apiToken)
}
Expand Down
13 changes: 11 additions & 2 deletions Auth/src/main/kotlin/com/infomaniak/core/auth/CredentialManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package com.infomaniak.core.auth

import androidx.lifecycle.LiveData
import com.infomaniak.core.auth.models.user.User
import com.infomaniak.core.auth.room.UserDatabase
import com.infomaniak.core.common.AssociatedUserDataCleanable
import com.infomaniak.core.login.ApiToken
import splitties.init.appCtx

/**
* CredentialManager: Adds a currentUserId and currentUser management layer to [BaseCredentialManager]
Expand All @@ -29,12 +32,18 @@ import com.infomaniak.core.login.ApiToken
* blocking methods which is fixed in the alternative classes.
*/
@Deprecated("It's recommended to use UserAccountUtils, AbstractCurrentUserAccountUtils or PersistedCurrentUserAccountUtils")
abstract class CredentialManager : BaseCredentialManager() {
abstract class CredentialManager(
userDataCleanableList: () -> List<AssociatedUserDataCleanable>
) : UserAccountUtils(
Comment thread
LouisCAD marked this conversation as resolved.
appContext = appCtx,
userDataCleanableList = userDataCleanableList,
userDatabase = UserDatabase.instance,
) {

abstract val currentUserId: Int
abstract var currentUser: User?

fun getAllUsers(): LiveData<List<User>> = userDatabase.userDao().getAll()
fun getAllUsers(): LiveData<List<User>> = userDatabase.userDao().allAsLiveData()

suspend fun getAllUsersCount(): Int = userDatabase.userDao().userCount()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Infomaniak Core - 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 <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.auth

import com.infomaniak.core.appintegrity.AppIntegrityIssue
import com.infomaniak.core.auth.DerivedTokenGenerator.Issue

fun Issue.shouldReport(): Boolean = when (this) {
is Issue.AppIntegrityCheckFailed -> false
is Issue.ErrorResponse -> response.code !in 500..599
is Issue.NetworkIssue -> false
is Issue.OtherIssue -> true
}

internal fun Issue.shouldRetryAutomatically(): Boolean = when (this) {
is Issue.AppIntegrityCheckFailed -> shouldRetryAutomatically()
is Issue.ErrorResponse -> shouldRetryAutomatically()
is Issue.NetworkIssue -> true
is Issue.OtherIssue -> false
}

private fun Issue.AppIntegrityCheckFailed.shouldRetryAutomatically(): Boolean = when (details.issue) {
is AppIntegrityIssue.RetryLater, is AppIntegrityIssue.Internal -> true
is AppIntegrityIssue.DeviceIssue, is AppIntegrityIssue.DevError, is AppIntegrityIssue.SuspiciousError -> false
}

private fun Issue.ErrorResponse.shouldRetryAutomatically(): Boolean = when (response.code) {
400, 401 -> false
else -> true
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.crossapplogin.back
package com.infomaniak.core.auth

import com.infomaniak.core.appintegrity.exceptions.AppIntegrityException
import com.infomaniak.core.common.Xor
import com.infomaniak.core.login.ApiToken
import okhttp3.Response

internal sealed interface DerivedTokenGenerator {
sealed interface DerivedTokenGenerator {

suspend fun attemptDerivingOneOfTheseTokens(tokensToTry: Set<String>): Xor<ApiToken, Issue>
suspend fun isAppIntegrityGuaranteedToFail(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.core.crossapplogin.back
package com.infomaniak.core.auth

import com.infomaniak.core.appintegrity.AppIntegrityIssue
import com.infomaniak.core.appintegrity.AppIntegrityManager
import com.infomaniak.core.appintegrity.AppIntegrityManager.Companion.APP_INTEGRITY_MANAGER_TAG
import com.infomaniak.core.appintegrity.exceptions.AppIntegrityException
import com.infomaniak.core.appintegrity.exceptions.NetworkException
import com.infomaniak.core.auth.DerivedTokenGenerator.Issue
import com.infomaniak.core.common.Xor
import com.infomaniak.core.common.cancellable
import com.infomaniak.core.crossapplogin.back.DerivedTokenGenerator.Issue
import com.infomaniak.core.login.ApiToken
import com.infomaniak.core.login.InfomaniakLogin
import com.infomaniak.core.network.api.ApiController
import com.infomaniak.core.network.utils.await
import com.infomaniak.core.network.utils.bodyAsStringOrNull
import com.infomaniak.core.sentry.SentryLog
import com.infomaniak.core.login.ApiToken
import com.infomaniak.core.login.InfomaniakLogin
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
Expand All @@ -39,9 +39,9 @@ import splitties.init.appCtx
import java.io.IOException
import kotlin.uuid.ExperimentalUuidApi

internal class DerivedTokenGeneratorImpl(
class DerivedTokenGeneratorImpl(
private val tokenRetrievalUrl: String,
private val hostAppPackageName: String,
private val hostAppPackageName: String = appCtx.packageName,
private val clientId: String,
private val userAgent: String,
private val accessType: InfomaniakLogin.AccessType? = null,
Expand Down
Loading
Loading