Skip to content

Commit e28a26f

Browse files
committed
add restart flow for select auth factor
1 parent d235b05 commit e28a26f

File tree

4 files changed

+98
-45
lines changed

4 files changed

+98
-45
lines changed

Sources/Authenticator/Models/Internal/Credentials.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ class Credentials: ObservableObject {
1313
@Published var password: String?
1414

1515
@Published var message: AuthenticatorMessage?
16+
17+
/// Tracks the currently selected auth factor during sign-in.
18+
/// Used to detect when user changes their auth factor selection after already selecting one.
19+
/// When non-nil, subsequent factor selections require restarting the sign-in flow.
20+
@Published var selectedAuthFactor: AuthFactor?
1621
}

Sources/Authenticator/States/SignInSelectAuthFactorState.swift

Lines changed: 88 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public class SignInSelectAuthFactorState: AuthenticatorBaseState {
3838
///
3939
/// Automatically sets the Authenticator's next step accordingly, as well as the
4040
/// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
41+
///
42+
/// If the user has already selected an auth factor previously (tracked via `credentials.selectedAuthFactor`),
43+
/// this method will restart the sign-in flow with the new factor as the preferred first factor.
44+
/// This is necessary because Cognito doesn't allow changing the auth factor selection once made.
45+
///
4146
/// - Throws: An `Amplify.AuthenticationError` if the operation fails
4247
public func selectAuthFactor() async throws {
4348
guard let factor = selectedAuthFactor else {
@@ -51,50 +56,28 @@ public class SignInSelectAuthFactorState: AuthenticatorBaseState {
5156
do {
5257
log.verbose("Selecting auth factor: \(factor)")
5358

59+
// Check if user has already selected an auth factor previously
60+
// If so, we need to restart the sign-in flow instead of calling confirmSignIn
61+
let flowRestartRequired = credentials.selectedAuthFactor != nil
62+
63+
// Update password in credentials if password factor is selected
64+
if factor.isPassword {
65+
credentials.password = password
66+
}
67+
68+
// Track the selected auth factor
69+
credentials.selectedAuthFactor = factor
70+
5471
let result: AuthSignInResult
5572

56-
switch factor {
57-
case .password:
58-
// Password requires 2-step flow, use dedicated method
59-
// Step 1: Select password factor → confirmSignIn("PASSWORD") → .confirmSignInWithPassword
60-
// Step 2: Send password → confirmSignIn("Pass@123") → .done
61-
result = try await signInWithPassword()
62-
63-
case .emailOtp, .smsOtp:
64-
// Select the auth factor and move to appropriate next step
65-
// Use the AuthFactor extension to get the challenge response
66-
let challengeResponse = factor.toAuthFactorType().challengeResponse
67-
68-
result = try await authenticationService.confirmSignIn(
69-
challengeResponse: challengeResponse,
70-
options: nil
71-
)
72-
73-
case .webAuthn:
74-
// WebAuthn sign-in - Amplify handles the native UI
75-
#if os(iOS) || os(macOS) || os(visionOS)
76-
guard #available(iOS 17.4, macOS 13.5, visionOS 1.0, *) else {
77-
setBusy(false)
78-
log.error("WebAuthn requires iOS 17.4+, macOS 13.5+, or visionOS 1.0+")
79-
setMessage(.error(message: "Passkey is not available"))
80-
return
81-
}
82-
83-
log.verbose("Initiating WebAuthn sign-in")
84-
85-
// Select WebAuthn as the auth factor
86-
let challengeResponse = factor.toAuthFactorType().challengeResponse
87-
88-
result = try await authenticationService.confirmSignIn(
89-
challengeResponse: challengeResponse,
90-
options: nil
91-
)
92-
#else
93-
setBusy(false)
94-
log.error("WebAuthn is not available on this platform")
95-
setMessage(.error(message: "Passkey is not available"))
96-
return
97-
#endif
73+
if flowRestartRequired {
74+
// User has already selected an auth factor before
75+
// Restart sign-in flow with the new factor as preferred
76+
log.verbose("Restarting sign-in flow with preferred factor: \(factor)")
77+
result = try await restartSignInWithPreferredFactor(factor)
78+
} else {
79+
// First-time selection - use confirmSignIn as normal
80+
result = try await confirmSignInWithFactor(factor)
9881
}
9982

10083
let nextStep = try await nextStep(for: result)
@@ -108,6 +91,69 @@ public class SignInSelectAuthFactorState: AuthenticatorBaseState {
10891
}
10992
}
11093

94+
/// Confirms sign-in with the selected auth factor (first-time selection)
95+
/// - Parameter factor: The auth factor to use
96+
/// - Returns: The `AuthSignInResult` from the confirmation
97+
private func confirmSignInWithFactor(_ factor: AuthFactor) async throws -> AuthSignInResult {
98+
switch factor {
99+
case .password:
100+
// Password requires 2-step flow, use dedicated method
101+
// Step 1: Select password factor → confirmSignIn("PASSWORD") → .confirmSignInWithPassword
102+
// Step 2: Send password → confirmSignIn("Pass@123") → .done
103+
return try await signInWithPassword()
104+
105+
case .emailOtp, .smsOtp:
106+
// Select the auth factor and move to appropriate next step
107+
// Use the AuthFactor extension to get the challenge response
108+
let challengeResponse = factor.toAuthFactorType().challengeResponse
109+
110+
return try await authenticationService.confirmSignIn(
111+
challengeResponse: challengeResponse,
112+
options: nil
113+
)
114+
115+
case .webAuthn:
116+
// WebAuthn sign-in - Amplify handles the native UI
117+
#if os(iOS) || os(macOS) || os(visionOS)
118+
guard #available(iOS 17.4, macOS 13.5, visionOS 1.0, *) else {
119+
log.error("WebAuthn requires iOS 17.4+, macOS 13.5+, or visionOS 1.0+")
120+
throw AuthError.unknown("Passkey is not available", nil)
121+
}
122+
123+
log.verbose("Initiating WebAuthn sign-in")
124+
125+
// Select WebAuthn as the auth factor
126+
let challengeResponse = factor.toAuthFactorType().challengeResponse
127+
128+
return try await authenticationService.confirmSignIn(
129+
challengeResponse: challengeResponse,
130+
options: nil
131+
)
132+
#else
133+
log.error("WebAuthn is not available on this platform")
134+
throw AuthError.unknown("Passkey is not available", nil)
135+
#endif
136+
}
137+
}
138+
139+
/// Restarts the sign-in flow with the specified factor as the preferred first factor.
140+
/// This is used when the user changes their auth factor selection after already selecting one.
141+
/// - Parameter factor: The auth factor to use as the preferred first factor
142+
/// - Returns: The `AuthSignInResult` from the sign-in attempt
143+
private func restartSignInWithPreferredFactor(_ factor: AuthFactor) async throws -> AuthSignInResult {
144+
let options = AuthSignInRequest.Options(
145+
pluginOptions: AWSAuthSignInOptions(
146+
authFlowType: .userAuth(preferredFirstFactor: factor.toAuthFactorType())
147+
)
148+
)
149+
150+
return try await authenticationService.signIn(
151+
username: credentials.username,
152+
password: factor.isPassword ? credentials.password : nil,
153+
options: options
154+
)
155+
}
156+
111157
/// Signs in with password using the multi-step flow
112158
///
113159
/// Password flow:

Sources/Authenticator/States/SignInState.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public class SignInState: AuthenticatorBaseState {
3131
/// - Throws: An `Amplify.AuthenticationError` if the operation fails
3232
public func signIn() async throws {
3333
setBusy(true)
34+
35+
// Reset selected auth factor tracking for new sign-in flow
36+
credentials.selectedAuthFactor = nil
3437

3538
do {
3639
log.verbose("Attempting to Sign In")

Sources/Authenticator/Views/SignInSelectAuthFactorView.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,8 @@ public struct SignInSelectAuthFactorView<Header: View,
127127
log.verbose("Password auth factor not available")
128128
return
129129
}
130-
131-
state.selectedAuthFactor = passwordFactor
132-
try? await state.selectAuthFactor()
130+
131+
await selectAuthFactor(passwordFactor)
133132
}
134133

135134
private func selectAuthFactor(_ factor: AuthFactor) async {

0 commit comments

Comments
 (0)