Skip to content

Commit 7ec52c3

Browse files
authored
feat(authenticator): Add ability to sign in with passwordless flow (#281)
1 parent 94c445e commit 7ec52c3

File tree

16 files changed

+269
-104
lines changed

16 files changed

+269
-104
lines changed

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

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequire
3737
import com.amplifyframework.auth.cognito.exceptions.service.UserNotConfirmedException
3838
import com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException
3939
import com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException
40+
import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions
41+
import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions
4042
import com.amplifyframework.auth.exceptions.NotAuthorizedException
4143
import com.amplifyframework.auth.exceptions.SessionExpiredException
4244
import com.amplifyframework.auth.exceptions.UnknownException
@@ -52,7 +54,12 @@ import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration
5254
import com.amplifyframework.ui.authenticator.auth.toAttributeKey
5355
import com.amplifyframework.ui.authenticator.auth.toFieldKey
5456
import com.amplifyframework.ui.authenticator.auth.toVerifiedAttributeKey
57+
import com.amplifyframework.ui.authenticator.data.AuthFactor
58+
import com.amplifyframework.ui.authenticator.data.AuthenticationFlow
5559
import com.amplifyframework.ui.authenticator.data.UserInfo
60+
import com.amplifyframework.ui.authenticator.data.challengeResponse
61+
import com.amplifyframework.ui.authenticator.data.toAuthFactors
62+
import com.amplifyframework.ui.authenticator.data.toAuthFlowType
5663
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
5764
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
5865
import com.amplifyframework.ui.authenticator.enums.SignInSource
@@ -69,6 +76,7 @@ import com.amplifyframework.ui.authenticator.states.BaseStateImpl
6976
import com.amplifyframework.ui.authenticator.states.StepStateFactory
7077
import com.amplifyframework.ui.authenticator.util.AmplifyResult
7178
import com.amplifyframework.ui.authenticator.util.AuthConfigurationResult
79+
import com.amplifyframework.ui.authenticator.util.AuthFlowSessionExpiredMessage
7280
import com.amplifyframework.ui.authenticator.util.AuthProvider
7381
import com.amplifyframework.ui.authenticator.util.AuthenticatorMessage
7482
import com.amplifyframework.ui.authenticator.util.CannotSendCodeMessage
@@ -83,7 +91,11 @@ import com.amplifyframework.ui.authenticator.util.PasswordResetMessage
8391
import com.amplifyframework.ui.authenticator.util.RealAuthProvider
8492
import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage
8593
import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage
94+
import com.amplifyframework.ui.authenticator.util.authFlow
95+
import com.amplifyframework.ui.authenticator.util.callingActivity
96+
import com.amplifyframework.ui.authenticator.util.isAuthFlowSessionExpiredError
8697
import com.amplifyframework.ui.authenticator.util.isConnectivityIssue
98+
import com.amplifyframework.ui.authenticator.util.preferredFirstFactor
8799
import com.amplifyframework.ui.authenticator.util.toFieldError
88100
import java.lang.ref.WeakReference
89101
import kotlinx.coroutines.channels.BufferOverflow
@@ -272,7 +284,8 @@ internal class AuthenticatorViewModel(application: Application, private val auth
272284
}
273285

274286
private suspend fun handleSignedUp(info: UserInfo) = startSignInJob {
275-
when (val result = authProvider.signIn(info.username, info.password)) {
287+
val options = getSignInOptions()
288+
when (val result = authProvider.signIn(info.username, info.password, options)) {
276289
is AmplifyResult.Error -> {
277290
moveTo(AuthenticatorStep.SignIn)
278291
handleSignInFailure(info, result.error)
@@ -295,21 +308,35 @@ internal class AuthenticatorViewModel(application: Application, private val auth
295308
}
296309

297310
private suspend fun startSignIn(info: UserInfo) = startSignInJob {
298-
when (val result = authProvider.signIn(info.username, info.password)) {
311+
val options = getSignInOptions()
312+
when (val result = authProvider.signIn(info.username, info.password, options)) {
299313
is AmplifyResult.Error -> handleSignInFailure(info, result.error)
300314
is AmplifyResult.Success -> handleSignInSuccess(info, result.data)
301315
}
302316
}
303317

318+
private fun getSignInOptions(preferredFirstFactorOverride: AuthFactor? = null) =
319+
AWSCognitoAuthSignInOptions.builder()
320+
.authFlow(configuration.authenticationFlow.toAuthFlowType())
321+
.callingActivity(activity)
322+
.preferredFirstFactor(configuration.authenticationFlow, preferredFirstFactorOverride)
323+
.build()
324+
304325
private suspend fun confirmSignIn(info: UserInfo, challengeResponse: String) = startSignInJob {
305-
when (val result = authProvider.confirmSignIn(challengeResponse)) {
306-
is AmplifyResult.Error -> handleSignInFailure(info, result.error)
326+
val options = AWSCognitoAuthConfirmSignInOptions.builder()
327+
.callingActivity(activity)
328+
.build()
329+
when (val result = authProvider.confirmSignIn(challengeResponse, options)) {
330+
is AmplifyResult.Error -> handleConfirmSignInFailure(info, result.error)
307331
is AmplifyResult.Success -> handleSignInSuccess(info, result.data)
308332
}
309333
}
310334

311335
private suspend fun setNewSignInPassword(info: UserInfo, newPassword: String) = startSignInJob {
312-
when (val result = authProvider.confirmSignIn(newPassword)) {
336+
val options = AWSCognitoAuthConfirmSignInOptions.builder()
337+
.callingActivity(activity)
338+
.build()
339+
when (val result = authProvider.confirmSignIn(newPassword, options)) {
313340
// an error here is more similar to a sign up error
314341
is AmplifyResult.Error -> handleSignUpFailure(result.error)
315342
is AmplifyResult.Success -> {
@@ -330,6 +357,17 @@ internal class AuthenticatorViewModel(application: Application, private val auth
330357
}
331358
}
332359

360+
private suspend fun handleConfirmSignInFailure(info: UserInfo, error: AuthException) {
361+
if (configuration.authenticationFlow is AuthenticationFlow.UserChoice &&
362+
error.isAuthFlowSessionExpiredError()
363+
) {
364+
moveTo(AuthenticatorStep.SignIn)
365+
sendMessage(AuthFlowSessionExpiredMessage(error))
366+
} else {
367+
handleSignInFailure(info, error)
368+
}
369+
}
370+
333371
private suspend fun handleUnconfirmedSignIn(info: UserInfo) {
334372
when (val result = authProvider.resendSignUpCode(info.username)) {
335373
is AmplifyResult.Error -> handleAuthException(result.error)
@@ -352,7 +390,33 @@ internal class AuthenticatorViewModel(application: Application, private val auth
352390
}
353391
}
354392

355-
private suspend fun handleTotpSetupRequired(info: UserInfo, totpSetupDetails: TOTPSetupDetails?) {
393+
private suspend fun handleFactorSelectionRequired(info: UserInfo, availableFactors: Set<AuthFactor>?) {
394+
if (availableFactors == null) {
395+
val exception = AuthException("Missing available AuthFactorTypes", "Please open a bug with Amplify")
396+
handleGeneralFailure(exception)
397+
return
398+
}
399+
400+
// Auto-select a single auth factor
401+
if (availableFactors.size == 1) {
402+
confirmSignIn(info, availableFactors.first().challengeResponse)
403+
return
404+
}
405+
406+
val newState = stateFactory.newSignInSelectFactorState(
407+
username = info.username,
408+
availableFactors = availableFactors,
409+
onSelect = { authFactor ->
410+
val passwordField = (currentState as? BaseStateImpl)?.form?.fields?.get(Password)
411+
val password = passwordField?.state?.content
412+
val newInfo = info.copy(password = password)
413+
confirmSignIn(newInfo, authFactor.challengeResponse)
414+
}
415+
)
416+
moveTo(newState)
417+
}
418+
419+
private fun handleTotpSetupRequired(info: UserInfo, totpSetupDetails: TOTPSetupDetails?) {
356420
if (totpSetupDetails == null) {
357421
val exception = AuthException("Missing TOTPSetupDetails", "Please open a bug with Amplify")
358422
handleGeneralFailure(exception)
@@ -444,6 +508,23 @@ internal class AuthenticatorViewModel(application: Application, private val auth
444508
confirmSignIn(info, confirmationCode)
445509
}
446510
)
511+
AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION ->
512+
handleFactorSelectionRequired(
513+
info,
514+
result.nextStep.availableFactors?.toAuthFactors()
515+
)
516+
AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD -> {
517+
if (info.password != null) {
518+
confirmSignIn(info, info.password)
519+
} else {
520+
moveTo(
521+
stateFactory.newSignInConfirmPasswordState(username = info.username) { password ->
522+
val newInfo = info.copy(password = password)
523+
confirmSignIn(newInfo, password)
524+
}
525+
)
526+
}
527+
}
447528
else -> {
448529
// Generic error for any other next steps that may be added in the future
449530
val exception = AuthException(
@@ -532,7 +613,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
532613
}
533614
}
534615

535-
private suspend fun handlePasswordResetComplete(username: String? = null, password: String? = null) {
616+
private suspend fun handlePasswordResetComplete() {
536617
logger.debug("Password reset complete")
537618
sendMessage(PasswordResetMessage)
538619
moveTo(stateFactory.newSignInState(this::signIn))

authenticator/src/main/java/com/amplifyframework/ui/authenticator/data/AuthenticationFlow.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,6 @@ internal val AuthenticationFlow.signInRequiresPassword: Boolean get() = when (th
6868
}
6969

7070
internal fun AuthenticationFlow.toAuthFlowType() = when (this) {
71-
is AuthenticationFlow.Password -> AuthFlowType.USER_SRP_AUTH
72-
is AuthenticationFlow.UserChoice -> AuthFlowType.USER_AUTH
71+
is AuthenticationFlow.Password -> null // Use whatever is defined in the user's config file
72+
is AuthenticationFlow.UserChoice -> AuthFlowType.USER_AUTH // Requires USER_AUTH
7373
}

authenticator/src/main/java/com/amplifyframework/ui/authenticator/forms/FieldValidator.kt

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ internal operator fun FieldValidator.plus(other: FieldValidator): FieldValidator
5151
internal object FieldValidators {
5252

5353
private val usernamePattern = """[\p{L}\p{M}\p{S}\p{N}\p{P}]+""".toPattern()
54-
private val confirmationCodePattern = """\d{6}""".toPattern()
54+
private val confirmationCodePattern = """\d{6}|\d{8}""".toPattern()
5555
private val specialRegex = """[\^\$\{}\*\.\[\]\{}\(\)\?\-"!@#%&/\\,><':;|_~`+=\s]+""".toRegex()
5656
private val numbersRegex = "\\d+".toRegex()
5757
private val upperRegex = "[A-Z]+".toRegex()
@@ -63,25 +63,17 @@ internal object FieldValidators {
6363
*/
6464
val None: FieldValidator = { null }
6565

66-
internal fun required(
67-
error: FieldError = FieldError.MissingRequired
68-
): FieldValidator = {
66+
internal fun required(error: FieldError = FieldError.MissingRequired): FieldValidator = {
6967
if (content.isBlank()) error else null
7068
}
7169

72-
private fun matchingField(
73-
other: FieldKey,
74-
error: FieldError
75-
): FieldValidator = {
70+
private fun matchingField(other: FieldKey, error: FieldError): FieldValidator = {
7671
if (content != formContent[other]) error else null
7772
}
7873

7974
fun confirmPassword() = matchingField(Password, PasswordsDoNotMatch)
8075

81-
private fun pattern(
82-
pattern: Pattern,
83-
error: FieldError = InvalidFormat
84-
): FieldValidator = {
76+
private fun pattern(pattern: Pattern, error: FieldError = InvalidFormat): FieldValidator = {
8577
if (content.isNotBlank() && !pattern.matcher(content).matches()) error else null
8678
}
8779

@@ -90,9 +82,7 @@ internal object FieldValidators {
9082
fun phoneNumber() = pattern(Patterns.PHONE)
9183
fun webUrl() = pattern(Patterns.WEB_URL)
9284

93-
fun date(
94-
error: FieldError = InvalidFormat
95-
): FieldValidator = {
85+
fun date(error: FieldError = InvalidFormat): FieldValidator = {
9686
if (content.isNotBlank()) {
9787
try {
9888
dateFormat.parse(content)
@@ -107,9 +97,7 @@ internal object FieldValidators {
10797

10898
internal fun confirmationCode() = pattern(confirmationCodePattern)
10999

110-
internal fun password(
111-
criteria: PasswordCriteria
112-
): FieldValidator = {
100+
internal fun password(criteria: PasswordCriteria): FieldValidator = {
113101
if (content.isNotBlank()) {
114102
val potentialErrors = mutableListOf<PasswordError>()
115103
if (content.length < criteria.length) {

authenticator/src/main/java/com/amplifyframework/ui/authenticator/forms/FormBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ internal class FormBuilderImpl : SignUpFormBuilder {
211211
key = FieldKey.ConfirmationCode,
212212
validator = FieldValidators.confirmationCode(),
213213
keyboardType = KeyboardType.Number,
214-
maxLength = 6
214+
maxLength = 8
215215
)
216216
}
217217

authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignInConfirmMfaStateImpl.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ internal class SignInConfirmMfaStateImpl(
2525
override val deliveryDetails: AuthCodeDeliveryDetails?,
2626
private val onSubmit: suspend (confirmationCode: String) -> Unit,
2727
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
28-
) : BaseStateImpl(), SignInConfirmMfaState {
28+
) : BaseStateImpl(),
29+
SignInConfirmMfaState {
2930

3031
init {
3132
form.addFields {

authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignInStateImpl.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ import com.amplifyframework.ui.authenticator.forms.FieldKey
2424

2525
internal class SignInStateImpl(
2626
private val signInMethod: SignInMethod,
27-
private val onSubmit: suspend (username: String, password: String) -> Unit,
27+
showPasswordField: Boolean,
28+
private val onSubmit: suspend (username: String, password: String?) -> Unit,
2829
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
2930
) : BaseStateImpl(), SignInState {
3031

3132
init {
3233
form.addFields {
3334
fieldForSignInMethod(signInMethod)
34-
password()
35+
if (showPasswordField) {
36+
password()
37+
}
3538
}
3639
}
3740

@@ -40,7 +43,7 @@ internal class SignInStateImpl(
4043

4144
override suspend fun signIn() = doSubmit {
4245
val username = form.getTrimmed(signInMethod.toFieldKey())!!
43-
val password = form.getTrimmed(FieldKey.Password)!!
46+
val password = form.getTrimmed(FieldKey.Password)
4447
onSubmit(username, password)
4548
}
4649
}

authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/StepStateFactory.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.amplifyframework.auth.MFAType
2323
import com.amplifyframework.auth.result.AuthSignOutResult
2424
import com.amplifyframework.ui.authenticator.AuthenticatorConfiguration
2525
import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration
26+
import com.amplifyframework.ui.authenticator.data.AuthFactor
27+
import com.amplifyframework.ui.authenticator.data.signInRequiresPassword
2628
import com.amplifyframework.ui.authenticator.data.signUpRequiresPassword
2729
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
2830
import com.amplifyframework.ui.authenticator.forms.FormData
@@ -39,12 +41,25 @@ internal class StepStateFactory(
3941
onSignOut = onSignOut
4042
)
4143

42-
fun newSignInState(onSubmit: suspend (username: String, password: String) -> Unit) = SignInStateImpl(
44+
fun newSignInState(onSubmit: suspend (username: String, password: String?) -> Unit) = SignInStateImpl(
4345
signInMethod = authConfiguration.signInMethod,
46+
showPasswordField = configuration.authenticationFlow.signInRequiresPassword,
4447
onSubmit = onSubmit,
4548
onMoveTo = onMoveTo
4649
)
4750

51+
fun newSignInSelectFactorState(
52+
username: String,
53+
availableFactors: Set<AuthFactor>,
54+
onSelect: suspend (AuthFactor) -> Unit
55+
) = SignInSelectAuthFactorStateImpl(
56+
username = username,
57+
signInMethod = authConfiguration.signInMethod,
58+
availableAuthFactors = availableFactors,
59+
onSubmit = onSelect,
60+
onMoveTo = onMoveTo
61+
)
62+
4863
fun newSignInMfaState(
4964
codeDeliveryDetails: AuthCodeDeliveryDetails?,
5065
onSubmit: suspend (confirmationCode: String) -> Unit
@@ -72,6 +87,14 @@ internal class StepStateFactory(
7287
onMoveTo = onMoveTo
7388
)
7489

90+
fun newSignInConfirmPasswordState(username: String, onSubmit: suspend (password: String) -> Unit) =
91+
SignInConfirmPasswordStateImpl(
92+
username = username,
93+
signInMethod = authConfiguration.signInMethod,
94+
onSubmit = onSubmit,
95+
onMoveTo = onMoveTo
96+
)
97+
7598
fun newSignInConfirmTotpCodeState(onSubmit: suspend (confirmationCode: String) -> Unit) =
7699
SignInConfirmTotpCodeStateImpl(
77100
onSubmit = onSubmit,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fun SignInConfirmPassword(
4747
value = state.username,
4848
onValueChange = {},
4949
label = { Text(usernameLabel) },
50-
readOnly = true
50+
enabled = false
5151
)
5252
Spacer(modifier = Modifier.size(AuthenticatorUiConstants.spaceBetweenFields))
5353
AuthenticatorForm(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.material3.OutlinedTextField
99
import androidx.compose.material3.Text
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.runtime.rememberCoroutineScope
12+
import androidx.compose.ui.Alignment
1213
import androidx.compose.ui.Modifier
1314
import androidx.compose.ui.platform.testTag
1415
import androidx.compose.ui.res.stringResource
@@ -60,7 +61,7 @@ fun SignInSelectAuthFactor(
6061
if (state.availableAuthFactors.size > 1) {
6162
DividerWithText(
6263
text = stringResource(R.string.amplify_ui_authenticator_or),
63-
modifier = Modifier.fillMaxWidth()
64+
modifier = Modifier.fillMaxWidth(0.5f).align(Alignment.CenterHorizontally)
6465
)
6566
}
6667
}

0 commit comments

Comments
 (0)