Skip to content

Commit 9d3d0b3

Browse files
authored
fix(auth): Enable signIn restart while another signIn is in progress (#2609)
1 parent 35ed09d commit 9d3d0b3

File tree

6 files changed

+149
-41
lines changed

6 files changed

+149
-41
lines changed

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/HostedUISignInHelper.swift

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ struct HostedUISignInHelper {
2626

2727
func initiateSignIn() async throws -> AuthSignInResult {
2828
try await isValidState()
29-
await prepareForSignIn()
3029
return try await doSignIn()
3130
}
3231

@@ -38,38 +37,19 @@ struct HostedUISignInHelper {
3837
}
3938
switch authenticationState {
4039
case .signingIn:
41-
continue
40+
await sendCancelSignInEvent()
4241
case .signedIn:
4342
throw AuthError.invalidState(
4443
"There is already a user in signedIn state. SignOut the user first before calling signIn",
4544
AuthPluginErrorConstants.invalidStateError, nil)
46-
default:
47-
return
48-
}
49-
}
50-
}
51-
52-
private func prepareForSignIn() async {
53-
54-
let stateSequences = await authStateMachine.listen()
55-
56-
for await state in stateSequences {
57-
guard case .configured(let authNState, _) = state else { continue }
58-
59-
switch authNState {
6045
case .signedOut:
6146
return
62-
63-
case .signingIn:
64-
Task {
65-
await sendCancelSignInEvent()
66-
}
67-
default:
68-
continue
47+
default: continue
6948
}
7049
}
7150
}
7251

52+
7353
private func doSignIn() async throws -> AuthSignInResult {
7454

7555
let oauthConfiguration: OAuthConfigurationData

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInTask.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class AWSAuthSignInTask: AuthSignInTask {
5656
"There is already a user in signedIn state. SignOut the user first before calling signIn",
5757
AuthPluginErrorConstants.invalidStateError, nil)
5858
throw error
59+
case .signingIn:
60+
await sendCancelSignInEvent()
5961
case .signedOut:
6062
return
6163
default: continue
@@ -94,6 +96,11 @@ class AWSAuthSignInTask: AuthSignInTask {
9496
throw AuthError.unknown("Sign in reached an error state")
9597
}
9698

99+
private func sendCancelSignInEvent() async {
100+
let event = AuthenticationEvent(eventType: .cancelSignIn)
101+
await authStateMachine.send(event)
102+
}
103+
97104
private func sendSignInEvent(authflowType: AuthFlowType) async {
98105
let signInData = SignInEventData(
99106
username: request.username,

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AWSAuthSignInPluginTests.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,4 +1100,70 @@ class AWSAuthSignInPluginTests: BasePluginTest {
11001100
}
11011101
}
11021102
}
1103+
1104+
/// Test a signIn restart while another sign in is in progress
1105+
///
1106+
/// - Given: Given an auth plugin with mocked service and a in progress signIn waiting for SMS verification
1107+
///
1108+
/// - When:
1109+
/// - I invoke another signIn with valid values
1110+
/// - Then:
1111+
/// - I should get a .done response
1112+
///
1113+
func testRestartSignIn() async {
1114+
1115+
self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { _ in
1116+
InitiateAuthOutputResponse(
1117+
authenticationResult: .none,
1118+
challengeName: .passwordVerifier,
1119+
challengeParameters: InitiateAuthOutputResponse.validChalengeParams,
1120+
session: "someSession")
1121+
}, mockRespondToAuthChallengeResponse: { _ in
1122+
RespondToAuthChallengeOutputResponse(
1123+
authenticationResult: .none,
1124+
challengeName: .smsMfa,
1125+
challengeParameters: [:],
1126+
session: "session")
1127+
})
1128+
1129+
let pluginOptions = AWSAuthSignInOptions(validationData: ["somekey": "somevalue"],
1130+
metadata: ["somekey": "somevalue"])
1131+
let options = AuthSignInRequest.Options(pluginOptions: pluginOptions)
1132+
1133+
do {
1134+
let result = try await plugin.signIn(username: "username", password: "password", options: options)
1135+
guard case .confirmSignInWithSMSMFACode = result.nextStep else {
1136+
XCTFail("Result should be .confirmSignInWithSMSMFACode for next step")
1137+
return
1138+
}
1139+
XCTAssertFalse(result.isSignedIn)
1140+
self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { _ in
1141+
InitiateAuthOutputResponse(
1142+
authenticationResult: .none,
1143+
challengeName: .passwordVerifier,
1144+
challengeParameters: InitiateAuthOutputResponse.validChalengeParams,
1145+
session: "someSession")
1146+
}, mockRespondToAuthChallengeResponse: { _ in
1147+
RespondToAuthChallengeOutputResponse(
1148+
authenticationResult: .init(
1149+
accessToken: Defaults.validAccessToken,
1150+
expiresIn: 300,
1151+
idToken: "idToken",
1152+
newDeviceMetadata: nil,
1153+
refreshToken: "refreshToken",
1154+
tokenType: ""),
1155+
challengeName: .none,
1156+
challengeParameters: [:],
1157+
session: "session")
1158+
})
1159+
let result2 = try await plugin.signIn(username: "username2", password: "password", options: options)
1160+
guard case .done = result2.nextStep else {
1161+
XCTFail("Result should be .confirmSignInWithSMSMFACode for next step")
1162+
return
1163+
}
1164+
XCTAssertTrue(result2.isSignedIn)
1165+
} catch {
1166+
XCTFail("Received failure with error \(error)")
1167+
}
1168+
}
11031169
}

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/HostedUITests/AWSAuthHostedUISignInTests.swift

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import XCTest
1111
@testable import Amplify
1212
@testable import AWSCognitoAuthPlugin
1313
import AuthenticationServices
14+
import AWSCognitoIdentityProvider
1415

