Harden Android biometric session flow and release process
This commit is contained in:
@@ -75,6 +75,7 @@ class LinkedSignRepository(private val context: Context) {
|
||||
)
|
||||
sessionStore.save(session)
|
||||
localCache.saveProfile(response.user)
|
||||
sessionStore.updateBiometricSession(response.user.email, response.refreshToken)
|
||||
session
|
||||
}
|
||||
}
|
||||
@@ -95,8 +96,9 @@ class LinkedSignRepository(private val context: Context) {
|
||||
suspend fun logout() {
|
||||
execute {
|
||||
val session = sessionStore.load()
|
||||
val biometric = sessionStore.loadBiometricState()
|
||||
try {
|
||||
if (!session.refreshToken.isNullOrBlank()) {
|
||||
if (!session.refreshToken.isNullOrBlank() && !biometric.enabled) {
|
||||
unwrap(service.logout(RefreshRequest(session.refreshToken)))
|
||||
}
|
||||
} finally {
|
||||
@@ -106,23 +108,41 @@ class LinkedSignRepository(private val context: Context) {
|
||||
}
|
||||
|
||||
suspend fun biometricLogin(): SessionSnapshot {
|
||||
val credentials = sessionStore.loadBiometricCredentials()
|
||||
?: throw AppException("No hay credenciales biométricas guardadas.")
|
||||
return login(credentials.first, credentials.second)
|
||||
val biometric = sessionStore.loadBiometricRefreshToken()
|
||||
?: throw AppException("No hay sesión biométrica guardada.")
|
||||
return execute {
|
||||
val refresh = unwrap(service.refresh(RefreshRequest(biometric.second)))
|
||||
val user = unwrap(service.me("Bearer ${refresh.accessToken}")).user
|
||||
val session = SessionSnapshot(
|
||||
accessToken = refresh.accessToken,
|
||||
refreshToken = refresh.refreshToken,
|
||||
user = user
|
||||
)
|
||||
sessionStore.save(session)
|
||||
localCache.saveProfile(user)
|
||||
sessionStore.updateBiometricSession(user.email, refresh.refreshToken)
|
||||
session
|
||||
}
|
||||
}
|
||||
|
||||
fun biometricState(): BiometricAccessState = sessionStore.loadBiometricState()
|
||||
|
||||
fun enableBiometricAccess(email: String, password: String) {
|
||||
sessionStore.saveBiometricAccess(email.trim(), password)
|
||||
fun enableBiometricAccess() {
|
||||
val session = sessionStore.load()
|
||||
val user = session.user ?: throw AppException("Primero debes iniciar sesión.")
|
||||
val refreshToken = session.refreshToken ?: throw AppException("No hay sesión para activar biometría.")
|
||||
sessionStore.saveBiometricAccess(user.email, refreshToken)
|
||||
}
|
||||
|
||||
fun declineBiometricAccess() {
|
||||
sessionStore.declineBiometricAccess()
|
||||
}
|
||||
|
||||
fun syncBiometricCredentials(email: String, password: String) {
|
||||
sessionStore.updateBiometricCredentials(email.trim(), password)
|
||||
fun syncBiometricSession() {
|
||||
val session = sessionStore.load()
|
||||
val user = session.user ?: return
|
||||
val refreshToken = session.refreshToken ?: return
|
||||
sessionStore.updateBiometricSession(user.email, refreshToken)
|
||||
}
|
||||
|
||||
suspend fun requestPasswordChange(currentPassword: String, newPassword: String): SimpleMessageData =
|
||||
@@ -201,6 +221,7 @@ class LinkedSignRepository(private val context: Context) {
|
||||
refreshToken = refresh.refreshToken
|
||||
)
|
||||
sessionStore.save(updated)
|
||||
sessionStore.updateBiometricSession(updated.user?.email ?: session.user?.email.orEmpty(), refresh.refreshToken)
|
||||
block("Bearer ${refresh.accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,22 +54,22 @@ class SessionStore(context: Context) {
|
||||
val enabled = preferences.getBoolean(KEY_BIO_ENABLED, false)
|
||||
val configured = preferences.getBoolean(KEY_BIO_CONFIGURED, false)
|
||||
val email = preferences.getString(KEY_BIO_EMAIL, null)
|
||||
val password = preferences.getString(KEY_BIO_PASSWORD, null)
|
||||
val refreshToken = preferences.getString(KEY_BIO_REFRESH, null)
|
||||
|
||||
return BiometricAccessState(
|
||||
enabled = enabled,
|
||||
configured = configured,
|
||||
email = email,
|
||||
hasCredentials = !email.isNullOrBlank() && !password.isNullOrBlank()
|
||||
hasCredentials = !email.isNullOrBlank() && !refreshToken.isNullOrBlank()
|
||||
)
|
||||
}
|
||||
|
||||
fun saveBiometricAccess(email: String, password: String) {
|
||||
fun saveBiometricAccess(email: String, refreshToken: String) {
|
||||
preferences.edit()
|
||||
.putBoolean(KEY_BIO_ENABLED, true)
|
||||
.putBoolean(KEY_BIO_CONFIGURED, true)
|
||||
.putString(KEY_BIO_EMAIL, email)
|
||||
.putString(KEY_BIO_PASSWORD, password)
|
||||
.putString(KEY_BIO_REFRESH, refreshToken)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -78,28 +78,28 @@ class SessionStore(context: Context) {
|
||||
.putBoolean(KEY_BIO_ENABLED, false)
|
||||
.putBoolean(KEY_BIO_CONFIGURED, true)
|
||||
.remove(KEY_BIO_EMAIL)
|
||||
.remove(KEY_BIO_PASSWORD)
|
||||
.remove(KEY_BIO_REFRESH)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun updateBiometricCredentials(email: String, password: String) {
|
||||
fun updateBiometricSession(email: String, refreshToken: String) {
|
||||
if (!preferences.getBoolean(KEY_BIO_ENABLED, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
preferences.edit()
|
||||
.putString(KEY_BIO_EMAIL, email)
|
||||
.putString(KEY_BIO_PASSWORD, password)
|
||||
.putString(KEY_BIO_REFRESH, refreshToken)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun loadBiometricCredentials(): Pair<String, String>? {
|
||||
fun loadBiometricRefreshToken(): Pair<String, String>? {
|
||||
val email = preferences.getString(KEY_BIO_EMAIL, null)
|
||||
val password = preferences.getString(KEY_BIO_PASSWORD, null)
|
||||
if (email.isNullOrBlank() || password.isNullOrBlank()) {
|
||||
val refreshToken = preferences.getString(KEY_BIO_REFRESH, null)
|
||||
if (email.isNullOrBlank() || refreshToken.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
return email to password
|
||||
return email to refreshToken
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -109,6 +109,6 @@ class SessionStore(context: Context) {
|
||||
private const val KEY_BIO_ENABLED = "bio_enabled"
|
||||
private const val KEY_BIO_CONFIGURED = "bio_configured"
|
||||
private const val KEY_BIO_EMAIL = "bio_email"
|
||||
private const val KEY_BIO_PASSWORD = "bio_password"
|
||||
private const val KEY_BIO_REFRESH = "bio_refresh_token"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,8 +205,6 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
val biometricAvailable = remember(activity) { canUseBiometricAccess(activity) }
|
||||
var appUnlocked by rememberSaveable { mutableStateOf(false) }
|
||||
var bootstrapGateHandled by rememberSaveable { mutableStateOf(false) }
|
||||
var pendingBiometricEmail by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var pendingBiometricPassword by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var showBiometricOptIn by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -354,8 +352,6 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
.onSuccess {
|
||||
selectedTab = AppTab.Documents
|
||||
appUnlocked = true
|
||||
pendingBiometricEmail = email.trim()
|
||||
pendingBiometricPassword = password
|
||||
showBiometricOptIn = biometricAvailable && !state.biometricConfigured
|
||||
snackbarHostState.showSnackbar("Sesión iniciada.")
|
||||
}
|
||||
@@ -434,13 +430,11 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
}
|
||||
}
|
||||
|
||||
if (showBiometricOptIn && pendingBiometricEmail != null && pendingBiometricPassword != null) {
|
||||
if (showBiometricOptIn) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.declineBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
pendingBiometricEmail = null
|
||||
pendingBiometricPassword = null
|
||||
},
|
||||
title = { Text("Activar acceso biométrico") },
|
||||
text = {
|
||||
@@ -448,13 +442,8 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
viewModel.enableBiometricAccess(
|
||||
pendingBiometricEmail.orEmpty(),
|
||||
pendingBiometricPassword.orEmpty()
|
||||
)
|
||||
viewModel.enableBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
pendingBiometricEmail = null
|
||||
pendingBiometricPassword = null
|
||||
}) {
|
||||
Text("Sí, usar huella")
|
||||
}
|
||||
@@ -463,8 +452,6 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.declineBiometricAccess()
|
||||
showBiometricOptIn = false
|
||||
pendingBiometricEmail = null
|
||||
pendingBiometricPassword = null
|
||||
}) {
|
||||
Text("Ahora no")
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
suspend fun login(email: String, password: String) {
|
||||
val session = repository.login(email, password)
|
||||
repository.syncBiometricCredentials(email, password)
|
||||
repository.syncBiometricSession()
|
||||
val (_, cache) = repository.loadLocalSnapshot()
|
||||
uiState = buildState(session, cache, loading = false)
|
||||
}
|
||||
@@ -145,8 +145,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
fun accessToken(): String? = repository.getAccessToken()
|
||||
|
||||
fun enableBiometricAccess(email: String, password: String) {
|
||||
repository.enableBiometricAccess(email, password)
|
||||
fun enableBiometricAccess() {
|
||||
repository.enableBiometricAccess()
|
||||
refreshState()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user