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

Added support for OOB and Recovery code MFA challenges #442

Merged
merged 6 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 10 additions & 0 deletions Auth0.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@
5FF2866F1D8A417B00314B72 /* _ObjectiveWebAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF2866E1D8A417B00314B72 /* _ObjectiveWebAuth.swift */; };
5FF465BC1CE2AC4500F7ED8C /* Management.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF465BB1CE2AC4500F7ED8C /* Management.swift */; };
5FF465BD1CE2AC4500F7ED8C /* Management.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF465BB1CE2AC4500F7ED8C /* Management.swift */; };
970BC36B25C27095007A7745 /* Challenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970BC36A25C27095007A7745 /* Challenge.swift */; };
970BC36C25C27095007A7745 /* Challenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970BC36A25C27095007A7745 /* Challenge.swift */; };
970BC36D25C27095007A7745 /* Challenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970BC36A25C27095007A7745 /* Challenge.swift */; };
970BC36E25C27095007A7745 /* Challenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970BC36A25C27095007A7745 /* Challenge.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -655,6 +659,7 @@
5FF346491CEFEC04000799DE /* Auth0Tests.iOS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Auth0Tests.iOS-Bridging-Header.h"; sourceTree = "<group>"; };
5FF3464A1CEFEC04000799DE /* Auth0Tests.OSX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Auth0Tests.OSX-Bridging-Header.h"; sourceTree = "<group>"; };
5FF465BB1CE2AC4500F7ED8C /* Management.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Management.swift; path = Auth0/Management.swift; sourceTree = SOURCE_ROOT; };
970BC36A25C27095007A7745 /* Challenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Challenge.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -1192,6 +1197,7 @@
5C4F552223C8FBA100C89615 /* JWKS.swift */,
5FDE874E1D8A424700EA27DC /* Credentials.swift */,
5FDE874F1D8A424700EA27DC /* Handlers.swift */,
970BC36A25C27095007A7745 /* Challenge.swift */,
);
name = Authentication;
sourceTree = "<group>";
Expand Down Expand Up @@ -1848,6 +1854,7 @@
5FDE876D1D8A424700EA27DC /* Handlers.swift in Sources */,
5FF2866F1D8A417B00314B72 /* _ObjectiveWebAuth.swift in Sources */,
5FE2F8BB1CD0EAAD003628F4 /* Response.swift in Sources */,
970BC36B25C27095007A7745 /* Challenge.swift in Sources */,
5FCCC31C1CF51DF300901E2E /* _ObjectiveManagementAPI.swift in Sources */,
5FDE87591D8A424700EA27DC /* Authentication.swift in Sources */,
5B16D88F1F7141A0009476A5 /* SafariSession.swift in Sources */,
Expand Down Expand Up @@ -1973,6 +1980,7 @@
5C41F6D9244F977900252548 /* WebAuthError.swift in Sources */,
5FCAB1741D09009600331C84 /* NSData+URLSafe.swift in Sources */,
5F7504F61D8C3F2900E3BA1C /* NSError+Helper.swift in Sources */,
970BC36C25C27095007A7745 /* Challenge.swift in Sources */,
5C41F6DD244F982700252548 /* DesktopWebAuth.swift in Sources */,
5FCAB17A1D09124D00331C84 /* NSURL+Auth0.swift in Sources */,
5FDE87521D8A424700EA27DC /* _ObjectiveAuthenticationAPI.swift in Sources */,
Expand Down Expand Up @@ -2076,6 +2084,7 @@
5CC9940424ED9EC90027DC74 /* CredentialsManagerError.swift in Sources */,
5F23E6EA1D4ACD9600C3F2D9 /* Auth0Error.swift in Sources */,
5FDE876F1D8A424700EA27DC /* Handlers.swift in Sources */,
970BC36D25C27095007A7745 /* Challenge.swift in Sources */,
5FDE87571D8A424700EA27DC /* Auth0Authentication.swift in Sources */,
5C4F552523C8FBA100C89615 /* JWKS.swift in Sources */,
5F23E6E51D4ACD8500C3F2D9 /* Request.swift in Sources */,
Expand Down Expand Up @@ -2114,6 +2123,7 @@
5F23E7111D4B88FC00C3F2D9 /* Result.swift in Sources */,
5FDE87701D8A424700EA27DC /* Handlers.swift in Sources */,
5C4F551D23C8FB8E00C89615 /* String+URLSafe.swift in Sources */,
970BC36E25C27095007A7745 /* Challenge.swift in Sources */,
5FDE87581D8A424700EA27DC /* Auth0Authentication.swift in Sources */,
5F23E7071D4B88EA00C3F2D9 /* NSURL+Auth0.swift in Sources */,
5F23E71A1D4B891E00C3F2D9 /* Auth0.swift in Sources */,
Expand Down
68 changes: 68 additions & 0 deletions Auth0/Auth0Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import Foundation

