From 9e9bff5b290584b327f0c2d94cc2e283f92b1479 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 9 Dec 2021 12:27:16 -0300 Subject: [PATCH] Add wrappers with default values for MFA methods (#583) --- Auth0/Authentication.swift | 40 +++++++++++++++++ Auth0Tests/AuthenticationSpec.swift | 70 ++++++++++++++++++++++++----- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/Auth0/Authentication.swift b/Auth0/Authentication.swift index b67be753..598e6f96 100644 --- a/Auth0/Authentication.swift +++ b/Auth0/Authentication.swift @@ -740,6 +740,46 @@ public extension Authentication { return self.login(usernameOrEmail: username, password: password, realm: realm, audience: audience, scope: scope) } + /// Verifies multi-factor authentication (MFA) using an out-of-band (OOB) challenge (either Push notification, SMS, or Voice). + /// + /// ``` + /// Auth0 + /// .authentication(clientId: clientId, domain: "samples.auth0.com") + /// .login(withOOBCode: "123456", mfaToken: "mfa token") + /// .start { result in + /// switch result { + /// case .success(let credentials): + /// print("Obtained credentials: \(credentials)") + /// case .failure(let error): + /// print("Failed with \(error)") + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - oobCode: The oob code received from the challenge request. + /// - mfaToken: Token returned when authentication fails due to MFA requirement. + /// - bindingCode: A code used to bind the side channel (used to deliver the challenge) with the main channel you are using to authenticate. This is usually an OTP-like code delivered as part of the challenge message. + /// - Returns: Authentication request that will yield Auth0 user's credentials. + /// - Requires: Grant `http://auth0.com/oauth/grant-type/mfa-oob`. Check [our documentation](https://auth0.com/docs/configure/applications/application-grant-types) for more info and how to enable it. + func login(withOOBCode oobCode: String, mfaToken: String, bindingCode: String? = nil) -> Request { + return self.login(withOOBCode: oobCode, mfaToken: mfaToken, bindingCode: bindingCode) + } + + /// Request a challenge for multi-factor authentication (MFA) based on the challenge types supported by the application and user. + /// The `type` is how the user will get the challenge and prove possession. Supported challenge types include: + /// * `otp`: for one-time password (OTP) + /// * `oob`: for SMS/Voice messages or out-of-band (OOB) + /// + /// - Parameters: + /// - mfaToken: Token returned when authentication fails due to MFA requirement. + /// - types: A list of the challenges types accepted by your application. Accepted challenge types are `oob` or `otp`. Excluding this parameter means that your client application accepts all supported challenge types. + /// - authenticatorId: The ID of the authenticator to challenge. You can get the ID by querying the list of available authenticators for the user. + /// - Returns: A request that will yield a multi-factor challenge. + func multifactorChallenge(mfaToken: String, types: [String]? = nil, authenticatorId: String? = nil) -> Request { + return self.multifactorChallenge(mfaToken: mfaToken, types: types, authenticatorId: authenticatorId) + } + /** Authenticate a user with their Sign In With Apple authorization code. diff --git a/Auth0Tests/AuthenticationSpec.swift b/Auth0Tests/AuthenticationSpec.swift index c51ec44e..26527429 100644 --- a/Auth0Tests/AuthenticationSpec.swift +++ b/Auth0Tests/AuthenticationSpec.swift @@ -100,12 +100,22 @@ class AuthenticationSpec: QuickSpec { describe("login MFA OOB") { beforeEach { + stub(condition: isToken(Domain) && hasAtLeast(["oob_code": OOB, "mfa_token": MFAToken])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "OpenID Auth" stub(condition: isToken(Domain) && hasAtLeast(["oob_code": OOB, "mfa_token": MFAToken, "binding_code": BindingCode])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "OpenID Auth" stub(condition: isToken(Domain) && hasAtLeast(["oob_code": "bad_oob", "mfa_token": MFAToken])) { _ in return authFailure(code: "invalid_grant", description: "Invalid oob_code.") }.name = "invalid oob_code" stub(condition: isToken(Domain) && hasAtLeast(["oob_code": OOB, "mfa_token": "bad_token"])) { _ in return authFailure(code: "invalid_grant", description: "Malformed mfa_token") }.name = "invalid mfa_token" } - it("should login with oob code and mfa tokens") { + it("should login with oob code and mfa tokens with default parameters") { + waitUntil(timeout: Timeout) { done in + auth.login(withOOBCode: OOB, mfaToken: MFAToken).start { result in + expect(result).to(haveCredentials()) + done() + } + } + } + + it("should login with oob code and mfa tokens with binding code") { waitUntil(timeout: Timeout) { done in auth.login(withOOBCode: OOB, mfaToken: MFAToken, bindingCode: BindingCode).start { result in expect(result).to(haveCredentials()) @@ -174,23 +184,63 @@ class AuthenticationSpec: QuickSpec { describe("MFA challenge") { beforeEach { + stub(condition: isMultifactorChallenge(Domain) && hasAtLeast([ + "mfa_token": MFAToken, + "client_id": ClientId + ])) { _ in return multifactorChallengeResponse(challengeType: "oob") }.name = "MFA Challenge" + stub(condition: isMultifactorChallenge(Domain) && hasAtLeast([ + "mfa_token": MFAToken, + "client_id": ClientId, + "challenge_type": "oob otp" + ])) { _ in return multifactorChallengeResponse(challengeType: "oob") }.name = "MFA Challenge" + stub(condition: isMultifactorChallenge(Domain) && hasAtLeast([ + "mfa_token": MFAToken, + "client_id": ClientId, + "authenticator_id": AuthenticatorId + ])) { _ in return multifactorChallengeResponse(challengeType: "oob") }.name = "MFA Challenge" stub(condition: isMultifactorChallenge(Domain) && hasAtLeast([ "mfa_token": MFAToken, "client_id": ClientId, "challenge_type": "oob otp", "authenticator_id": AuthenticatorId - ])) { _ in return multifactorChallengeResponse(challengeType: "oob") }.name = "MFA Challenge" + ])) { _ in return multifactorChallengeResponse(challengeType: "oob") }.name = "MFA Challenge" } - it("should request without filters") { + it("should request MFA challenge with default parameters") { waitUntil(timeout: Timeout) { done in - auth.multifactorChallenge(mfaToken: MFAToken, types: ChallengeTypes, authenticatorId: AuthenticatorId).start { result in + auth.multifactorChallenge(mfaToken: MFAToken).start { result in + expect(result).to(beSuccessful()) + done() + } + } + } + + it("should request MFA challenge with challenge types") { + waitUntil(timeout: Timeout) { done in + auth.multifactorChallenge(mfaToken: MFAToken, types: ChallengeTypes).start { result in expect(result).to(beSuccessful()) done() } } } + it("should request MFA challenge with authenticator id") { + waitUntil(timeout: Timeout) { done in + auth.multifactorChallenge(mfaToken: MFAToken, authenticatorId: AuthenticatorId).start { result in + expect(result).to(beSuccessful()) + done() + } + } + } + + it("should request MFA challenge with all parameters") { + waitUntil(timeout: Timeout) { done in + auth.multifactorChallenge(mfaToken: MFAToken, types: ChallengeTypes, authenticatorId: AuthenticatorId).start { result in + expect(result).to(beSuccessful()) + done() + } + } + } } // MARK:- Refresh Tokens @@ -513,7 +563,7 @@ class AuthenticationSpec: QuickSpec { } } - it("should handle errors") { + it("should fail to revoke token") { let code = "invalid_request" let description = "missing params" stub(condition: isRevokeToken(Domain) && hasAtLeast(["token": refreshToken])) { _ in @@ -582,7 +632,7 @@ class AuthenticationSpec: QuickSpec { } } - it("should specify audience,scope and realm in request") { + it("should specify audience, scope and realm in request") { stub(condition: isToken(Domain) && hasAtLeast(["username": SupportAtAuth0, "password": ValidPassword, "scope": "openid", "audience" : "https://myapi.com/api", "realm" : "customconnection"])) { _ in return authResponse(accessToken: AccessToken) }.name = "Grant Password Custom audience, scope and realm" waitUntil(timeout: Timeout) { done in auth.login(usernameOrEmail: SupportAtAuth0, password: ValidPassword, realm: "customconnection", audience: "https://myapi.com/api", scope: "openid").start { result in @@ -744,7 +794,7 @@ class AuthenticationSpec: QuickSpec { } } - it("should handle errors") { + it("should fail to reset password") { let code = "reset_failed" let description = "failed reset password" stub(condition: isResetPassword(Domain) && hasAllOf(["email": SupportAtAuth0, "connection": ConnectionName, "client_id": ClientId])) { _ in return authFailure(code: code, description: description) }.name = "reset failed" @@ -812,7 +862,7 @@ class AuthenticationSpec: QuickSpec { } } - it("should report failure") { + it("should fail to start") { stub(condition: isPasswordless(Domain)) { _ in return authFailure(error: "error", description: "description") }.name = "failed passwordless start" waitUntil(timeout: Timeout) { done in auth.startPasswordless(email: SupportAtAuth0).start { result in @@ -900,7 +950,7 @@ class AuthenticationSpec: QuickSpec { } } - it("should report failure") { + it("should fail to start") { stub(condition: isPasswordless(Domain)) { _ in return authFailure(error: "error", description: "description") }.name = "failed passwordless start" waitUntil(timeout: Timeout) { done in auth.startPasswordless(phoneNumber: Phone).start { result in @@ -977,7 +1027,7 @@ class AuthenticationSpec: QuickSpec { } } - it("should report failure to get user info") { + it("should fail to get user info") { stub(condition: isUserInfo(Domain)) { _ in return authFailure(error: "invalid_token", description: "the token is invalid") }.name = "token info failed" waitUntil(timeout: Timeout) { done in auth.userInfo(withAccessToken: AccessToken).start { result in