1516
class AWSAuthHostedUISignInTests: XCTestCase {
1617

17-
var plugin: AWSCognitoAuthPlugin?
18+
var plugin: AWSCognitoAuthPlugin!
1819
let networkTimeout = TimeInterval(5)
20+
var mockIdentityProvider: CognitoUserPoolBehavior!
1921
var mockHostedUIResult: Result<[URLQueryItem], HostedUIError>!
2022
var mockTokenResult = ["id_token": AWSCognitoUserPoolTokens.testData.idToken,
2123
"access_token": AWSCognitoUserPoolTokens.testData.accessToken,
@@ -57,11 +59,15 @@ class AWSAuthHostedUISignInTests: XCTestCase {
5759
hostedUISessionFactory: sessionFactory,
5860
urlSessionFactory: urlSessionMock,
5961
randomStringFactory: mockRandomString)
60-
let authEnvironment = Defaults.makeDefaultAuthEnvironment(hostedUIEnvironment: environment)
61-
let stateMachine = Defaults.authStateMachineWith(environment: authEnvironment,
62-
initialState: initialState)
62+
let authEnvironment = Defaults.makeDefaultAuthEnvironment(
63+
userPoolFactory: { self.mockIdentityProvider },
64+
hostedUIEnvironment: environment)
65+
let stateMachine = Defaults.authStateMachineWith(
66+
environment: authEnvironment,
67+
initialState: initialState
68+
)
6369

64-
plugin?.configure(
70+
plugin.configure(
6571
authConfiguration: Defaults.makeDefaultAuthConfigData(withHostedUI: configuration),
6672
authEnvironment: authEnvironment,
6773
authStateMachine: stateMachine,
@@ -76,16 +82,16 @@ class AWSAuthHostedUISignInTests: XCTestCase {
7682
.init(name: "state", value: mockState),
7783
.init(name: "code", value: mockProof)
7884
])
79-
let result = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
80-
XCTAssertTrue(result!.isSignedIn)
85+
let result = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
86+
XCTAssertTrue(result.isSignedIn)
8187
}
8288

8389
@MainActor
8490
func testUserCancelSignIn() async {
8591
mockHostedUIResult = .failure(.cancelled)
8692
let expectation = expectation(description: "SignIn operation should complete")
8793
do {
88-
_ = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
94+
_ = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
8995
XCTFail("Should not succeed")
9096
} catch {
9197
guard case AuthError.service(_, _, let underlyingError) = error,
@@ -103,7 +109,7 @@ class AWSAuthHostedUISignInTests: XCTestCase {
103109
mockHostedUIResult = .failure(.cancelled)
104110
let errorExpectation = expectation(description: "SignIn operation should complete")
105111
do {
106-
_ = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
112+
_ = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
107113
XCTFail("Should not succeed")
108114
} catch {
109115
guard case AuthError.service(_, _, let underlyingError) = error,
@@ -121,8 +127,8 @@ class AWSAuthHostedUISignInTests: XCTestCase {
121127
])
122128
let signInExpectation = expectation(description: "SignIn operation should complete")
123129
do {
124-
let result = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
125-
XCTAssertTrue(result!.isSignedIn)
130+
let result = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
131+
XCTAssertTrue(result.isSignedIn)
126132
signInExpectation.fulfill()
127133
} catch {
128134
XCTFail("Should not fail with error = \(error)")
@@ -138,7 +144,7 @@ class AWSAuthHostedUISignInTests: XCTestCase {
138144
])
139145
let expectation = expectation(description: "SignIn operation should complete")
140146
do {
141-
_ = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
147+
_ = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
142148
XCTFail("Should not succeed")
143149
} catch {
144150
guard case AuthError.service = error else {
@@ -155,7 +161,7 @@ class AWSAuthHostedUISignInTests: XCTestCase {
155161
mockHostedUIResult = .failure(.invalidContext)
156162
let expectation = expectation(description: "SignIn operation should complete")
157163
do {
158-
_ = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
164+
_ = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
159165
XCTFail("Should not succeed")
160166
} catch {
161167
guard case AuthError.invalidState = error else {
@@ -183,7 +189,7 @@ class AWSAuthHostedUISignInTests: XCTestCase {
183189

184190
let expectation = expectation(description: "SignIn operation should complete")
185191
do {
186-
_ = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
192+
_ = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
187193
XCTFail("Should not succeed")
188194
} catch {
189195
guard case AuthError.service = error else {
@@ -211,7 +217,7 @@ class AWSAuthHostedUISignInTests: XCTestCase {
211217

212218
let expectation = expectation(description: "SignIn operation should complete")
213219
do {
214-
_ = try await plugin?.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
220+
_ = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
215221
XCTFail("Should not succeed")
216222
} catch {
217223
guard case AuthError.service = error else {
@@ -223,4 +229,53 @@ class AWSAuthHostedUISignInTests: XCTestCase {
223229
wait(for: [expectation], timeout: networkTimeout)
224230
}
225231

232+
233+
234+
/// Test a signIn restart while another sign in is in progress
235+
///
236+
/// - Given: Given an auth plugin with mocked service and a in progress signIn waiting for SMS verification
237+
///
238+
/// - When:
239+
/// - I invoke another signIn with valid values
240+
/// - Then:
241+
/// - I should get a .done response
242+
///
243+
@MainActor
244+
func testRestartSignInWithWebUI() async {
245+
246+
self.mockIdentityProvider = MockIdentityProvider(mockInitiateAuthResponse: { _ in
247+
InitiateAuthOutputResponse(
248+
authenticationResult: .none,
249+
challengeName: .passwordVerifier,
250+
challengeParameters: InitiateAuthOutputResponse.validChalengeParams,
251+
session: "someSession")
252+
}, mockRespondToAuthChallengeResponse: { _ in
253+
RespondToAuthChallengeOutputResponse(
254+
authenticationResult: .none,
255+
challengeName: .smsMfa,
256+
challengeParameters: [:],
257+
session: "session")
258+
})
259+
260+
let pluginOptions = AWSAuthSignInOptions(validationData: ["somekey": "somevalue"],
261+
metadata: ["somekey": "somevalue"])
262+
let options = AuthSignInRequest.Options(pluginOptions: pluginOptions)
263+
264+
do {
265+
let result = try await plugin.signIn(username: "username", password: "password", options: options)
266+
guard case .confirmSignInWithSMSMFACode = result.nextStep else {
267+
XCTFail("Result should be .confirmSignInWithSMSMFACode for next step")
268+
return
269+
}
270+
XCTAssertFalse(result.isSignedIn)
271+
mockHostedUIResult = .success([
272+
.init(name: "state", value: mockState),
273+
.init(name: "code", value: mockProof)
274+
])
275+
let result2 = try await plugin.signInWithWebUI(presentationAnchor: ASPresentationAnchor(), options: nil)
276+
XCTAssertTrue(result2.isSignedIn)
277+
} catch {
278+
XCTFail("Received failure with error \(error)")
279+
}
280+
}
226281
}

AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ class AuthSRPSignInTests: AWSAuthBaseTest {
318318
signInExpectation.fulfill()
319319
}
320320

321-
wait(for: [signInExpectation, fetchAuthSessionExpectation], timeout: networkTimeout)
321+
wait(for: [signInExpectation, fetchAuthSessionExpectation], timeout: 10)
322322
}
323323

324324
}

0 commit comments

Comments
 (0)