Skip to content

Commit d496b4b

Browse files
authored
feat(authenticator): Add Select Auth Factor and Confirm Password UIs (#268)
1 parent dddceaa commit d496b4b

31 files changed

+1007
-93
lines changed

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorStepState.kt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.amplifyframework.auth.AuthUserAttribute
2424
import com.amplifyframework.auth.MFAType
2525
import com.amplifyframework.auth.result.AuthSignOutResult
2626
import com.amplifyframework.auth.result.AuthWebAuthnCredential
27+
import com.amplifyframework.ui.authenticator.enums.AuthFactor
2728
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
2829
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
2930
import com.amplifyframework.ui.authenticator.forms.MutableFormState
@@ -94,6 +95,68 @@ interface SignInState : AuthenticatorStepState {
9495
suspend fun signIn()
9596
}
9697

98+
/**
99+
* The user has entered their username and must select the authentication factor they'd like to use to sign in
100+
*/
101+
@Stable
102+
interface SignInSelectAuthFactorState : AuthenticatorStepState {
103+
/**
104+
* The input form state holder for this step.
105+
*/
106+
val form: MutableFormState
107+
108+
/**
109+
* The username entered in the SignIn step
110+
*/
111+
val username: String
112+
113+
/**
114+
* The available types to select how to sign in.
115+
*/
116+
val availableAuthFactors: Set<AuthFactor>
117+
118+
/**
119+
* The factor the user selected and is currently being processed
120+
*/
121+
val selectedFactor: AuthFactor?
122+
123+
/**
124+
* Move the user to a different [AuthenticatorInitialStep].
125+
*/
126+
fun moveTo(step: AuthenticatorInitialStep)
127+
128+
/**
129+
* Initiate a sign in with one of the available sign in types
130+
*/
131+
suspend fun select(authFactor: AuthFactor)
132+
}
133+
134+
/**
135+
* A user has entered their username and must enter their password to continue signing in
136+
*/
137+
@Stable
138+
interface SignInConfirmPasswordState : AuthenticatorStepState {
139+
/**
140+
* The input form state holder for this step.
141+
*/
142+
val form: MutableFormState
143+
144+
/**
145+
* The username entered in the SignIn step
146+
*/
147+
val username: String
148+
149+
/**
150+
* Move the user to a different [AuthenticatorInitialStep].
151+
*/
152+
fun moveTo(step: AuthenticatorInitialStep)
153+
154+
/**
155+
* Initiate a sign in with the information entered into the [form].
156+
*/
157+
suspend fun signIn()
158+
}
159+
97160
/**
98161
* The user has completed the initial Sign In step, and needs to enter the confirmation code from an MFA
99162
* message to complete the sign in process.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.ui.authenticator.enums
17+
18+
import com.amplifyframework.auth.AuthFactorType
19+
20+
sealed interface AuthFactor {
21+
data class Password(val srp: Boolean = true) : AuthFactor
22+
data object EmailOtp : AuthFactor
23+
data object SmsOtp : AuthFactor
24+
data object WebAuthn : AuthFactor
25+
}
26+
27+
internal fun AuthFactor.toAuthFactorType() = when (this) {
28+
AuthFactor.EmailOtp -> AuthFactorType.EMAIL_OTP
29+
AuthFactor.SmsOtp -> AuthFactorType.SMS_OTP
30+
AuthFactor.WebAuthn -> AuthFactorType.WEB_AUTHN
31+
is AuthFactor.Password -> if (srp) AuthFactorType.PASSWORD_SRP else AuthFactorType.PASSWORD
32+
}
33+
34+
internal fun AuthFactorType.toAuthFactor() = when (this) {
35+
AuthFactorType.PASSWORD -> AuthFactor.Password(srp = false)
36+
AuthFactorType.PASSWORD_SRP -> AuthFactor.Password(srp = true)
37+
AuthFactorType.EMAIL_OTP -> AuthFactor.EmailOtp
38+
AuthFactorType.SMS_OTP -> AuthFactor.SmsOtp
39+
AuthFactorType.WEB_AUTHN -> AuthFactor.WebAuthn
40+
}
41+
42+
internal val AuthFactor.challengeResponse: String
43+
get() = this.toAuthFactorType().challengeResponse
44+
45+
internal fun Collection<AuthFactorType>.toAuthFactors(): Set<AuthFactor> {
46+
// If both SRP and password are available then use SRP to sign in
47+
var factors = this
48+
if (this.contains(AuthFactorType.PASSWORD) && this.contains(AuthFactorType.PASSWORD_SRP)) {
49+
factors = this - AuthFactorType.PASSWORD // remove password
50+
}
51+
return factors.map { it.toAuthFactor() }.toSet()
52+
}
53+
internal fun Collection<AuthFactor>.containsPassword() = any { it is AuthFactor.Password }

authenticator/src/main/java/com/amplifyframework/ui/authenticator/enums/AuthenticatorStep.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ abstract class AuthenticatorStep internal constructor() {
4646
*/
4747
object SignIn : AuthenticatorInitialStep()
4848

49+
/**
50+
* The user has entered their username and must select the authentication factor they'd like to use to sign in
51+
*/
52+
object SignInSelectAuthFactor : AuthenticatorStep()
53+
54+
/**
55+
* A user has entered their username and must enter their password to continue signing in
56+
*/
57+
object SignInConfirmPassword : AuthenticatorStep()
58+
4959
/**
5060
* The user has completed the initial Sign In step, and needs to enter the confirmation code from a custom
5161
* challenge to complete the sign in process.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.amplifyframework.ui.authenticator.states
2+
3+
import com.amplifyframework.ui.authenticator.SignInConfirmPasswordState
4+
import com.amplifyframework.ui.authenticator.auth.SignInMethod
5+
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
6+
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
7+
import com.amplifyframework.ui.authenticator.forms.FieldKey
8+
9+
internal class SignInConfirmPasswordStateImpl(
10+
override val username: String,
11+
val signInMethod: SignInMethod,
12+
private val onSubmit: suspend (password: String) -> Unit,
13+
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
14+
) : BaseStateImpl(),
15+
SignInConfirmPasswordState {
16+
17+
init {
18+
form.addFields {
19+
password()
20+
}
21+
}
22+
23+
override val step: AuthenticatorStep = AuthenticatorStep.SignInConfirmPassword
24+
override fun moveTo(step: AuthenticatorInitialStep) = onMoveTo(step)
25+
26+
override suspend fun signIn() = doSubmit {
27+
val password = form.getTrimmed(FieldKey.Password)!!
28+
onSubmit(password)
29+
}
30+
}
31+
32+
internal val SignInConfirmPasswordState.signInMethod: SignInMethod
33+
get() = (this as SignInConfirmPasswordStateImpl).signInMethod
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.amplifyframework.ui.authenticator.states
2+
3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.setValue
6+
import com.amplifyframework.ui.authenticator.SignInSelectAuthFactorState
7+
import com.amplifyframework.ui.authenticator.auth.SignInMethod
8+
import com.amplifyframework.ui.authenticator.enums.AuthFactor
9+
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
10+
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
11+
import com.amplifyframework.ui.authenticator.enums.containsPassword
12+
13+
internal class SignInSelectAuthFactorStateImpl(
14+
override val username: String,
15+
val signInMethod: SignInMethod,
16+
override val availableAuthFactors: Set<AuthFactor>,
17+
private val onSubmit: suspend (authFactor: AuthFactor) -> Unit,
18+
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
19+
) : BaseStateImpl(),
20+
SignInSelectAuthFactorState {
21+
override val step: AuthenticatorStep = AuthenticatorStep.SignInSelectAuthFactor
22+
23+
override var selectedFactor: AuthFactor? by mutableStateOf(null)
24+
25+
init {
26+
if (availableAuthFactors.containsPassword()) {
27+
form.addFields { password() }
28+
}
29+
}
30+
31+
override fun moveTo(step: AuthenticatorInitialStep) = onMoveTo(step)
32+
33+
override suspend fun select(authFactor: AuthFactor) {
34+
// Clear errors
35+
form.fields.values.forEach { it.state.error = null }
36+
37+
selectedFactor = authFactor
38+
form.enabled = false
39+
onSubmit(authFactor)
40+
form.enabled = true
41+
selectedFactor = null
42+
}
43+
}
44+
45+
internal fun SignInSelectAuthFactorState.getPasswordFactor(): AuthFactor =
46+
availableAuthFactors.first { it is AuthFactor.Password }
47+
48+
internal val SignInSelectAuthFactorState.signInMethod: SignInMethod
49+
get() = (this as SignInSelectAuthFactorStateImpl).signInMethod

0 commit comments

Comments
 (0)