Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wrappers with default values for MFA methods #583

Merged
merged 2 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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