Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import com.amplifyframework.ui.authenticator.data.AuthenticationFlow
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
import com.amplifyframework.ui.authenticator.forms.SignUpFormBuilder
import com.amplifyframework.ui.authenticator.options.TotpOptions
import com.amplifyframework.ui.authenticator.util.AuthenticatorMessage
import com.amplifyframework.ui.authenticator.util.findActivity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
Expand All @@ -52,6 +54,8 @@ fun rememberAuthenticatorState(
): AuthenticatorState {
val viewModel = viewModel<AuthenticatorViewModel>()
val scope = rememberCoroutineScope()
val context = LocalContext.current

return remember {
val configuration = AuthenticatorConfiguration(
initialStep = initialStep,
Expand All @@ -60,7 +64,7 @@ fun rememberAuthenticatorState(
authenticationFlow = authenticationFlow
)

viewModel.start(configuration)
viewModel.start(configuration, context.findActivity())
AuthenticatorStateImpl(viewModel).also { state ->
viewModel.stepState.onEach { state.stepState = it }.launchIn(scope)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ interface AuthenticatorStepState {
val step: AuthenticatorStep
}

/**
* A state holder for the UI that has multiple possible actions that may be in progress.
*/
@Stable
interface AuthenticatorActionState<T> {
/**
* The action in progress, or null if state is idle
*/
val action: T?
}

/**
* The Authenticator is loading the current state of the user's Auth session.
*/
Expand Down Expand Up @@ -99,7 +110,17 @@ interface SignInState : AuthenticatorStepState {
* The user has entered their username and must select the authentication factor they'd like to use to sign in
*/
@Stable
interface SignInSelectAuthFactorState : AuthenticatorStepState {
interface SignInSelectAuthFactorState :
AuthenticatorStepState,
AuthenticatorActionState<SignInSelectAuthFactorState.Action> {

sealed interface Action {
/**
* User has selected an auth factor
*/
data class SelectFactor(val factor: AuthFactor) : Action
}

/**
* The input form state holder for this step.
*/
Expand All @@ -115,11 +136,6 @@ interface SignInSelectAuthFactorState : AuthenticatorStepState {
*/
val availableAuthFactors: Set<AuthFactor>

/**
* The factor the user selected and is currently being processed
*/
val selectedFactor: AuthFactor?

/**
* Move the user to a different [AuthenticatorInitialStep].
*/
Expand Down Expand Up @@ -530,7 +546,21 @@ interface VerifyUserConfirmState : AuthenticatorStepState {
* via biometrics
*/
@Stable
interface PasskeyCreationPromptState : AuthenticatorStepState {
interface PasskeyCreationPromptState :
AuthenticatorStepState,
AuthenticatorActionState<PasskeyCreationPromptState.Action> {
sealed interface Action {
/**
* User is creating a passkey
*/
class CreatePasskey : Action

/**
* User has selected the Skip button
*/
class Skip : Action
}

/**
* Create a passkey
*/
Expand All @@ -546,7 +576,16 @@ interface PasskeyCreationPromptState : AuthenticatorStepState {
* The user is being shown a confirmation screen after creating a passkey
*/
@Stable
interface PasskeyCreatedState : AuthenticatorStepState {
interface PasskeyCreatedState :
AuthenticatorStepState,
AuthenticatorActionState<PasskeyCreatedState.Action> {
sealed interface Action {
/**
* User has selected the Done button
*/
class Done : Action
}

/**
* A list of existing passkeys for this user, including the one they've just created
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import android.app.Activity
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.amplifyframework.AmplifyException
import com.amplifyframework.auth.AuthChannelEventName
import com.amplifyframework.auth.AuthException
import com.amplifyframework.auth.AuthUser
Expand All @@ -34,6 +35,7 @@ import com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterExce
import com.amplifyframework.auth.cognito.exceptions.service.InvalidPasswordException
import com.amplifyframework.auth.cognito.exceptions.service.LimitExceededException
import com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequiredException
import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException
import com.amplifyframework.auth.cognito.exceptions.service.UserNotConfirmedException
import com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException
import com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException
Expand Down Expand Up @@ -87,12 +89,15 @@ import com.amplifyframework.ui.authenticator.util.InvalidLoginMessage
import com.amplifyframework.ui.authenticator.util.LimitExceededMessage
import com.amplifyframework.ui.authenticator.util.MissingConfigurationException
import com.amplifyframework.ui.authenticator.util.NetworkErrorMessage
import com.amplifyframework.ui.authenticator.util.PasskeyCreationFailedMessage
import com.amplifyframework.ui.authenticator.util.PasskeyPromptCheck
import com.amplifyframework.ui.authenticator.util.PasswordResetMessage
import com.amplifyframework.ui.authenticator.util.RealAuthProvider
import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage
import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage
import com.amplifyframework.ui.authenticator.util.authFlow
import com.amplifyframework.ui.authenticator.util.callingActivity
import com.amplifyframework.ui.authenticator.util.getOrDefault
import com.amplifyframework.ui.authenticator.util.isAuthFlowSessionExpiredError
import com.amplifyframework.ui.authenticator.util.isConnectivityIssue
import com.amplifyframework.ui.authenticator.util.preferredFirstFactor
Expand All @@ -107,8 +112,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.VisibleForTesting

internal class AuthenticatorViewModel(application: Application, private val authProvider: AuthProvider) :
AndroidViewModel(application) {
internal class AuthenticatorViewModel(
application: Application,
private val authProvider: AuthProvider,
private val passkeyCheck: PasskeyPromptCheck = PasskeyPromptCheck(authProvider)
) : AndroidViewModel(application) {

// Constructor for compose viewModels provider
constructor(application: Application) : this(application, RealAuthProvider())
Expand Down Expand Up @@ -140,13 +148,14 @@ internal class AuthenticatorViewModel(application: Application, private val auth

// The current activity is used for WebAuthn sign-in when using passwordless functionality
private var activityReference: WeakReference<Activity> = WeakReference(null)
var activity: Activity?
private var activity: Activity?
get() = activityReference.get()
set(value) {
activityReference = WeakReference(value)
}

fun start(configuration: AuthenticatorConfiguration) {
fun start(configuration: AuthenticatorConfiguration, activity: Activity?) {
this.activity = activity
if (::configuration.isInitialized) {
return
}
Expand Down Expand Up @@ -216,7 +225,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
suspend fun signUp(username: String, password: String?, attributes: List<AuthUserAttribute>) {
viewModelScope.launch {
val options = AuthSignUpOptions.builder().userAttributes(attributes).build()
val info = UserInfo(username = username, password = password, signInSource = SignInSource.SignUp)
val info = UserInfo(username = username, password = password, signInSource = SignInSource.AutoSignIn)

when (val result = authProvider.signUp(username, password, options)) {
is AmplifyResult.Error -> handleSignUpFailure(result.error)
Expand Down Expand Up @@ -350,6 +359,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
// UserNotConfirmed and PasswordResetRequired are special cases where we need
// to enter different flows
when (error) {
is UserCancelledException -> Unit // This is an expected error, user can simply retry
is UserNotConfirmedException -> handleUnconfirmedSignIn(info)
is PasswordResetRequiredException -> handleResetRequiredSignIn(info.username)
is NotAuthorizedException -> sendMessage(InvalidLoginMessage(error))
Expand Down Expand Up @@ -471,7 +481,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth

private suspend fun handleSignInSuccess(info: UserInfo, result: AuthSignInResult) {
when (val nextStep = result.nextStep.signInStep) {
AuthSignInStep.DONE -> checkVerificationMechanisms()
AuthSignInStep.DONE -> checkForPasskeyPrompt(info)
AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE,
AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP -> moveTo(
stateFactory.newSignInMfaState(
Expand Down Expand Up @@ -537,6 +547,53 @@ internal class AuthenticatorViewModel(application: Application, private val auth
}
}

private suspend fun checkForPasskeyPrompt(info: UserInfo) {
if (passkeyCheck.shouldPromptForPasskey(userInfo = info, config = configuration)) {
moveTo(
stateFactory.newPasskeyPromptState(
onSubmit = {
val activityRef = activity
if (activityRef == null) {
// This shouldn't happen, it indicates a bug. If it does the user can retry or choose to
// skip
sendMessage(
UnknownErrorMessage(
AuthException(
message = "Missing activity reference",
recoverySuggestion = AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION
)
)
)
} else {
createPasskey(activityRef)
}
},
onSkip = ::checkVerificationMechanisms
)
)
} else {
checkVerificationMechanisms()
}
}

private suspend fun createPasskey(activityRef: Activity) {
when (val result = authProvider.createPasskey(activityRef)) {
is AmplifyResult.Error -> when (result.error) {
is UserCancelledException -> Unit // This is expected, user can retry or skip
else -> sendMessage(PasskeyCreationFailedMessage(result.error)) // User can retry/skip
}
is AmplifyResult.Success -> {
val passkeys = authProvider.getPasskeys().getOrDefault { emptyList() }
moveTo(
stateFactory.newPasskeyCreatedState(
passkeys = passkeys,
onDone = ::checkVerificationMechanisms
)
)
}
}
}

private suspend fun checkVerificationMechanisms() {
val mechanisms = authConfiguration.verificationMechanisms
if (mechanisms.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,5 @@ internal enum class SignInSource {
SignIn,

// Automatic sign in after completing sign up
SignUp,

// Signed in outside of Authenticator
External
AutoSignIn
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.amplifyframework.ui.authenticator.states

import com.amplifyframework.ui.authenticator.AuthenticatorActionState

internal interface MutableActionState<T> : AuthenticatorActionState<T> {
override var action: T?
}

internal inline fun <T> MutableActionState<T>.withAction(action: T, func: () -> Unit) {
this.action = action
func()
this.action = null
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package com.amplifyframework.ui.authenticator.states

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.amplifyframework.auth.result.AuthWebAuthnCredential
import com.amplifyframework.ui.authenticator.PasskeyCreatedState
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep

internal class PasskeyCreatedStateImpl(
override val passkeys: List<AuthWebAuthnCredential>,
private val onDone: suspend () -> Unit
) : PasskeyCreatedState {
) : PasskeyCreatedState,
MutableActionState<PasskeyCreatedState.Action> {
override val step: AuthenticatorStep = AuthenticatorStep.PasskeyCreated

override suspend fun done() = onDone()
override var action: PasskeyCreatedState.Action? by mutableStateOf(null)

override suspend fun done() = withAction(PasskeyCreatedState.Action.Done()) {
onDone()
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package com.amplifyframework.ui.authenticator.states

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.amplifyframework.ui.authenticator.PasskeyCreationPromptState
import com.amplifyframework.ui.authenticator.PasskeyCreationPromptState.Action
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class PasskeyCreationPromptStateImpl(private val onSubmit: suspend () -> Unit, private val onSkip: suspend () -> Unit) :
PasskeyCreationPromptState {
PasskeyCreationPromptState,
MutableActionState<Action> {
private val mutex = Mutex()

override suspend fun createPasskey() {
override val step = AuthenticatorStep.PasskeyCreationPrompt

override var action: Action? by mutableStateOf(null)

override suspend fun createPasskey() = withAction(Action.CreatePasskey()) {
mutex.withLock {
onSubmit()
}
}

override suspend fun skip() = onSkip()

override val step = AuthenticatorStep.PasskeyCreationPrompt
override suspend fun skip() = withAction(Action.Skip()) { onSkip() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.amplifyframework.ui.authenticator.SignInSelectAuthFactorState
import com.amplifyframework.ui.authenticator.SignInSelectAuthFactorState.Action
import com.amplifyframework.ui.authenticator.auth.SignInMethod
import com.amplifyframework.ui.authenticator.data.AuthFactor
import com.amplifyframework.ui.authenticator.data.containsPassword
Expand All @@ -17,10 +18,11 @@ internal class SignInSelectAuthFactorStateImpl(
private val onSubmit: suspend (authFactor: AuthFactor) -> Unit,
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
) : BaseStateImpl(),
SignInSelectAuthFactorState {
SignInSelectAuthFactorState,
MutableActionState<Action> {
override val step: AuthenticatorStep = AuthenticatorStep.SignInSelectAuthFactor

override var selectedFactor: AuthFactor? by mutableStateOf(null)
override var action: Action? by mutableStateOf(null)

init {
if (availableAuthFactors.containsPassword()) {
Expand All @@ -30,15 +32,13 @@ internal class SignInSelectAuthFactorStateImpl(

override fun moveTo(step: AuthenticatorInitialStep) = onMoveTo(step)

override suspend fun select(authFactor: AuthFactor) {
override suspend fun select(authFactor: AuthFactor) = withAction(Action.SelectFactor(authFactor)) {
// Clear errors
form.fields.values.forEach { it.state.error = null }

selectedFactor = authFactor
form.enabled = false
onSubmit(authFactor)
form.enabled = true
selectedFactor = null
}
}

Expand Down
Loading