Skip to content

Commit 5ca7336

Browse files
committed
fix(auth): Device binding add retry incase of device not found (#2699)
1 parent c5ed343 commit 5ca7336

File tree

10 files changed

+174
-137
lines changed

10 files changed

+174
-137
lines changed

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SRPAuth/VerifyPasswordSRP.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ struct VerifyPasswordSRP: Action {
2525
environment: Environment) async {
2626

2727
logVerbose("\(#fileID) Starting execution", environment: environment)
28-
2928
let inputUsername = stateData.username
3029
var username = inputUsername
3130
var deviceMetadata = DeviceMetadata.noData
@@ -37,7 +36,6 @@ struct VerifyPasswordSRP: Action {
3736

3837
username = parameters["USERNAME"] ?? inputUsername
3938
let userIdForSRP = parameters["USER_ID_FOR_SRP"] ?? inputUsername
40-
4139
let saltHex = try saltHex(parameters)
4240
let secretBlockString = try secretBlockString(parameters)
4341
let secretBlock = try secretBlock(secretBlockString)
@@ -73,16 +71,42 @@ struct VerifyPasswordSRP: Action {
7371
let event = SignInEvent(
7472
eventType: .throwPasswordVerifierError(error)
7573
)
74+
logVerbose("\(#fileID) Sending event \(event)",
75+
environment: environment)
76+
await dispatcher.send(event)
77+
} catch let error where deviceNotFound(error: error, deviceMetadata: deviceMetadata) {
78+
logVerbose("\(#fileID) Received device not found \(error)", environment: environment)
79+
// Remove the saved device details and retry password verify
80+
await DeviceMetadataHelper.removeDeviceMetaData(of: username, environment: environment)
81+
let event = SignInEvent(eventType: .retryRespondPasswordVerifier(stateData, authResponse))
82+
logVerbose("\(#fileID) Sending event \(event)",
83+
environment: environment)
7684
await dispatcher.send(event)
7785
} catch {
7886
logVerbose("\(#fileID) SRPSignInError Generic \(error)", environment: environment)
7987
let authError = SignInError.service(error: error)
8088
let event = SignInEvent(
8189
eventType: .throwAuthError(authError)
8290
)
91+
logVerbose("\(#fileID) Sending event \(event)",
92+
environment: environment)
8393
await dispatcher.send(event)
8494
}
8595
}
96+
97+
func deviceNotFound(error: Error, deviceMetadata: DeviceMetadata) -> Bool {
98+
99+
// If deviceMetadata was not send, the error returned is not from device not found.
100+
if case .noData = deviceMetadata {
101+
return false
102+
}
103+
104+
if let serviceError: RespondToAuthChallengeOutputError = error.internalAWSServiceError(),
105+
case .resourceNotFoundException = serviceError {
106+
return true
107+
}
108+
return false
109+
}
86110
}
87111

88112
extension VerifyPasswordSRP: DefaultLogger { }

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,43 @@ struct VerifySignInChallenge: Action {
5454
logVerbose("\(#fileID) Sending event \(responseEvent)",
5555
environment: environment)
5656
await dispatcher.send(responseEvent)
57+
} catch let error where deviceNotFound(error: error, deviceMetadata: deviceMetadata) {
58+
logVerbose("\(#fileID) Received device not found \(error)", environment: environment)
59+
// Remove the saved device details and retry verify challenge
60+
await DeviceMetadataHelper.removeDeviceMetaData(of: username, environment: environment)
61+
let event = SignInChallengeEvent(
62+
eventType: .retryVerifyChallengeAnswer(confirmSignEventData)
63+
)
64+
logVerbose("\(#fileID) Sending event \(event)", environment: environment)
65+
await dispatcher.send(event)
5766
} catch let error as SignInError {
5867
let errorEvent = SignInEvent(eventType: .throwAuthError(error))
5968
logVerbose("\(#fileID) Sending event \(errorEvent)",
6069
environment: environment)
6170
await dispatcher.send(errorEvent)
6271
} catch {
63-
let error = SignInError.invalidServiceResponse(message: error.localizedDescription)
72+
let error = SignInError.service(error: error)
6473
let errorEvent = SignInEvent(eventType: .throwAuthError(error))
6574
logVerbose("\(#fileID) Sending event \(errorEvent)",
6675
environment: environment)
6776
await dispatcher.send(errorEvent)
6877
}
6978
}
7079

