diff --git a/Cartfile b/Cartfile index 4689d4508..54aba3602 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "auth0/Auth0.swift" ~> 1.0 +github "auth0/Auth0.swift" ~> 1.31 diff --git a/Cartfile.resolved b/Cartfile.resolved index f48570ddb..ce9bd1ad7 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,7 @@ github "AliSoftware/OHHTTPStubs" "9.1.0" github "Quick/Nimble" "v9.0.0" -github "Quick/Quick" "v3.0.0" -github "auth0/Auth0.swift" "1.30.1" -github "auth0/JWTDecode.swift" "2.5.0" -github "auth0/SimpleKeychain" "0.12.1" +github "Quick/Quick" "v3.1.2" +github "auth0/Auth0.swift" "1.31.0" +github "auth0/JWTDecode.swift" "2.6.0" +github "auth0/SimpleKeychain" "0.12.2" github "emaloney/CleanroomLogger" "7.0.0" diff --git a/Lock.podspec b/Lock.podspec index faa611251..6e976194e 100644 --- a/Lock.podspec +++ b/Lock.podspec @@ -18,7 +18,7 @@ Auth0 is a SaaS that helps you with Authentication and Authorization. You can us s.requires_arc = true - s.dependency 'Auth0' + s.dependency 'Auth0', '~> 1.31' s.default_subspecs = 'Classic' s.subspec 'Classic' do |classic| diff --git a/Lock/MultifactorInteractor.swift b/Lock/MultifactorInteractor.swift index d859d0498..322c417c1 100644 --- a/Lock/MultifactorInteractor.swift +++ b/Lock/MultifactorInteractor.swift @@ -23,7 +23,7 @@ import Foundation import Auth0 -struct MultifactorInteractor: MultifactorAuthenticatable, Loggable { +class MultifactorInteractor: MultifactorAuthenticatable, Loggable { private var connection: DatabaseConnection private var user: DatabaseUser @@ -32,6 +32,7 @@ struct MultifactorInteractor: MultifactorAuthenticatable, Loggable { private(set) var code: String? private(set) var validCode: Bool = false private(set) var mfaToken: String? + private(set) var challenge: Challenge? let dispatcher: Dispatcher private let validator = OneTimePasswordValidator() @@ -45,9 +46,13 @@ struct MultifactorInteractor: MultifactorAuthenticatable, Loggable { self.dispatcher = dispatcher self.options = options self.mfaToken = mfaToken + + if let mfaToken = mfaToken { + self.startMultifactorChallenge(mfaToken: mfaToken) + } } - mutating func setMultifactorCode(_ code: String?) throws { + func setMultifactorCode(_ code: String?) throws { self.validCode = false self.code = code?.trimmingCharacters(in: CharacterSet.whitespaces) if let error = self.validator.validate(code) { @@ -76,9 +81,16 @@ struct MultifactorInteractor: MultifactorAuthenticatable, Loggable { self.logger.error("Token required for OIDC MFA") return callback(.couldNotLogin) } - authentication - .login(withOTP: code, mfaToken: mfaToken) - .start { self.handle(identifier: identifier, result: $0, callback: callback) } + + if self.challenge?.challengeType == "oob", let oobCode = self.challenge?.oobCode { + authentication + .login(withOOBCode: oobCode, mfaToken: mfaToken, bindingCode: code) + .start { self.handle(identifier: identifier, result: $0, callback: callback) } + } else { + authentication + .login(withOTP: code, mfaToken: mfaToken) + .start { self.handle(identifier: identifier, result: $0, callback: callback) } + } } else { authentication .login( @@ -92,4 +104,17 @@ struct MultifactorInteractor: MultifactorAuthenticatable, Loggable { .start { self.handle(identifier: identifier, result: $0, callback: callback) } } } + + func startMultifactorChallenge(mfaToken: String, completion: ((Auth0.Result) -> Void)? = nil) { + authentication.multifactorChallenge(mfaToken: mfaToken, types: nil, channel: nil, authenticatorId: nil).start { [weak self] result in + switch result { + case let .success(response): + self?.challenge = response + case let .failure(error): + self?.logger.error("Failed to start MFA challenge \(error)") + } + + completion?(result) + } + } } diff --git a/LockTests/Interactors/MultifactorInteractorSpec.swift b/LockTests/Interactors/MultifactorInteractorSpec.swift index bd747b802..3b11e2306 100644 --- a/LockTests/Interactors/MultifactorInteractorSpec.swift +++ b/LockTests/Interactors/MultifactorInteractorSpec.swift @@ -53,6 +53,39 @@ class MultifactorInteractorSpec: QuickSpec { interactor = MultifactorInteractor(user: user, authentication: Auth0.authentication(clientId: clientId, domain: domain), connection: connection, options: options, dispatcher: dispatcher) } + describe("OOB challenge") { + + let mfaToken = "VALID_TOKEN" + + describe("start challenge") { + + it("should yield challenge on success") { + let challengeType = "oob" + let oobCode = "oob:\(UUID().uuidString)" + let bindingMethod = "binding method" + stub(condition: mfaChallengeStart(mfaToken: mfaToken)) { _ in return Auth0Stubs.multifactorChallenge(challengeType: challengeType, oobCode: oobCode, bindingMethod: bindingMethod) } + waitUntil(timeout: .seconds(2)) { done in + interactor.startMultifactorChallenge(mfaToken: mfaToken) { _ in + expect(interactor.challenge?.challengeType) == challengeType + expect(interactor.challenge?.oobCode) == oobCode + expect(interactor.challenge?.bindingMethod) == bindingMethod + done() + } + } + } + + it("should yield error on failure") { + stub(condition: mfaChallengeStart(mfaToken: mfaToken)) { _ in return Auth0Stubs.failure() } + waitUntil(timeout: .seconds(2)) { done in + interactor.startMultifactorChallenge(mfaToken: mfaToken) { _ in + expect(interactor.challenge).to(beNil()) + done() + } + } + } + } + } + describe("updateCode") { it("should update code") { @@ -243,6 +276,42 @@ class MultifactorInteractorSpec: QuickSpec { } } } + + describe("OOB challenge code") { + + beforeEach { + let oobCode = "oob:\(UUID().uuidString)" + stub(condition: mfaChallengeStart(mfaToken: mfaToken)) { _ in return Auth0Stubs.multifactorChallenge(challengeType: "oob", oobCode: oobCode) } + stub(condition: oobLogin(oob: oobCode, mfaToken: mfaToken, bindingCode: code)) { _ in return Auth0Stubs.authentication() } + } + + it("should yield no error on success") { + waitUntil(timeout: .seconds(2)) { done in + interactor.startMultifactorChallenge(mfaToken: mfaToken) { _ in + done() + } + } + + try! interactor.setMultifactorCode(code) + waitUntil(timeout: .seconds(2)) { done in + interactor.login { error in + expect(error).to(beNil()) + done() + } + } + } + + it("should yield credentials") { + waitUntil(timeout: .seconds(2)) { done in + interactor.startMultifactorChallenge(mfaToken: mfaToken) { _ in + done() + } + } + try! interactor.setMultifactorCode(code) + interactor.login { _ in } + expect(credentials).toEventuallyNot(beNil()) + } + } } } diff --git a/LockTests/Utils/Mocks.swift b/LockTests/Utils/Mocks.swift index 244b4cb26..0f978cc8a 100644 --- a/LockTests/Utils/Mocks.swift +++ b/LockTests/Utils/Mocks.swift @@ -275,6 +275,18 @@ class MockAuthentication: Authentication { return self.authentication.login(withOTP: otp, mfaToken: mfaToken) } + func login(withOOBCode oobCode: String, mfaToken: String, bindingCode: String?) -> Request { + return self.authentication.login(withOOBCode: oobCode, mfaToken: mfaToken, bindingCode: bindingCode) + } + + func login(withRecoveryCode recoveryCode: String, mfaToken: String) -> Request { + return self.authentication.login(withRecoveryCode: recoveryCode, mfaToken: mfaToken) + } + + func multifactorChallenge(mfaToken: String, types: [String]?, channel: String?, authenticatorId: String?) -> Request { + return self.authentication.multifactorChallenge(mfaToken: mfaToken, types: types, channel: channel, authenticatorId: authenticatorId) + } + func tokenExchange(withCode code: String, codeVerifier: String, redirectURI: String) -> Request { return self.authentication.tokenExchange(withCode: code, codeVerifier: codeVerifier, redirectURI: redirectURI) } diff --git a/LockTests/Utils/NetworkStub.swift b/LockTests/Utils/NetworkStub.swift index 2b68d6da4..e9c66bc90 100644 --- a/LockTests/Utils/NetworkStub.swift +++ b/LockTests/Utils/NetworkStub.swift @@ -38,7 +38,7 @@ func += (left: inout [K:V], right: [K:V]) { func realmLogin(identifier: String, password: String, realm: String) -> HTTPStubsTestBlock { var parameters = ["username": identifier, "password": password] parameters["realm"] = realm - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/oauth/token") && hasAtLeast(parameters) + return isHost(domain) && isMethodPOST() && isPath("/oauth/token") && hasAtLeast(parameters) } func databaseLogin(identifier: String, password: String, code: String? = nil, connection: String) -> HTTPStubsTestBlock { @@ -47,35 +47,47 @@ func databaseLogin(identifier: String, password: String, code: String? = nil, co parameters["mfa_code"] = code } parameters["connection"] = connection - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/oauth/ro") && hasAtLeast(parameters) + return isHost(domain) && isMethodPOST() && isPath("/oauth/ro") && hasAtLeast(parameters) } func passwordlessLogin(username: String, otp: String, realm: String) -> HTTPStubsTestBlock { let parameters = ["username": username, "otp": otp, "realm": realm, "grant_type": "http://auth0.com/oauth/grant-type/passwordless/otp"] - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/oauth/token") && hasAtLeast(parameters) + return isHost(domain) && isMethodPOST() && isPath("/oauth/token") && hasAtLeast(parameters) } func otpLogin(otp: String, mfaToken: String) -> HTTPStubsTestBlock { - let parameters = ["otp": code, "mfa_token": mfaToken] - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/oauth/token") && hasAtLeast(parameters) + let parameters = ["otp": otp, "mfa_token": mfaToken] + return isHost(domain) && isMethodPOST() && isPath("/oauth/token") && hasAtLeast(parameters) +} + +func oobLogin(oob: String, mfaToken: String, bindingCode: String? = nil) -> HTTPStubsTestBlock { + var parameters = ["oob_code": oob, "mfa_token": mfaToken] + if let bindingCode = bindingCode { + parameters["binding_code"] = bindingCode + } + return isHost(domain) && isMethodPOST() && isPath("/oauth/token") && hasAtLeast(parameters) } func databaseSignUp(email: String, username: String? = nil, password: String, connection: String) -> HTTPStubsTestBlock { var parameters = ["email": email, "password": password, "connection": connection] if let username = username { parameters["username"] = username } - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/dbconnections/signup") && hasAtLeast(parameters) + return isHost(domain) && isMethodPOST() && isPath("/dbconnections/signup") && hasAtLeast(parameters) } func databaseForgotPassword(email: String, connection: String) -> HTTPStubsTestBlock { - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/dbconnections/change_password") && hasAtLeast(["email": email, "connection": connection]) + return isHost(domain) && isMethodPOST() && isPath("/dbconnections/change_password") && hasAtLeast(["email": email, "connection": connection]) } func passwordlessStart(email: String, connection: String) -> HTTPStubsTestBlock { - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/passwordless/start") && hasAtLeast(["email": email, "connection": connection]) + return isHost(domain) && isMethodPOST() && isPath("/passwordless/start") && hasAtLeast(["email": email, "connection": connection]) } func passwordlessStart(phone: String, connection: String) -> HTTPStubsTestBlock { - return isHost("samples.auth0.com") && isMethodPOST() && isPath("/passwordless/start") && hasAtLeast(["phone_number": phone, "connection": connection]) + return isHost(domain) && isMethodPOST() && isPath("/passwordless/start") && hasAtLeast(["phone_number": phone, "connection": connection]) +} + +func mfaChallengeStart(mfaToken: String) -> HTTPStubsTestBlock { + return isHost(domain) && isMethodPOST() && isPath("/mfa/challenge") && hasAtLeast(["mfa_token": mfaToken]) } // MARK: - Internal Matchers @@ -192,4 +204,8 @@ struct Auth0Stubs { let jsonp = "Auth0.setClient(\(string));" return HTTPStubsResponse(data: jsonp.data(using: String.Encoding.utf8)!, statusCode: 200, headers: ["Content-Type": "application/x-javascript"]) } + + static func multifactorChallenge(challengeType: String, oobCode: String? = nil, bindingMethod: String? = nil) -> HTTPStubsResponse { + return HTTPStubsResponse(jsonObject: ["challenge_type": challengeType, "oob_code": oobCode, "binding_method": bindingMethod], statusCode: 200, headers: nil) + } } diff --git a/Package.swift b/Package.swift index bff6239aa..2955f1f6b 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "Lock", targets: ["Lock"]) ], dependencies: [ - .package(name: "Auth0", url: "https://github.com/auth0/Auth0.swift.git", .upToNextMajor(from: "1.30.0")), + .package(name: "Auth0", url: "https://github.com/auth0/Auth0.swift.git", .upToNextMajor(from: "1.31.0")), .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "9.0.0")), .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .upToNextMajor(from: "9.0.0"))