diff --git a/.gitignore b/.gitignore index 82e10ef5..6c0a1f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Auth0 Auth0.plist +!Auth0Tests/Auth0.plist # Xcode build/ diff --git a/Auth0.xcodeproj/project.pbxproj b/Auth0.xcodeproj/project.pbxproj index 07302b63..6055fc56 100644 --- a/Auth0.xcodeproj/project.pbxproj +++ b/Auth0.xcodeproj/project.pbxproj @@ -118,6 +118,8 @@ 5FE2F8C41CD1498E003628F4 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE2F8C21CD1498E003628F4 /* UserProfile.swift */; }; 5FE2F8C61CD1522F003628F4 /* UserProfileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE2F8C51CD1522F003628F4 /* UserProfileSpec.swift */; }; 5FE2F8C71CD1522F003628F4 /* UserProfileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE2F8C51CD1522F003628F4 /* UserProfileSpec.swift */; }; + 5FE686A11D1877C10075874C /* Auth0.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5FE686A01D1877C10075874C /* Auth0.plist */; }; + 5FE686A21D1877C10075874C /* Auth0.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5FE686A01D1877C10075874C /* Auth0.plist */; }; 5FF465BC1CE2AC4500F7ED8C /* Management.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF465BB1CE2AC4500F7ED8C /* Management.swift */; }; 5FF465BD1CE2AC4500F7ED8C /* Management.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF465BB1CE2AC4500F7ED8C /* Management.swift */; }; /* End PBXBuildFile section */ @@ -254,6 +256,7 @@ 5FE2F8BD1CD0EC52003628F4 /* ResponseSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseSpec.swift; sourceTree = ""; }; 5FE2F8C21CD1498E003628F4 /* UserProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 5FE2F8C51CD1522F003628F4 /* UserProfileSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfileSpec.swift; sourceTree = ""; }; + 5FE686A01D1877C10075874C /* Auth0.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Auth0.plist; sourceTree = ""; }; 5FF346491CEFEC04000799DE /* Auth0Tests.iOS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Auth0Tests.iOS-Bridging-Header.h"; sourceTree = ""; }; 5FF3464A1CEFEC04000799DE /* Auth0Tests.OSX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Auth0Tests.OSX-Bridging-Header.h"; sourceTree = ""; }; 5FF465BB1CE2AC4500F7ED8C /* Management.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Management.swift; sourceTree = ""; }; @@ -360,6 +363,7 @@ 5FF3464A1CEFEC04000799DE /* Auth0Tests.OSX-Bridging-Header.h */, 5FF346491CEFEC04000799DE /* Auth0Tests.iOS-Bridging-Header.h */, 5F06DD951CC451430011842B /* Info.plist */, + 5FE686A01D1877C10075874C /* Auth0.plist */, ); path = Auth0Tests; sourceTree = ""; @@ -758,6 +762,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5FE686A11D1877C10075874C /* Auth0.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -765,6 +770,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5FE686A21D1877C10075874C /* Auth0.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Auth0/Auth0.swift b/Auth0/Auth0.swift index 08ace26e..5ce0e86d 100644 --- a/Auth0/Auth0.swift +++ b/Auth0/Auth0.swift @@ -62,12 +62,13 @@ public func authentication(clientId clientId: String, domain: String, session: N ``` - parameter session: instance of NSURLSession used for networking. By default it will use the shared NSURLSession + - parameter bundle: bundle used to locate the `Auth0.plist` file. By default is the main bundle - returns: Auth0 Authentication API - important: Calling this method without a valid `Auth0.plist` will crash your application */ -public func authentication(session session: NSURLSession = .sharedSession()) -> Authentication { - let values = plistValues()! +public func authentication(session session: NSURLSession = .sharedSession(), bundle: NSBundle = NSBundle.mainBundle()) -> Authentication { + let values = plistValues(bundle: bundle)! return authentication(clientId: values.clientId, domain: values.domain, session: session) } @@ -95,13 +96,14 @@ public func authentication(session session: NSURLSession = .sharedSession()) -> - parameter token: token of Management API v2 with the correct allowed scopes to perform the desired action - parameter session: instance of NSURLSession used for networking. By default it will use the shared NSURLSession + - parameter bundle: bundle used to locate the `Auth0.plist` file. By default is the main bundle - returns: Auth0 Management API v2 - important: Auth0.swift has yet to implement all endpoints. Now you can only perform some CRUD operations against Users - important: Calling this method without a valid `Auth0.plist` will crash your application */ -public func management(token token: String, session: NSURLSession = .sharedSession()) -> Management { - let values = plistValues()! +public func management(token token: String, session: NSURLSession = .sharedSession(), bundle: NSBundle = NSBundle.mainBundle()) -> Management { + let values = plistValues(bundle: bundle)! return management(token: token, domain: values.domain, session: session) } @@ -154,12 +156,13 @@ public func management(token token: String, domain: String, session: NSURLSessio - parameter token: token of Management API v2 with the correct allowed scopes to perform the desired action - parameter session: instance of NSURLSession used for networking. By default it will use the shared NSURLSession + - parameter bundle: bundle used to locate the `Auth0.plist` file. By default is the main bundle - returns: Auth0 Management API v2 - important: Calling this method without a valid `Auth0.plist` will crash your application */ -public func users(token token: String, session: NSURLSession = .sharedSession()) -> Users { - let values = plistValues()! +public func users(token token: String, session: NSURLSession = .sharedSession(), bundle: NSBundle = NSBundle.mainBundle()) -> Users { + let values = plistValues(bundle: bundle)! return users(token: token, domain: values.domain, session: session) } @@ -198,8 +201,7 @@ public func enableLogging(enabled enabled: Bool = true) { Auth0Logger.sharedInstance.logger = enabled ? DefaultLogger() : nil } -func plistValues() -> (clientId: String, domain: String)? { - let bundle = NSBundle.mainBundle() +func plistValues(bundle bundle: NSBundle) -> (clientId: String, domain: String)? { guard let path = bundle.pathForResource("Auth0", ofType: "plist"), let values = NSDictionary(contentsOfFile: path) as? [String: AnyObject] diff --git a/Auth0/Authentication/Authentication.swift b/Auth0/Authentication/Authentication.swift index 4ea2af79..079049a9 100644 --- a/Auth0/Authentication/Authentication.swift +++ b/Auth0/Authentication/Authentication.swift @@ -81,6 +81,7 @@ public struct Authentication { - parameter usernameOrEmail: username or email used of the user to authenticate, e.g. in email in Database connections or phone number for SMS connections. - parameter password: password of the user or one time password (OTP) for passwordless connection users + - parameter multifactorCode: multifactor code if the user has enrolled one. e.g. Guardian. By default is `nil` and no code is sent. - parameter connection: name of any of your configured database or passwordless connections - parameter scope: scope value requested when authenticating the user. Default is 'openid' - parameter parameters: additional parameters that are optionally sent with the authentication request @@ -88,7 +89,7 @@ public struct Authentication { - returns: authentication request that will yield Auth0 User Credentials - seeAlso: Credentials */ - public func login(usernameOrEmail username: String, password: String, connection: String, scope: String = "openid", parameters: [String: AnyObject] = [:]) -> Request { + public func login(usernameOrEmail username: String, password: String, multifactorCode: String? = nil, connection: String, scope: String = "openid", parameters: [String: AnyObject] = [:]) -> Request { let resourceOwner = NSURL(string: "/oauth/ro", relativeToURL: self.url)! var payload: [String: AnyObject] = [ "username": username, @@ -98,6 +99,7 @@ public struct Authentication { "scope": scope, "client_id": self.clientId, ] + payload["mfa_code"] = multifactorCode parameters.forEach { key, value in payload[key] = value } return Request(session: session, url: resourceOwner, method: "POST", handle: authenticationObject, payload: payload) } @@ -131,11 +133,11 @@ public struct Authentication { .start { print($0) } ``` - - parameter email: email of the user to create - - parameter username: username of the user if the connection requires username. By default is 'nil' - - parameter password: password for the new user - - parameter connection: name where the user will be created (Database connection) - - parameter userMetadata: additional userMetadata parameters that will be added to the newly created user. + - parameter email: email of the user to create + - parameter username: username of the user if the connection requires username. By default is 'nil' + - parameter password: password for the new user + - parameter connection: name where the user will be created (Database connection) + - parameter userMetadata: additional userMetadata parameters that will be added to the newly created user. - returns: request that will yield a created database user (just email, username and email verified flag) */ @@ -146,13 +148,8 @@ public struct Authentication { "connection": connection, "client_id": self.clientId, ] - if let username = username { - payload["username"] = username - } - - if let userMetadata = userMetadata { - payload["user_metadata"] = userMetadata - } + payload["username"] = username + payload["user_metadata"] = userMetadata let createUser = NSURL(string: "/dbconnections/signup", relativeToURL: self.url)! return Request(session: session, url: createUser, method: "POST", handle: databaseUser, payload: payload) diff --git a/Auth0/WebAuth/WebAuth.swift b/Auth0/WebAuth/WebAuth.swift index d3266b94..6e97daf2 100644 --- a/Auth0/WebAuth/WebAuth.swift +++ b/Auth0/WebAuth/WebAuth.swift @@ -45,11 +45,13 @@ import SafariServices ``` + - parameter bundle: bundle used to locate the `Auth0.plist` file. By default is the main bundle + - returns: Auth0 WebAuth component - important: Calling this method without a valid `Auth0.plist` will crash your application */ -public func webAuth() -> WebAuth { - let values = plistValues()! +public func webAuth(bundle bundle: NSBundle = NSBundle.mainBundle()) -> WebAuth { + let values = plistValues(bundle: bundle)! return webAuth(clientId: values.clientId, domain: values.domain) } diff --git a/Auth0Tests/Auth0.plist b/Auth0Tests/Auth0.plist new file mode 100644 index 00000000..0584af17 --- /dev/null +++ b/Auth0Tests/Auth0.plist @@ -0,0 +1,10 @@ + + + + + Domain + samples.auth0.com + ClientId + CLIENT_ID + + diff --git a/Auth0Tests/Auth0Spec.swift b/Auth0Tests/Auth0Spec.swift index e6fffd43..6606ef0f 100644 --- a/Auth0Tests/Auth0Spec.swift +++ b/Auth0Tests/Auth0Spec.swift @@ -45,7 +45,41 @@ class Auth0Spec: QuickSpec { it("should return authentication endpoint with domain url") { let domain = "https://mycustomdomain.com" let auth = Auth0.authentication(clientId: ClientId, domain: domain) - expect(auth.url.absoluteString).to(equal(domain)) + expect(auth.url.absoluteString) == domain + } + + it("should return management endopoint") { + let management = Auth0.management(token: "token", domain: Domain) + expect(management.token) == "token" + expect(management.url.absoluteString) == "https://\(Domain)" + } + + it("should return users endopoint") { + let users = Auth0.users(token: "token", domain: Domain) + expect(users.management.token) == "token" + expect(users.management.url.absoluteString) == "https://\(Domain)" + } + + } + + describe("plist loading") { + + let bundle = NSBundle(forClass: Auth0Spec.classForCoder()) + + it("should return authentication endpoint with account from plist") { + let auth = Auth0.authentication(bundle: bundle) + expect(auth.url.absoluteString) == "https://samples.auth0.com" + expect(auth.clientId) == "CLIENT_ID" + } + + it("should return management endpoint with domain from plist") { + let management = Auth0.management(token: "TOKEN", bundle: bundle) + expect(management.url.absoluteString) == "https://samples.auth0.com" + } + + it("should return users endpoint with domain from plist") { + let users = Auth0.users(token: "TOKEN", bundle: bundle) + expect(users.management.url.absoluteString) == "https://samples.auth0.com" } } diff --git a/Auth0Tests/Authentication/AuthenticationSpec.swift b/Auth0Tests/Authentication/AuthenticationSpec.swift index 0188d94e..aac89a82 100644 --- a/Auth0Tests/Authentication/AuthenticationSpec.swift +++ b/Auth0Tests/Authentication/AuthenticationSpec.swift @@ -550,5 +550,39 @@ class AuthenticationSpec: QuickSpec { } + describe("resource owner multifactor") { + + var code: String! + + beforeEach { + code = NSUUID().UUIDString.stringByReplacingOccurrencesOfString("-", withString: "") + stub(isResourceOwner(Domain) && hasAtLeast(["username":SupportAtAuth0, "password": ValidPassword]) && hasNoneOf(["mfa_code"])) { _ in return authFailure(error: "a0.mfa_required", description: "need multifactor") }.name = "MFA Required" + stub(isResourceOwner(Domain) && hasAtLeast(["username": SupportAtAuth0, "password": ValidPassword, "mfa_code": code])) { _ in return authResponse(accessToken: AccessToken) }.name = "MFA Login" + } + + it("should report multifactor is required") { + waitUntil(timeout: Timeout) { done in + auth + .login(usernameOrEmail: SupportAtAuth0, password: ValidPassword, connection: ConnectionName) + .start { result in + expect(result).to(beFailure { (error: AuthenticationError) in return error.isMultifactorRequired }) + done() + } + } + } + + it("should login with multifactor") { + waitUntil(timeout: Timeout) { done in + auth + .login(usernameOrEmail: SupportAtAuth0, password: ValidPassword, multifactorCode: code, connection: ConnectionName) + .start { result in + expect(result).to(haveCredentials()) + done() + } + } + } + + } + } } \ No newline at end of file diff --git a/Auth0Tests/Utils/Matchers.swift b/Auth0Tests/Utils/Matchers.swift index ff24b022..08bfc7ed 100644 --- a/Auth0Tests/Utils/Matchers.swift +++ b/Auth0Tests/Utils/Matchers.swift @@ -37,7 +37,8 @@ func hasAllOf(parameters: [String: String]) -> OHHTTPStubsTestBlock { func hasAtLeast(parameters: [String: String]) -> OHHTTPStubsTestBlock { return { request in guard let payload = request.a0_payload else { return false } - return parameters.filter { (key, _) in payload.contains { (name, _) in key == name } }.reduce(true, combine: { (initial, entry) -> Bool in + let entries = parameters.filter { (key, _) in payload.contains { (name, _) in key == name } } + return entries.count == parameters.count && entries.reduce(true, combine: { (initial, entry) -> Bool in return initial && payload[entry.0] as? String == entry.1 }) } @@ -57,6 +58,13 @@ func hasObjectAttribute(name: String, value: [String: String]) -> OHHTTPStubsTes } } +func hasNoneOf(names: [String]) -> OHHTTPStubsTestBlock { + return { request in + guard let payload = request.a0_payload else { return false } + return payload.filter { names.contains($0.0) }.isEmpty + } +} + func hasNoneOf(parameters: [String: String]) -> OHHTTPStubsTestBlock { return !hasAtLeast(parameters) }