80+
func deviceNotFound(error: Error, deviceMetadata: DeviceMetadata) -> Bool {
81+
82+
// If deviceMetadata was not send, the error returned is not from device not found.
83+
if case .noData = deviceMetadata {
84+
return false
85+
}
86+
87+
if let serviceError: RespondToAuthChallengeOutputError = error.internalAWSServiceError(),
88+
case .resourceNotFoundException = serviceError {
89+
return true
90+
}
91+
return false
92+
}
93+
7194
}
7295

7396
extension VerifySignInChallenge: CustomDebugDictionaryConvertible {

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInChallengeEvent.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ struct SignInChallengeEvent: StateMachineEvent {
1515

1616
case verifyChallengeAnswer(ConfirmSignInEventData)
1717

18+
case retryVerifyChallengeAnswer(ConfirmSignInEventData)
19+
1820
case verified
1921

2022
}
@@ -28,6 +30,7 @@ struct SignInChallengeEvent: StateMachineEvent {
2830
case .verified: return "SignInChallengeEvent.verified"
2931
case .verifyChallengeAnswer: return "SignInChallengeEvent.verifyChallengeAnswer"
3032
case .waitForAnswer: return "SignInChallengeEvent.waitForAnswer"
33+
case .retryVerifyChallengeAnswer: return "SignInChallengeEvent.retryVerifyChallengeAnswer"
3134
}
3235
}
3336

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignInEvent.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ struct SignInEvent: StateMachineEvent {
2929

3030
case respondPasswordVerifier(SRPStateData, InitiateAuthOutputResponse)
3131

32+
case retryRespondPasswordVerifier(SRPStateData, InitiateAuthOutputResponse)
33+
3234
case initiateDeviceSRP(Username, SignInResponseBehavior)
3335

3436
case respondDeviceSRPChallenge(Username, SignInResponseBehavior)
@@ -72,6 +74,7 @@ struct SignInEvent: StateMachineEvent {
7274
case .throwAuthError: return "SignInEvent.throwAuthError"
7375
case .receivedChallenge: return "SignInEvent.receivedChallenge"
7476
case .verifySMSChallenge: return "SignInEvent.verifySMSChallenge"
77+
case .retryRespondPasswordVerifier: return "SignInEvent.retryRespondPasswordVerifier"
7578
}
7679
}
7780

