Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@

package com.amplifyframework.ui.authenticator

import com.amplifyframework.ui.authenticator.data.AuthenticationFlow
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
import com.amplifyframework.ui.authenticator.forms.SignUpFormBuilder
import com.amplifyframework.ui.authenticator.options.TotpOptions

internal data class AuthenticatorConfiguration(
val initialStep: AuthenticatorInitialStep,
val signUpForm: SignUpFormBuilder.() -> Unit,
val totpOptions: TotpOptions?
val totpOptions: TotpOptions?,
val authenticationFlow: AuthenticationFlow
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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
Expand All @@ -46,15 +47,17 @@ import kotlinx.coroutines.flow.onEach
fun rememberAuthenticatorState(
initialStep: AuthenticatorInitialStep = AuthenticatorStep.SignIn,
signUpForm: SignUpFormBuilder.() -> Unit = {},
totpOptions: TotpOptions? = null
totpOptions: TotpOptions? = null,
authenticationFlow: AuthenticationFlow = AuthenticationFlow.Password
): AuthenticatorState {
val viewModel = viewModel<AuthenticatorViewModel>()
val scope = rememberCoroutineScope()
return remember {
val configuration = AuthenticatorConfiguration(
initialStep = initialStep,
signUpForm = signUpForm,
totpOptions = totpOptions
totpOptions = totpOptions,
authenticationFlow = authenticationFlow
)

viewModel.start(configuration)
Expand Down Expand Up @@ -102,9 +105,7 @@ interface AuthenticatorState {
val messages: Flow<AuthenticatorMessage>
}

internal class AuthenticatorStateImpl constructor(
private val viewModel: AuthenticatorViewModel
) : AuthenticatorState {
internal class AuthenticatorStateImpl constructor(private val viewModel: AuthenticatorViewModel) : AuthenticatorState {
override var stepState by mutableStateOf<AuthenticatorStepState>(LoadingState)

override val messages: Flow<AuthenticatorMessage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.amplifyframework.auth.AuthUser
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.data.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 @@ -93,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 Expand Up @@ -460,3 +524,36 @@ interface VerifyUserConfirmState : AuthenticatorStepState {
*/
fun skip()
}

/**
* The user is being shown a prompt to create a passkey, encouraging them to use this as a way to sign in quickly
* via biometrics
*/
@Stable
interface PasskeyCreationPromptState : AuthenticatorStepState {
/**
* Create a passkey
*/
suspend fun createPasskey()

/**
* Skip passkey creation and continue to the next step
*/
suspend fun skip()
}

/**
* The user is being shown a confirmation screen after creating a passkey
*/
@Stable
interface PasskeyCreatedState : AuthenticatorStepState {
/**
* A list of existing passkeys for this user, including the one they've just created
*/
val passkeys: List<AuthWebAuthnCredential>

/**
* Continue to the next step
*/
suspend fun done()
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ internal class AuthenticatorViewModel(application: Application, private val auth
// Is there a current Amplify call in progress that could result in a signed in event?
private var expectingSignInEvent: Boolean = false

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

fun start(configuration: AuthenticatorConfiguration) {
if (::configuration.isInitialized) {
return
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.data

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
@@ -0,0 +1,73 @@
/*
* 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.data

import com.amplifyframework.auth.cognito.options.AuthFlowType

/**
* AuthenticationFlow represents the different styles of authentication supported by the Authenticator component.
*/
sealed interface AuthenticationFlow {
/**
* The standard password-based auth flow. The user will be prompted to enter a username and password on the SignIn
* screen. You can use this with either Password or PasswordSrp sign ins.
*/
data object Password : AuthenticationFlow

/**
* A choice-based auth flow, where the user may log in via a password, a passkey, or a one-time-password (OTP) sent
* to their email or SMS. The user is first prompted to enter only their sign in attribute (username/email/phone)
* and then may be presented with options for how to log in. You must have ALLOW_USER_AUTH enabled as an
* authentication flow in your Cognito User Pool.
*/
data class UserChoice(
/**
* Specify an [AuthFactor] to use by default, if available to the user.
*
* For example, if you want any user with a registered passkey to sign in with that passkey without being
* prompted, then set this value to `AuthFactor.WebAuthn`.
*
* If this is null or the [AuthFactor] is not available to the user, they may go directly into a different
* [AuthFactor] (if they only have one available) or may be prompted to choose a factor (if they have multiple
* available).
*
* If this is set to [AuthFactor.Password] or [AuthFactor.PasswordSrp] then the user will be prompted for a
* password directly when signing in. Use these values only if you're certain that no users exist who don't
* have passwords.
*/
val preferredAuthFactor: AuthFactor? = null,

/**
* Control when/if the user is prompted to create a passkey after logging in.
*/
val passkeyPrompts: PasskeyPrompts = PasskeyPrompts()
) : AuthenticationFlow
}

internal val AuthenticationFlow.signUpRequiresPassword: Boolean get() = when (this) {
is AuthenticationFlow.Password -> true
is AuthenticationFlow.UserChoice -> false
}

internal val AuthenticationFlow.signInRequiresPassword: Boolean get() = when (this) {
is AuthenticationFlow.Password -> true
is AuthenticationFlow.UserChoice -> this.preferredAuthFactor is AuthFactor.Password
}

internal fun AuthenticationFlow.toAuthFlowType() = when (this) {
is AuthenticationFlow.Password -> AuthFlowType.USER_SRP_AUTH
is AuthenticationFlow.UserChoice -> AuthFlowType.USER_AUTH
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.data

/**
* Class that contains configuration values for when/if to show prompts to create passkeys to the user.
*/
data class PasskeyPrompts(
/**
* Show a prompt after a user who does not have a passkey registered signs in to the application.
*/
val afterSignIn: PasskeyPrompt = PasskeyPrompt.Always,
/**
* Show a prompt to create a passkey after the automatic sign in following a new user signing up.
*/
val afterSignUp: PasskeyPrompt = PasskeyPrompt.Always
)

/**
* Possible selections for controlling passkey prompts.
*/
sealed interface PasskeyPrompt {
/**
* Never prompt users to create a passkey after signing in.
*/
data object Never : PasskeyPrompt

/**
* Always prompt users to create a passkey after signing in if they don't already have an existing registered
* passkey.
*/
data object Always : PasskeyPrompt
}
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 Expand Up @@ -120,4 +130,15 @@ abstract class AuthenticatorStep internal constructor() {
* The user has initiated verification of an account recovery mechanism (email, phone) and needs to provide a confirmation code.
*/
object VerifyUserConfirm : AuthenticatorStep()

/**
* The user is being shown a prompt to create a passkey, encouraging them to use this as a way to sign in quickly
* via biometrics
*/
object PasskeyCreationPrompt : AuthenticatorStep()

/**
* The user is being shown a confirmation screen after creating a passkey
*/
object PasskeyCreated : AuthenticatorStep()
}
Loading