diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/AssertWebAuthnCredentials.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/AssertWebAuthnCredentials.swift index 3b3ceab8c7..6331f56547 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/AssertWebAuthnCredentials.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/AssertWebAuthnCredentials.swift @@ -53,14 +53,15 @@ struct AssertWebAuthnCredentials: Action { await dispatcher.send(event) } catch let error as WebAuthnError { logVerbose("\(#fileID) Raised error \(error)", environment: environment) - let event = SignInEvent( - eventType: .throwAuthError(.webAuthn(error)) + let event = WebAuthnEvent( + eventType: .error(error, respondToAuthChallenge) ) await dispatcher.send(event) } catch { logVerbose("\(#fileID) Raised error \(error)", environment: environment) - let event = SignInEvent( - eventType: .throwAuthError(.service(error: error)) + let webAuthnError = WebAuthnError.service(error: error) + let event = WebAuthnEvent( + eventType: .error(webAuthnError, respondToAuthChallenge) ) await dispatcher.send(event) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/FetchCredentialOptions.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/FetchCredentialOptions.swift index 862629145d..95c1ae2a13 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/FetchCredentialOptions.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/FetchCredentialOptions.swift @@ -66,9 +66,9 @@ struct FetchCredentialOptions: Action { await dispatcher.send(event) } catch { logVerbose("\(#fileID) Caught error \(error)", environment: environment) - let authError = SignInError.service(error: error) - let event = SignInEvent( - eventType: .throwAuthError(authError) + let webAuthnError = WebAuthnError.service(error: error) + let event = WebAuthnEvent( + eventType: .error(webAuthnError, respondToAuthChallenge) ) await dispatcher.send(event) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/InitializeWebAuthn.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/InitializeWebAuthn.swift index 2a592a92fe..eccb11e144 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/InitializeWebAuthn.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/InitializeWebAuthn.swift @@ -44,15 +44,17 @@ struct InitializeWebAuthn: Action { await dispatcher.send(event) } catch let error as SignInError { logVerbose("\(#fileID) Raised error \(error)", environment: environment) - let event = SignInEvent( - eventType: .throwAuthError(error) + let webAuthnError = WebAuthnError.service(error: error) + let event = WebAuthnEvent( + eventType: .error(webAuthnError, respondToAuthChallenge) ) await dispatcher.send(event) } catch { logVerbose("\(#fileID) Caught error \(error)", environment: environment) let authError = SignInError.service(error: error) - let event = SignInEvent( - eventType: .throwAuthError(authError) + let webAuthnError = WebAuthnError.service(error: error) + let event = WebAuthnEvent( + eventType: .error(webAuthnError, respondToAuthChallenge) ) await dispatcher.send(event) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/VerifyWebAuthnCredential.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/VerifyWebAuthnCredential.swift index c5682f7351..36c250c154 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/VerifyWebAuthnCredential.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/VerifyWebAuthnCredential.swift @@ -69,8 +69,9 @@ struct VerifyWebAuthnCredential: Action { await dispatcher.send(event) } catch { logVerbose("\(#fileID) Caught error \(error)", environment: environment) - let event = SignInEvent( - eventType: .throwAuthError(.service(error: error)) + let webAuthnError = WebAuthnError.service(error: error) + let event = WebAuthnEvent( + eventType: .error(webAuthnError, respondToAuthChallenge) ) await dispatcher.send(event) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/WebAuthnSignInData.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/WebAuthnSignInData.swift index a114fbba0a..0d303c903c 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/WebAuthnSignInData.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/WebAuthnSignInData.swift @@ -10,21 +10,7 @@ import Foundation struct WebAuthnSignInData { let username: String - private(set) var presentationAnchor: AuthUIPresentationAnchor? = nil - - init( - username: String, - presentationAnchor: AuthUIPresentationAnchor? = nil - ) { - self.username = username - self.presentationAnchor = presentationAnchor - } -} - -extension WebAuthnSignInData: Codable { - private enum CodingKeys: String, CodingKey { - case username - } + let presentationAnchor: AuthUIPresentationAnchor? } extension WebAuthnSignInData: Equatable {} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/WebAuthnEvent.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/WebAuthnEvent.swift index 20df0fe139..87aba8161b 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/WebAuthnEvent.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/WebAuthnEvent.swift @@ -15,6 +15,7 @@ struct WebAuthnEvent: StateMachineEvent { case assertCredentials(CredentialAssertionOptions, Input) case verifyCredentialsAndSignIn(String, Input) case signedIn(SignedInData) + case error(WebAuthnError, RespondToAuthChallenge) } let id: String @@ -27,6 +28,7 @@ struct WebAuthnEvent: StateMachineEvent { case .assertCredentials: return "WebAuthnEvent.assertCredentials" case .verifyCredentialsAndSignIn: return "WebAuthnEvent.verifyCredentials" case .signedIn: return "WebAuthnEvent.signedIn" + case .error: return "WebAuthnEvent.error" } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/WebAuthnSignInState+Debug.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/WebAuthnSignInState+Debug.swift index ef968b23dd..6def4fa311 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/WebAuthnSignInState+Debug.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/DebugInfo/WebAuthnSignInState+Debug.swift @@ -22,8 +22,8 @@ extension WebAuthnSignInState: CustomDebugDictionaryConvertible { additionalMetadataDictionary = [:] case .verifyingCredentialsAndSigningIn: additionalMetadataDictionary = [:] - case .cancelled(let error): - additionalMetadataDictionary = ["Cancelled": error] + case .error(let error, _): + additionalMetadataDictionary = ["Error": error] case .signedIn(let signedInData): additionalMetadataDictionary = ["SignedInData": signedInData.debugDictionary] } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/WebAuthnSignInState.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/WebAuthnSignInState.swift index 422d8e4b51..52f193d9c7 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/WebAuthnSignInState.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/States/WebAuthnSignInState.swift @@ -11,7 +11,7 @@ enum WebAuthnSignInState: State { case assertingCredentials case verifyingCredentialsAndSigningIn case signedIn(SignedInData) - case cancelled(SignInError) + case error(SignInError, RespondToAuthChallenge) } extension WebAuthnSignInState { @@ -22,7 +22,7 @@ extension WebAuthnSignInState { case .assertingCredentials: return "WebAuthnSignInState.assertingCredentialsWithAuthenticator" case .verifyingCredentialsAndSigningIn: return "WebAuthnSignInState.verifyingCredentials" case .signedIn: return "WebAuthnSignInState.signedIn" - case .cancelled: return "WebAuthnSignInState.cancelled" + case .error: return "WebAuthnSignInState.error" } } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInState+Resolver.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInState+Resolver.swift index 2352ec5d15..972ce72370 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInState+Resolver.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInState+Resolver.swift @@ -464,18 +464,44 @@ extension SignInState { } return .from(oldState) - case .signingInWithWebAuthn(let oldState): + case .signingInWithWebAuthn(let webAuthnState): if #available(iOS 17.4, macOS 13.5, *) { + if case .throwAuthError(let error) = event.isSignInEvent { + let action = ThrowSignInError(error: error) + return .init( + newState: .error, + actions: [action] + ) + } + + if case .initiateWebAuthnSignIn(let data, let respondToAuthChallenge) = event.isSignInEvent { + let action = InitializeWebAuthn( + username: data.username, + respondToAuthChallenge: respondToAuthChallenge, + presentationAnchor: data.presentationAnchor + ) + return .init( + newState: .signingInWithWebAuthn(.notStarted), + actions: [action] + ) + } + let resolution = WebAuthnSignInState.Resolver().resolve( - oldState: oldState, + oldState: webAuthnState, byApplying: event ) - let signInState = SignInState.signingInWithWebAuthn(resolution.newState) - return .init(newState: signInState, actions: resolution.actions) + return .init( + newState: .signingInWithWebAuthn(resolution.newState), + actions: resolution.actions + ) } else { // "WebAuthn is not supported in this OS version // It should technically never happen. - return .init(newState: .error) + let error = SignInError.unknown(message: "WebAuthn is not supported in this OS version") + return .init( + newState: .error, + actions: [ThrowSignInError(error: error)] + ) } } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/WebAuthnSignInState+Resolver.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/WebAuthnSignInState+Resolver.swift index c848f9af9b..1375b34489 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/WebAuthnSignInState+Resolver.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/WebAuthnSignInState+Resolver.swift @@ -4,6 +4,8 @@ // // SPDX-License-Identifier: Apache-2.0 // + +import enum Amplify.AuthFactorType import Foundation extension WebAuthnSignInState { @@ -18,10 +20,9 @@ extension WebAuthnSignInState { oldState: StateType, byApplying event: StateMachineEvent) -> StateResolution { - if let signInEvent = event as? SignInEvent, - case .throwAuthError(let error) = signInEvent.eventType { - return StateResolution( - newState: WebAuthnSignInState.cancelled(error) + if case .error(let error, let challenge) = event.isWebAuthnEvent { + return .init( + newState: .error(.webAuthn(error), challenge) ) } @@ -75,8 +76,23 @@ extension WebAuthnSignInState { } case .signedIn: return .from(oldState) - case .cancelled(_): - return .from(oldState) + case .error(_, let challenge): + // The WebAuthn flow can be retried on error state when confirming Sign In, + // so if we receive a new .verifyChallengeAnswer event for WebAuthn, we'll restart the flow + if case .verifyChallengeAnswer(let data) = event.isChallengeEvent, + let authFactorType = AuthFactorType(rawValue: data.answer), + case .webAuthn = authFactorType { + let action = VerifySignInChallenge( + challenge: challenge, + confirmSignEventData: data, + signInMethod: .apiBased(.userAuth), + currentSignInStep: .continueSignInWithFirstFactorSelection([authFactorType]) + ) + return .init( + newState: .notStarted, + actions: [action] + ) + } } return .from(oldState) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift index e89e6e1c92..73f27d8ab8 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift @@ -53,7 +53,7 @@ struct UserPoolSignInHelper: DefaultLogger { return .init(nextStep: .continueSignInWithTOTPSetup( .init(sharedSecret: totpSetupData.secretCode, username: totpSetupData.username))) } else if case .signingInWithWebAuthn(let webAuthnState) = signInState, - case .cancelled(let signInError) = webAuthnState { + case .error(let signInError, _) = webAuthnState { return try validateError(signInError: signInError) } return nil diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift index d9a93f9dbf..edb3fa91da 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift @@ -74,6 +74,14 @@ class AWSAuthConfirmSignInTask: AuthConfirmSignInTask, DefaultLogger { default: throw invalidStateError } + } else if case .signingInWithWebAuthn(let webAuthnState) = signInState { + switch webAuthnState { + case .error: + log.verbose("Sending initiate webAuthn signIn event: \(webAuthnState)") + await sendConfirmSignInEvent() + default: + throw invalidStateError + } } let stateSequences = await authStateMachine.listen()