@@ -104,7 +107,8 @@ extension SignInEvent.EventType: Equatable {
104107
(.cancelSRPSignIn, .cancelSRPSignIn),
105108
(.throwAuthError, .throwAuthError),
106109
(.receivedChallenge, .receivedChallenge),
107-
(.verifySMSChallenge, .verifySMSChallenge):
110+
(.verifySMSChallenge, .verifySMSChallenge),
111+
(.retryRespondPasswordVerifier, .retryRespondPasswordVerifier):
108112
return true
109113
default: return false
110114
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SRP/SRPSignInState+Resolver.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ extension SRPSignInState {
102102
byApplying signInEvent: SignInEvent)
103103
-> StateResolution<SRPSignInState> {
104104
switch signInEvent.eventType {
105+
case .retryRespondPasswordVerifier(let srpStateData, let authResponse):
106+
let action = VerifyPasswordSRP(stateData: srpStateData,
107+
authResponse: authResponse)
108+
return StateResolution(
109+
newState: SRPSignInState.respondingPasswordVerifier(srpStateData),
110+
actions: [action]
111+
)
105112
case .finalizeSignIn(let signedInData):
106113
return .init(newState: .signedIn(signedInData),
107114
actions: [SignInComplete(signedInData: signedInData)])

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignIn/SignInChallengeState+Resolver.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ extension SignInChallengeState {
4242

4343
case .verifying(let challenge, let signInMethod, _):
4444

45+
if case .retryVerifyChallengeAnswer(let answerEventData) = event.isChallengeEvent {
46+
let action = VerifySignInChallenge(
47+
challenge: challenge,
48+
confirmSignEventData: answerEventData,
49+
signInMethod: signInMethod)
50+
return .init(
51+
newState: .verifying(challenge, signInMethod, answerEventData.answer),
52+
actions: [action]
53+
)
54+
}
55+
4556
if case .finalizeSignIn(let signedInData) = event.isSignInEvent {
4657
return .init(newState: .verified,
4758
actions: [SignInComplete(signedInData: signedInData)])

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,13 @@ struct UserPoolSignInHelper: DefaultLogger {
8080
environment: UserPoolEnvironment) async throws -> StateMachineEvent {
8181

8282
let client = try environment.cognitoUserPoolFactory()
83-
84-
do {
85-
let response = try await client.respondToAuthChallenge(input: request)
86-
let event = try self.parseResponse(response, for: username, signInMethod: signInMethod)
87-
return event
88-
} catch {
89-
let authError = SignInError.service(error: error)
90-
return SignInEvent(eventType: .throwAuthError(authError))
91-
}
83+
let response = try await client.respondToAuthChallenge(input: request)
84+
let event = try self.parseResponse(response, for: username, signInMethod: signInMethod)
85+
return event
9286
}
9387

88+
89+
9490
static func parseResponse(
9591
_ response: SignInResponseBehavior,
9692
for username: String,

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/InitiateAuthSRP/VerifyPasswordSRPTests.swift

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -419,52 +419,52 @@ class VerifyPasswordSRPTests: XCTestCase {
419419
await waitForExpectations(timeout: 0.1)
420420
}
421421

422-
// /// Test verify password retry on device not found
423-
// ///
424-
// /// - Given: VerifyPasswordSRP action with mocked cognito client and configuration
425-
// /// - When:
426-
// /// - I invoke the action with valid input and mock empty device not found error from Cognito
427-
// /// - Then:
428-
// /// - Should send an event with retryRespondPasswordVerifier
429-
// ///
430-
// func testPasswordVerifierWithDeviceNotFound() async {
431-
//
432-
// let identityProviderFactory: CognitoFactory = {
433-
// MockIdentityProvider(
434-
// mockRespondToAuthChallengeResponse: { _ in
435-
// throw RespondToAuthChallengeOutputError.resourceNotFoundException(
436-
// ResourceNotFoundException()
437-
// )
438-
// })
439-
// }
440-
//
441-
// let environment = Defaults.makeDefaultAuthEnvironment(
442-
// userPoolFactory: identityProviderFactory)
443-
//
444-
// let data = InitiateAuthOutputResponse.validTestData
445-
// let action = VerifyPasswordSRP(stateData: SRPStateData.testData,
446-
// authResponse: data)
447-
//
448-
// let passwordVerifierError = expectation(description: "passwordVerifierError")
449-
//
450-
// let dispatcher = MockDispatcher { event in
451-
// defer { passwordVerifierError.fulfill() }
452-
//
453-
// guard let event = event as? SignInEvent else {
454-
// XCTFail("Expected event to be SignInEvent but got \(event)")
455-
// return
456-
// }
457-
//
458-
// guard case .retryRespondPasswordVerifier = event.eventType
459-
// else {
460-
// XCTFail("Should receive retryRespondPasswordVerifier")
461-
// return
462-
// }
463-
// }
464-
//
465-
// await action.execute(withDispatcher: dispatcher, environment: environment)
466-
// await waitForExpectations(timeout: 0.1)
467-
// }
422+
/// Test verify password retry on device not found
423+
///
424+
/// - Given: VerifyPasswordSRP action with mocked cognito client and configuration
425+
/// - When:
426+
/// - I invoke the action with valid input and mock empty device not found error from Cognito
427+
/// - Then:
428+
/// - Should send an event with retryRespondPasswordVerifier
429+
///
430+
func testPasswordVerifierWithDeviceNotFound() async {
431+
432+
let identityProviderFactory: CognitoFactory = {
433+
MockIdentityProvider(
434+
mockRespondToAuthChallengeResponse: { _ in
435+
throw RespondToAuthChallengeOutputError.resourceNotFoundException(
436+
ResourceNotFoundException()
437+
)
438+
})
439+
}
440+
441+
let environment = Defaults.makeDefaultAuthEnvironment(
442+
userPoolFactory: identityProviderFactory)
443+
444+
let data = InitiateAuthOutputResponse.validTestData
445+
let action = VerifyPasswordSRP(stateData: SRPStateData.testData,
446+
authResponse: data)
447+
448+
let passwordVerifierError = expectation(description: "passwordVerifierError")
449+
450+
let dispatcher = MockDispatcher { event in
451+
defer { passwordVerifierError.fulfill() }
452+
453+
guard let event = event as? SignInEvent else {
454+
XCTFail("Expected event to be SignInEvent but got \(event)")
455+
return
456+
}
457+
458+
guard case .retryRespondPasswordVerifier = event.eventType
459+
else {
460+
XCTFail("Should receive retryRespondPasswordVerifier")
461+
return
462+
}
463+
}
464+
465+
await action.execute(withDispatcher: dispatcher, environment: environment)
466+
await waitForExpectations(timeout: 0.1)
467+
}
468468

469469
/// Test successful response from the VerifyPasswordSRP for confirmDevice
470470
///

0 commit comments

Comments
 (0)