Skip to content

Commit

Permalink
Add wrappers with default values for MFA methods (#583)
Browse files Browse the repository at this point in the history
  • Loading branch information
Widcket authored Dec 9, 2021
1 parent 08340cc commit 9e9bff5
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 10 deletions.
40 changes: 40 additions & 0 deletions Auth0/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Credentials, AuthenticationError> {
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<Challenge, AuthenticationError> {
return self.multifactorChallenge(mfaToken: mfaToken, types: types, authenticatorId: authenticatorId)
}

/**
Authenticate a user with their Sign In With Apple authorization code.

Expand Down
70 changes: 60 additions & 10 deletions Auth0Tests/AuthenticationSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9e9bff5

Please sign in to comment.