Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.amplifyframework.auth.AuthUserAttribute
import com.amplifyframework.auth.MFAType
import com.amplifyframework.auth.result.AuthSignOutResult
import com.amplifyframework.auth.result.AuthWebAuthnCredential
import com.amplifyframework.ui.authenticator.enums.AuthFactor
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
import com.amplifyframework.ui.authenticator.forms.MutableFormState
Expand Down Expand Up @@ -94,6 +95,68 @@ interface SignInState : AuthenticatorStepState {
suspend fun signIn()
}

/**
* The user has entered their username and must select the authentication factor they'd like to use to sign in
*/
@Stable
interface SignInSelectAuthFactorState : AuthenticatorStepState {
/**
* The input form state holder for this step.
*/
val form: MutableFormState

/**
* The username entered in the SignIn step
*/
val username: String

/**
* The available types to select how to sign in.
*/
val availableAuthFactors: Set<AuthFactor>

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

/**
* Move the user to a different [AuthenticatorInitialStep].
*/
fun moveTo(step: AuthenticatorInitialStep)

/**
* Initiate a sign in with one of the available sign in types
*/
suspend fun select(authFactor: AuthFactor)
}

/**
* A user has entered their username and must enter their password to continue signing in
*/
@Stable
interface SignInConfirmPasswordState : AuthenticatorStepState {
/**
* The input form state holder for this step.
*/
val form: MutableFormState

/**
* The username entered in the SignIn step
*/
val username: String

/**
* Move the user to a different [AuthenticatorInitialStep].
*/
fun moveTo(step: AuthenticatorInitialStep)

/**
* Initiate a sign in with the information entered into the [form].
*/
suspend fun signIn()
}

/**
* The user has completed the initial Sign In step, and needs to enter the confirmation code from an MFA
* message to complete the sign in process.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package com.amplifyframework.ui.authenticator.enums

import com.amplifyframework.auth.AuthFactorType

sealed interface AuthFactor {
data class Password(val srp: Boolean = true) : AuthFactor
data object EmailOtp : AuthFactor
data object SmsOtp : AuthFactor
data object WebAuthn : AuthFactor
}

internal fun AuthFactor.toAuthFactorType() = when (this) {
AuthFactor.EmailOtp -> AuthFactorType.EMAIL_OTP
AuthFactor.SmsOtp -> AuthFactorType.SMS_OTP
AuthFactor.WebAuthn -> AuthFactorType.WEB_AUTHN
is AuthFactor.Password -> if (srp) AuthFactorType.PASSWORD_SRP else AuthFactorType.PASSWORD
}

internal fun AuthFactorType.toAuthFactor() = when (this) {
AuthFactorType.PASSWORD -> AuthFactor.Password(srp = false)
AuthFactorType.PASSWORD_SRP -> AuthFactor.Password(srp = true)
AuthFactorType.EMAIL_OTP -> AuthFactor.EmailOtp
AuthFactorType.SMS_OTP -> AuthFactor.SmsOtp
AuthFactorType.WEB_AUTHN -> AuthFactor.WebAuthn
}

internal val AuthFactor.challengeResponse: String
get() = this.toAuthFactorType().challengeResponse

internal fun Collection<AuthFactorType>.toAuthFactors(): Set<AuthFactor> {
// If both SRP and password are available then use SRP to sign in
var factors = this
if (this.contains(AuthFactorType.PASSWORD) && this.contains(AuthFactorType.PASSWORD_SRP)) {
factors = this - AuthFactorType.PASSWORD // remove password
}
return factors.map { it.toAuthFactor() }.toSet()
}
internal fun Collection<AuthFactor>.containsPassword() = any { it is AuthFactor.Password }
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ abstract class AuthenticatorStep internal constructor() {
*/
object SignIn : AuthenticatorInitialStep()

/**
* The user has entered their username and must select the authentication factor they'd like to use to sign in
*/
object SignInSelectAuthFactor : AuthenticatorStep()

/**
* A user has entered their username and must enter their password to continue signing in
*/
object SignInConfirmPassword : AuthenticatorStep()

/**
* The user has completed the initial Sign In step, and needs to enter the confirmation code from a custom
* challenge to complete the sign in process.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.amplifyframework.ui.authenticator.states

import com.amplifyframework.ui.authenticator.SignInConfirmPasswordState
import com.amplifyframework.ui.authenticator.auth.SignInMethod
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
import com.amplifyframework.ui.authenticator.forms.FieldKey

internal class SignInConfirmPasswordStateImpl(
override val username: String,
val signInMethod: SignInMethod,
private val onSubmit: suspend (password: String) -> Unit,
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
) : BaseStateImpl(),
SignInConfirmPasswordState {

init {
form.addFields {
password()
}
}

override val step: AuthenticatorStep = AuthenticatorStep.SignInConfirmPassword
override fun moveTo(step: AuthenticatorInitialStep) = onMoveTo(step)

override suspend fun signIn() = doSubmit {
val password = form.getTrimmed(FieldKey.Password)!!
onSubmit(password)
}
}

internal val SignInConfirmPasswordState.signInMethod: SignInMethod
get() = (this as SignInConfirmPasswordStateImpl).signInMethod
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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.SignInSelectAuthFactorState
import com.amplifyframework.ui.authenticator.auth.SignInMethod
import com.amplifyframework.ui.authenticator.enums.AuthFactor
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
import com.amplifyframework.ui.authenticator.enums.containsPassword

internal class SignInSelectAuthFactorStateImpl(
override val username: String,
val signInMethod: SignInMethod,
override val availableAuthFactors: Set<AuthFactor>,
private val onSubmit: suspend (authFactor: AuthFactor) -> Unit,
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
) : BaseStateImpl(),
SignInSelectAuthFactorState {
override val step: AuthenticatorStep = AuthenticatorStep.SignInSelectAuthFactor

override var selectedFactor: AuthFactor? by mutableStateOf(null)

init {
if (availableAuthFactors.containsPassword()) {
form.addFields { password() }
}
}

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

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

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

internal fun SignInSelectAuthFactorState.getPasswordFactor(): AuthFactor =
availableAuthFactors.first { it is AuthFactor.Password }

internal val SignInSelectAuthFactorState.signInMethod: SignInMethod
get() = (this as SignInSelectAuthFactorStateImpl).signInMethod
Loading