// swiftlint:disable:next type_body_length
struct Auth0Authentication: Authentication {

let clientId: String
Expand Down Expand Up @@ -133,6 +134,73 @@ struct Auth0Authentication: Authentication {
telemetry: self.telemetry)
}

func login(withOOBCode oobCode: String, mfaToken: String, bindingCode: String?) -> Request<Credentials, AuthenticationError> {
let url = URL(string: "/oauth/token", relativeTo: self.url)!
var payload: [String: Any] = [
"oob_code": oobCode,
"mfa_token": mfaToken,
"grant_type": "http://auth0.com/oauth/grant-type/mfa-oob",
"client_id": self.clientId
]

if let bindingCode = bindingCode {
payload["binding_code"] = bindingCode
}

return Request(session: session,
url: url,
method: "POST",
handle: authenticationObject,
payload: payload,
logger: self.logger,
telemetry: self.telemetry)
}

func login(withRecoveryCode recoveryCode: String, mfaToken: String) -> Request<Credentials, AuthenticationError> {
let url = URL(string: "/oauth/token", relativeTo: self.url)!
let payload: [String: Any] = [
"recovery_code": recoveryCode,
"mfa_token": mfaToken,
"grant_type": "http://auth0.com/oauth/grant-type/mfa-recovery-code",
"client_id": self.clientId
]
return Request(session: session,
url: url,
method: "POST",
handle: authenticationObject,
payload: payload,
logger: self.logger,
telemetry: self.telemetry)
}

func multifactorChallenge(mfaToken: String, types: [String]?, channel: String?, authenticatorId: String?) -> Request<Challenge, AuthenticationError> {
let url = URL(string: "/mfa/challenge", relativeTo: self.url)!
var payload: [String: String] = [
"mfa_token": mfaToken,
"client_id": self.clientId
]

if let types = types {
payload["challenge_type"] = types.joined(separator: " ")
}

if let channel = channel {
payload["oob_channel"] = channel
}

if let authenticatorId = authenticatorId {
payload["authenticator_id"] = authenticatorId
}

return Request(session: session,
url: url,
method: "POST",
handle: codable,
payload: payload,
logger: self.logger,
telemetry: self.telemetry)
}

