Skip to content

Commit

Permalink
feat(WebAuthn): Adding support for retrying a confirmSignIn with WebA…
Browse files Browse the repository at this point in the history
…uthn request, if the first one fails (#158)
  • Loading branch information
sebaland authored and harsh62 committed Nov 3, 2024
1 parent ac9dde4 commit a6e760b
Show file tree
Hide file tree
Showing 12 changed files with 86 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum WebAuthnSignInState: State {
case assertingCredentials
case verifyingCredentialsAndSigningIn
case signedIn(SignedInData)
case cancelled(SignInError)
case error(SignInError, RespondToAuthChallenge)
}

extension WebAuthnSignInState {
Expand All @@ -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"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//
// SPDX-License-Identifier: Apache-2.0
//

import enum Amplify.AuthFactorType
import Foundation

extension WebAuthnSignInState {
Expand All @@ -18,10 +20,9 @@ extension WebAuthnSignInState {
oldState: StateType,
byApplying event: StateMachineEvent)
-> StateResolution<StateType> {
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)
)
}

Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit a6e760b

Please sign in to comment.