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

Support OOB MFA challenges #649

Merged
merged 4 commits into from
Feb 19, 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
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github "auth0/Auth0.swift" ~> 1.0
github "auth0/Auth0.swift" ~> 1.31
8 changes: 4 additions & 4 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion Lock.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
35 changes: 30 additions & 5 deletions Lock/MultifactorInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import Foundation
import Auth0

struct MultifactorInteractor: MultifactorAuthenticatable, Loggable {
class MultifactorInteractor: MultifactorAuthenticatable, Loggable {
Widcket marked this conversation as resolved.
Show resolved Hide resolved

private var connection: DatabaseConnection
private var user: DatabaseUser
Expand All @@ -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()
Expand All @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -92,4 +104,17 @@ struct MultifactorInteractor: MultifactorAuthenticatable, Loggable {
.start { self.handle(identifier: identifier, result: $0, callback: callback) }
}
}

func startMultifactorChallenge(mfaToken: String, completion: ((Auth0.Result<Challenge>) -> 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)
}
}
}
69 changes: 69 additions & 0 deletions LockTests/Interactors/MultifactorInteractorSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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())
}
}
}

}
Expand Down
12 changes: 12 additions & 0 deletions LockTests/Utils/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Credentials, AuthenticationError> {
return self.authentication.login(withOOBCode: oobCode, mfaToken: mfaToken, bindingCode: bindingCode)
}

func login(withRecoveryCode recoveryCode: String, mfaToken: String) -> Request<Credentials, AuthenticationError> {
return self.authentication.login(withRecoveryCode: recoveryCode, mfaToken: mfaToken)
}

func multifactorChallenge(mfaToken: String, types: [String]?, channel: String?, authenticatorId: String?) -> Request<Challenge, AuthenticationError> {
return self.authentication.multifactorChallenge(mfaToken: mfaToken, types: types, channel: channel, authenticatorId: authenticatorId)
}

func tokenExchange(withCode code: String, codeVerifier: String, redirectURI: String) -> Request<Credentials, AuthenticationError> {
return self.authentication.tokenExchange(withCode: code, codeVerifier: codeVerifier, redirectURI: redirectURI)
}
Expand Down
34 changes: 25 additions & 9 deletions LockTests/Utils/NetworkStub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func += <K, V> (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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down