func login(appleAuthorizationCode authorizationCode: String, fullName: PersonNameComponents?, profile: [String: Any]?, scope: String?, audience: String?) -> Request<Credentials, AuthenticationError> {
var parameters: [String: Any] = [:]
var profile: [String: Any] = profile ?? [:]
Expand Down
34 changes: 34 additions & 0 deletions Auth0/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,40 @@ public protocol Authentication: Trackable, Loggable {
*/
func login(withOTP otp: String, mfaToken: String) -> Request<Credentials, AuthenticationError>

/// Verifies multi-factor authentication (MFA) using an out-of-band (OOB) challenge (either Push notification, SMS, or Voice).
///
/// - 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 Credentials
/// - requires: Grant `http://auth0.com/oauth/grant-type/mfa-oob`. Check [our documentation](https://auth0.com/docs/clients/client-grant-types) for more info and how to enable it.
func login(withOOBCode oobCode: String, mfaToken: String, bindingCode: String?) -> Request<Credentials, AuthenticationError>

/// Verifies multi-factor authentication (MFA) using a recovery code.
/// Some multi-factor authentication (MFA) providers (such as Guardian) support using a recovery code to login. Use this method to authenticate when the user's enrolled device is unavailable, or the user cannot receive the challenge or accept it due to connectivity issues.
///
/// - Parameters:
/// - recoveryCode: Recovery code provided by the end-user
/// - mfaToken: Token returned when authentication fails due to MFA requirement
///
/// - returns: authentication request that will yield Auth0 User Credentials
/// - requires: Grant `http://auth0.com/oauth/grant-type/mfa-recovery-code`. Check [our documentation](https://auth0.com/docs/clients/client-grant-types) for more info and how to enable it.
func login(withRecoveryCode recoveryCode: String, mfaToken: String) -> Request<Credentials, AuthenticationError>

/// 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
/// - channel: The channel to use for OOB. Can only be provided when challenge type is `oob`. Accepted values are `sms`, `voice`, or `auth0`. Excluding this parameter means that your client application will accept all supported OOB channels
/// - authenticatorId: The ID of the authenticator to challenge. You can get the ID by querying the list of available authenticators for the user
func multifactorChallenge(mfaToken: String, types: [String]?, channel: String?, authenticatorId: String?) -> Request<Challenge, AuthenticationError>

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

Expand Down
33 changes: 33 additions & 0 deletions Auth0/Challenge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Challenge.swift
//
// Copyright (c) 2021 Auth0 (http://auth0.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

public struct Challenge: Codable {
public let challengeType: String
public let oobCode: String?
public let bindingMethod: String?

public enum CodingKeys: String, CodingKey {
case challengeType = "challenge_type"
case oobCode = "oob_code"
case bindingMethod = "binding_method"
}
}
74 changes: 73 additions & 1 deletion Auth0Tests/AuthenticationSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class AuthenticationSpec: QuickSpec {

}

describe("login MFA") {
describe("login MFA OTP") {

beforeEach {
stub(condition: isToken(Domain) && hasAtLeast(["otp": OTP, "mfa_token": MFAToken])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "OpenID Auth"
Expand Down Expand Up @@ -183,6 +183,78 @@ 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": "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") {
waitUntil(timeout: Timeout) { done in
auth.login(withOOBCode: OOB, mfaToken: MFAToken, bindingCode: nil).start { result in
expect(result).to(haveCredentials())
done()
}
}
}

it("should fail login with bad oob code") {
waitUntil(timeout: Timeout) { done in
auth.login(withOOBCode: "bad_oob", mfaToken: MFAToken, bindingCode: nil).start { result in
expect(result).to(haveAuthenticationError(code: "invalid_grant", description: "Invalid oob_code."))
done()
}
}
}

it("should fail login with invalid mfa") {
waitUntil(timeout: Timeout) { done in
auth.login(withOOBCode: OOB, mfaToken: "bad_token", bindingCode: nil).start { result in
expect(result).to(haveAuthenticationError(code: "invalid_grant", description: "Malformed mfa_token"))
done()
}
}
}
}

describe("login MFA recovery code") {

beforeEach {
stub(condition: isToken(Domain) && hasAtLeast(["recovery_code": RecoveryCode, "mfa_token": MFAToken])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "OpenID Auth"
stub(condition: isToken(Domain) && hasAtLeast(["recovery_code": "bad_recovery", "mfa_token": MFAToken])) { _ in return authFailure(code: "invalid_grant", description: "Invalid recovery_code.") }.name = "invalid recovery code"
stub(condition: isToken(Domain) && hasAtLeast(["recovery_code": RecoveryCode, "mfa_token": "bad_token"])) { _ in return authFailure(code: "invalid_grant", description: "Malformed mfa_token") }.name = "invalid mfa_token"
}

it("should login with recovery code and mfa tokens") {
waitUntil(timeout: Timeout) { done in
auth.login(withRecoveryCode: RecoveryCode, mfaToken: MFAToken).start { result in
expect(result).to(haveCredentials())
done()
}
}
}

it("should fail login with bad recovery code") {
waitUntil(timeout: Timeout) { done in
auth.login(withRecoveryCode: "bad_recovery", mfaToken: MFAToken).start { result in
expect(result).to(haveAuthenticationError(code: "invalid_grant", description: "Invalid recovery_code."))
done()
}
}
}

it("should fail login with invalid mfa") {
waitUntil(timeout: Timeout) { done in
auth.login(withRecoveryCode: RecoveryCode, mfaToken: "bad_token").start { result in
expect(result).to(haveAuthenticationError(code: "invalid_grant", description: "Malformed mfa_token"))
done()
}
}
}
}

// MARK:- Refresh Tokens

describe("renew auth with refresh token") {
Expand Down
3 changes: 3 additions & 0 deletions Auth0Tests/Responses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ let Sub = "auth0|\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))"
let LocaleUS = "en-US"
let ZoneEST = "US/Eastern"
let OTP = "123456"
let OOB = "654321"
let BindingCode = "214365"
let RecoveryCode = "162534"
let MFAToken = UUID().uuidString.replacingOccurrences(of: "-", with: "")
let JWKKid = "key123"

Expand Down