Skip to content

Commit 1e16641

Browse files
authored
[iOS SDK] Add new E2E test for authentication context MFA (#2520)
* Add E2E test for AC * PR Comments
1 parent 314b191 commit 1e16641

File tree

1 file changed

+122
-1
lines changed

1 file changed

+122
-1
lines changed

MSAL/test/integration/native_auth/end_to_end/mfa/MSALNativeAuthSignInWithMFAEndToEndTests.swift

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,128 @@ final class MSALNativeAuthSignInWithMFAEndToEndTests: MSALNativeAuthEndToEndPass
191191
// Now retrieve and submit the email OTP code
192192
await completeSignInWithMFAFlow(state: mfaRequiredState, username: username)
193193
}
194-
194+
195+
func test_signInAuthenticationContextClaim_mfaFlowIsTriggeredAndAccessTokenContainsClaims() async throws {
196+
throw XCTSkip("Retrieving OTP failure")
197+
#if os(macOS)
198+
throw XCTSkip("For some reason this test now requires Keychain access, reason needs to be investigated")
199+
#endif
200+
guard let username = retrieveUsernameForSignInUsernamePasswordAndMFA(),
201+
let password = await retrievePasswordForSignInUsername(),
202+
let application = initialisePublicClientApplication()
203+
else {
204+
XCTFail("Something went wrong")
205+
return
206+
}
207+
208+
let authenticationContextId = "c4"
209+
let authenticationContextRequestClaimJson = "{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"\(authenticationContextId)\"}}}"
210+
let authenticationContextATClaimJson = "\"acrs\":[\"\(authenticationContextId)\"]"
211+
212+
let parameters = MSALNativeAuthSignInParameters(username: username)
213+
parameters.password = password
214+
var error: NSError? = nil
215+
216+
parameters.claimsRequest = MSALClaimsRequest(jsonString: authenticationContextRequestClaimJson,
217+
error: &error)
218+
219+
let signInExpectation = expectation(description: "signing in")
220+
let signInDelegateSpy = SignInPasswordStartDelegateSpy(expectation: signInExpectation)
221+
222+
application.signIn(parameters: parameters, delegate: signInDelegateSpy)
223+
224+
await fulfillment(of: [signInExpectation])
225+
226+
guard signInDelegateSpy.onSignInAwaitingMFACalled, let awaitingMFAState = signInDelegateSpy.newStateAwaitingMFA else {
227+
XCTFail("Awaiting MFA not called")
228+
return
229+
}
230+
231+
// Request to send challenge to the default strong auth method
232+
let mfaExpectation = expectation(description: "mfa")
233+
let mfaDelegateSpy = MFARequestChallengeDelegateSpy(expectation: mfaExpectation)
234+
235+
awaitingMFAState.requestChallenge(delegate: mfaDelegateSpy)
236+
237+
await fulfillment(of: [mfaExpectation])
238+
239+
guard mfaDelegateSpy.onSelectionRequiredCalled, let mfaRequiredState = mfaDelegateSpy.newStateMFARequired, let authMethod = mfaDelegateSpy.authMethods?.first else {
240+
XCTFail("Selection required not triggered")
241+
return
242+
}
243+
244+
XCTAssertTrue(authMethod.channelTargetType.isEmailType)
245+
246+
// Request to send challenge to a specific strong auth method
247+
248+
let mfaSendChallengeExpectation = expectation(description: "mfa")
249+
let mfaSendChallengeDelegateSpy = MFARequestChallengeDelegateSpy(expectation: mfaSendChallengeExpectation)
250+
mfaRequiredState.requestChallenge(authMethod: authMethod, delegate: mfaSendChallengeDelegateSpy)
251+
252+
await fulfillment(of: [mfaSendChallengeExpectation])
253+
254+
guard mfaSendChallengeDelegateSpy.onVerificationRequiredCalled, let newMfaRequiredState = mfaSendChallengeDelegateSpy.newStateMFARequired else {
255+
XCTFail("Challenge not sent to MFA method")
256+
return
257+
}
258+
259+
XCTAssertNotNil(mfaSendChallengeDelegateSpy.sentTo)
260+
XCTAssertNotNil(mfaSendChallengeDelegateSpy.codeLength)
261+
XCTAssertTrue(mfaSendChallengeDelegateSpy.channelTargetType!.isEmailType)
262+
263+
guard let code = await retrieveCodeFor(email: username) else {
264+
XCTFail("OTP code could not be retrieved")
265+
return
266+
}
267+
268+
let submitChallengeExpectation = expectation(description: "submitChallenge")
269+
let mfaSubmitChallengeDelegateSpy = MFASubmitChallengeDelegateSpy(expectation: submitChallengeExpectation)
270+
271+
newMfaRequiredState.submitChallenge(challenge: code, delegate: mfaSubmitChallengeDelegateSpy)
272+
273+
await fulfillment(of: [submitChallengeExpectation])
274+
275+
XCTAssertTrue(mfaSubmitChallengeDelegateSpy.onSignInCompletedCalled)
276+
XCTAssertNotNil(mfaSubmitChallengeDelegateSpy.result)
277+
XCTAssertNotNil(mfaSubmitChallengeDelegateSpy.result?.idToken)
278+
XCTAssertEqual(mfaSubmitChallengeDelegateSpy.result?.account.username, username)
279+
280+
let geAccessTokenExpectation = expectation(description: "get access token")
281+
let credentialsDelegateSpy = CredentialsDelegateSpy(expectation: geAccessTokenExpectation)
282+
283+
signInDelegateSpy.result?.getAccessToken(parameters: MSALNativeAuthGetAccessTokenParameters(), delegate: credentialsDelegateSpy)
284+
285+
await fulfillment(of: [geAccessTokenExpectation])
286+
287+
XCTAssertTrue(credentialsDelegateSpy.onAccessTokenRetrieveCompletedCalled)
288+
XCTAssertNotNil(credentialsDelegateSpy.result?.accessToken)
289+
290+
let atParts = credentialsDelegateSpy.result?.accessToken.components(separatedBy: ".")
291+
292+
// It should have 3 parts
293+
guard let atParts, atParts.count == 3 else {
294+
XCTFail("Invalid Access token received")
295+
return
296+
}
297+
298+
// We need to use the middle part
299+
var atBody: String! = atParts[1]
300+
301+
//There could be the case that the length of the access token is not a multiple of 4 so we pad it with "="
302+
let length = Double(atBody.lengthOfBytes(using: String.Encoding.utf8))
303+
let requiredLength = 4 * ceil(length / 4.0)
304+
let paddingLength = requiredLength - length
305+
if paddingLength > 0 {
306+
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
307+
atBody = atBody + padding
308+
}
309+
310+
let atEncodedData = Data(base64Encoded: atBody!, options: .ignoreUnknownCharacters)
311+
let atString = String(data: atEncodedData!, encoding: .utf8)!
312+
313+
XCTAssertTrue(atString.contains(authenticationContextATClaimJson))
314+
}
315+
195316
// MARK: private methods
196317

197318
private func signInUsernameAndPassword(username: String, password: String) async -> AwaitingMFAState? {

0 commit comments

Comments
 (0)