From 8ab2408f90cfc207d515187f05b70df122796372 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 30 Nov 2021 11:33:26 -0300 Subject: [PATCH 1/2] Add support for custom URL sessions to Web Auth (#556) --- Auth0/Auth0.swift | 8 +++--- Auth0/Auth0WebAuth.swift | 41 +++++++++++++++++------------ Auth0/AuthenticationError.swift | 2 +- Auth0/JWK+RSA.swift | 1 - Auth0/ManagementError.swift | 2 +- Auth0/OAuth2Grant.swift | 1 - Auth0/WebAuth.swift | 10 ++++--- Auth0Tests/AuthenticationSpec.swift | 27 ++++++++++++++++++- Auth0Tests/ManagementSpec.swift | 17 +++++++++--- Auth0Tests/WebAuthSpec.swift | 30 +++++++++++++++++++++ V2_MIGRATION_GUIDE.md | 2 +- 11 files changed, 107 insertions(+), 34 deletions(-) diff --git a/Auth0/Auth0.swift b/Auth0/Auth0.swift index e83f2f66..186abd8e 100644 --- a/Auth0/Auth0.swift +++ b/Auth0/Auth0.swift @@ -19,7 +19,7 @@ public let defaultScope = "openid profile email" - parameter clientId: clientId of your Auth0 application - parameter domain: domain of your Auth0 account. e.g.: 'samples.auth0.com' - - parameter session: instance of NSURLSession used for networking. By default it will use the shared NSURLSession + - parameter session: instance of URLSession used for networking. By default it will use the shared URLSession - returns: Auth0 Authentication API */ @@ -49,7 +49,7 @@ public func authentication(clientId: String, domain: String, session: URLSession ``` - - parameter session: instance of NSURLSession used for networking. By default it will use the shared NSURLSession + - parameter session: instance of URLSession used for networking. By default it will use the shared URLSession - parameter bundle: bundle used to locate the `Auth0.plist` file. By default is the main bundle - returns: Auth0 Authentication API @@ -90,7 +90,7 @@ public func authentication(session: URLSession = .shared, bundle: Bundle = .main ``` - 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 session: instance of URLSession used for networking. By default it will use the shared URLSession - parameter bundle: bundle used to locate the `Auth0.plist` file. By default is the main bundle - returns: Auth0 Management API v2 @@ -117,7 +117,7 @@ public func users(token: String, session: URLSession = .shared, bundle: Bundle = - parameter token: token of Management API v2 with the correct allowed scopes to perform the desired action - parameter domain: domain of your Auth0 account. e.g.: 'samples.auth0.com' - - parameter session: instance of NSURLSession used for networking. By default it will use the shared NSURLSession + - parameter session: instance of URLSession used for networking. By default it will use the shared URLSession - returns: Auth0 Management API v2 */ diff --git a/Auth0/Auth0WebAuth.swift b/Auth0/Auth0WebAuth.swift index 9c83aee4..365d3f76 100644 --- a/Auth0/Auth0WebAuth.swift +++ b/Auth0/Auth0WebAuth.swift @@ -1,30 +1,32 @@ #if WEB_AUTH_PLATFORM -import AuthenticationServices +import Foundation final class Auth0WebAuth: WebAuth { let clientId: String let url: URL + let session: URLSession let storage: TransactionStore + var telemetry: Telemetry var logger: Logger? - var ephemeralSession = false #if os(macOS) private let platform = "macos" #else private let platform = "ios" #endif - private let responseType = "code" private let requiredScope = "openid" + private(set) var parameters: [String: String] = [:] + private(set) var ephemeralSession = false private(set) var issuer: String private(set) var leeway: Int = 60 * 1000 // Default leeway is 60 seconds + private(set) var nonce: String? + private(set) var maxAge: Int? private(set) var organization: String? private(set) var invitationURL: URL? - private var nonce: String? - private var maxAge: Int? lazy var redirectURL: URL? = { guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return nil } @@ -38,10 +40,12 @@ final class Auth0WebAuth: WebAuth { init(clientId: String, url: URL, + session: URLSession = URLSession.shared, storage: TransactionStore = TransactionStore.shared, telemetry: Telemetry = Telemetry()) { self.clientId = clientId self.url = url + self.session = session self.storage = storage self.telemetry = telemetry self.issuer = "\(url.absoluteString)/" @@ -207,18 +211,6 @@ final class Auth0WebAuth: WebAuth { return components.url! } - private func handler(_ redirectURL: URL) -> OAuth2Grant { - var authentication = Auth0Authentication(clientId: self.clientId, url: self.url, telemetry: self.telemetry) - authentication.logger = self.logger - return PKCE(authentication: authentication, - redirectURL: redirectURL, - issuer: self.issuer, - leeway: self.leeway, - maxAge: self.maxAge, - nonce: self.nonce, - organization: self.organization) - } - func generateDefaultState() -> String? { let data = Data(count: 32) var tempData = data @@ -231,6 +223,21 @@ final class Auth0WebAuth: WebAuth { return tempData.a0_encodeBase64URLSafe() } + private func handler(_ redirectURL: URL) -> OAuth2Grant { + var authentication = Auth0Authentication(clientId: self.clientId, + url: self.url, + session: self.session, + telemetry: self.telemetry) + authentication.logger = self.logger + return PKCE(authentication: authentication, + redirectURL: redirectURL, + issuer: self.issuer, + leeway: self.leeway, + maxAge: self.maxAge, + nonce: self.nonce, + organization: self.organization) + } + } extension Auth0Authentication { diff --git a/Auth0/AuthenticationError.swift b/Auth0/AuthenticationError.swift index 437dcc4d..5660f0f6 100644 --- a/Auth0/AuthenticationError.swift +++ b/Auth0/AuthenticationError.swift @@ -140,7 +140,7 @@ extension AuthenticationError: CustomDebugStringConvertible { extension AuthenticationError { /** - Returns a value from the error's `info` dictionary + Returns a value from the error data - parameter key: key of the value to return diff --git a/Auth0/JWK+RSA.swift b/Auth0/JWK+RSA.swift index 118bd53f..8a60740a 100644 --- a/Auth0/JWK+RSA.swift +++ b/Auth0/JWK+RSA.swift @@ -1,6 +1,5 @@ #if WEB_AUTH_PLATFORM import Foundation -import SimpleKeychain extension JWK { var rsaPublicKey: SecKey? { diff --git a/Auth0/ManagementError.swift b/Auth0/ManagementError.swift index 72ee9534..2ba0ae96 100644 --- a/Auth0/ManagementError.swift +++ b/Auth0/ManagementError.swift @@ -79,7 +79,7 @@ extension ManagementError: CustomDebugStringConvertible { extension ManagementError { /** - Returns a value from the error's `info` dictionary + Returns a value from the error data - parameter key: key of the value to return diff --git a/Auth0/OAuth2Grant.swift b/Auth0/OAuth2Grant.swift index c168ec26..c7496791 100644 --- a/Auth0/OAuth2Grant.swift +++ b/Auth0/OAuth2Grant.swift @@ -1,5 +1,4 @@ import Foundation -import JWTDecode protocol OAuth2Grant { var defaults: [String: String] { get } diff --git a/Auth0/WebAuth.swift b/Auth0/WebAuth.swift index 659faf62..3cbbe089 100644 --- a/Auth0/WebAuth.swift +++ b/Auth0/WebAuth.swift @@ -23,14 +23,15 @@ import Foundation ``` + - parameter session: instance of URLSession used for networking. By default it will use the shared URLSession - 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(bundle: Bundle = Bundle.main) -> WebAuth { +public func webAuth(session: URLSession = .shared, bundle: Bundle = Bundle.main) -> WebAuth { let values = plistValues(bundle: bundle)! - return webAuth(clientId: values.clientId, domain: values.domain) + return webAuth(clientId: values.clientId, domain: values.domain, session: session) } /** @@ -42,11 +43,12 @@ public func webAuth(bundle: Bundle = Bundle.main) -> WebAuth { - parameter clientId: Id of your Auth0 client - parameter domain: name of your Auth0 domain + - parameter session: instance of URLSession used for networking. By default it will use the shared URLSession - returns: Auth0 WebAuth component */ -public func webAuth(clientId: String, domain: String) -> WebAuth { - return Auth0WebAuth(clientId: clientId, url: .a0_url(domain)) +public func webAuth(clientId: String, domain: String, session: URLSession = .shared) -> WebAuth { + return Auth0WebAuth(clientId: clientId, url: .a0_url(domain), session: session) } /// WebAuth Authentication using Auth0 diff --git a/Auth0Tests/AuthenticationSpec.swift b/Auth0Tests/AuthenticationSpec.swift index 2c05939d..4774f303 100644 --- a/Auth0Tests/AuthenticationSpec.swift +++ b/Auth0Tests/AuthenticationSpec.swift @@ -10,6 +10,7 @@ import OHHTTPStubsSwift private let ClientId = "CLIENT_ID" private let Domain = "samples.auth0.com" +private let DomainURL = URL(string: "https://\(Domain)")! private let Phone = "+144444444444" private let ValidPassword = "I.O.U. a password" @@ -26,7 +27,7 @@ private let PasswordlessGrantType = "http://auth0.com/oauth/grant-type/passwordl class AuthenticationSpec: QuickSpec { override func spec() { - let auth: Authentication = Auth0Authentication(clientId: ClientId, url: URL(string: "https://\(Domain)")!) + let auth: Authentication = Auth0Authentication(clientId: ClientId, url: DomainURL) beforeEach { stub(condition: isHost(Domain)) { _ in @@ -38,6 +39,30 @@ class AuthenticationSpec: QuickSpec { HTTPStubs.removeAllStubs() } + describe("init") { + + it("should init with client id & url") { + let authentication = Auth0Authentication(clientId: ClientId, url: DomainURL) + expect(authentication.clientId) == ClientId + expect(authentication.url) == DomainURL + } + + it("should init with client id, url & session") { + let session = URLSession(configuration: URLSession.shared.configuration) + let authentication = Auth0Authentication(clientId: ClientId, url: DomainURL, session: session) + expect(authentication.session).to(be(session)) + } + + it("should init with client id, url & telemetry") { + let telemetryInfo = "info" + var telemetry = Telemetry() + telemetry.info = telemetryInfo + let authentication = Auth0Authentication(clientId: ClientId, url: DomainURL, telemetry: telemetry) + expect(authentication.telemetry.info) == telemetryInfo + } + + } + describe("login MFA OTP") { beforeEach { diff --git a/Auth0Tests/ManagementSpec.swift b/Auth0Tests/ManagementSpec.swift index 539fa34c..896765d5 100644 --- a/Auth0Tests/ManagementSpec.swift +++ b/Auth0Tests/ManagementSpec.swift @@ -15,18 +15,29 @@ class ManagementSpec: QuickSpec { it("should init with token & url") { let management = Management(token: Token, url: DomainURL) - expect(management).toNot(beNil()) + expect(management.token) == Token + expect(management.url) == DomainURL } it("should init with token, url & session") { - let management = Management(token: Token, url: DomainURL, session: URLSession.shared) - expect(management).toNot(beNil()) + let session = URLSession(configuration: URLSession.shared.configuration) + let management = Management(token: Token, url: DomainURL, session: session) + expect(management.session).to(be(session)) + } + + it("should init with token, url & telemetry") { + let telemetryInfo = "info" + var telemetry = Telemetry() + telemetry.info = telemetryInfo + let management = Management(token: Token, url: DomainURL, telemetry: telemetry) + expect(management.telemetry.info) == telemetryInfo } it("should have bearer token in authorization header") { let management = Management(token: Token, url: DomainURL) expect(management.defaultHeaders["Authorization"]) == "Bearer \(Token)" } + } describe("object response handling") { diff --git a/Auth0Tests/WebAuthSpec.swift b/Auth0Tests/WebAuthSpec.swift index 6b6002d9..7e32ce33 100644 --- a/Auth0Tests/WebAuthSpec.swift +++ b/Auth0Tests/WebAuthSpec.swift @@ -66,6 +66,36 @@ class WebAuthSpec: QuickSpec { override func spec() { + describe("init") { + + it("should init with client id & url") { + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL) + expect(webAuth.clientId) == ClientId + expect(webAuth.url) == DomainURL + } + + it("should init with client id, url & session") { + let session = URLSession(configuration: URLSession.shared.configuration) + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL, session: session) + expect(webAuth.session).to(be(session)) + } + + it("should init with client id, url & storage") { + let storage = TransactionStore() + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL, storage: storage) + expect(webAuth.storage).to(be(storage)) + } + + it("should init with client id, url & telemetry") { + let telemetryInfo = "info" + var telemetry = Telemetry() + telemetry.info = telemetryInfo + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL, telemetry: telemetry) + expect(webAuth.telemetry.info) == telemetryInfo + } + + } + describe("authorize URL") { itBehavesLike(ValidAuthorizeURLExample) { diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md index 4ca4328e..56fe01af 100644 --- a/V2_MIGRATION_GUIDE.md +++ b/V2_MIGRATION_GUIDE.md @@ -236,7 +236,7 @@ switch error { switch error { case .revokeFailed: handleError(error.cause) // handle underlying error // ... - default: // handle unkwown errors, e.g. errors added in future versions + default: // handle unknown errors, e.g. errors added in future versions } ``` From 49309e686959e778c586aa141f78006a6e20ebb9 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 30 Nov 2021 18:54:21 -0300 Subject: [PATCH 2/2] Add support for subpaths in Auth0 domain [SDK-2963] (#557) --- Auth0.xcodeproj/project.pbxproj | 2 - Auth0/Auth0.swift | 4 +- Auth0/Auth0Authentication.swift | 40 +++++------ Auth0/Auth0WebAuth.swift | 6 +- Auth0/NSURL+Auth0.swift | 20 ++---- Auth0/Users.swift | 8 +-- Auth0/WebAuth.swift | 2 +- Auth0Tests/Auth0Spec.swift | 84 ++++++++++++++++++----- Auth0Tests/ClaimValidatorsSpec.swift | 2 +- Auth0Tests/IDTokenValidatorBaseSpec.swift | 2 +- Auth0Tests/OAuth2GrantSpec.swift | 4 +- Auth0Tests/WebAuthSpec.swift | 81 ++++++++++++++++++---- OAuth2Mac/OAuth2Mac.entitlements | 12 ---- V2_MIGRATION_GUIDE.md | 75 +++++++++++--------- 14 files changed, 218 insertions(+), 124 deletions(-) delete mode 100644 OAuth2Mac/OAuth2Mac.entitlements diff --git a/Auth0.xcodeproj/project.pbxproj b/Auth0.xcodeproj/project.pbxproj index dc2359b2..6574e6b4 100644 --- a/Auth0.xcodeproj/project.pbxproj +++ b/Auth0.xcodeproj/project.pbxproj @@ -435,7 +435,6 @@ 5B7EE47F20FCA0A200367724 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 5B7EE48220FCA0A200367724 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 5B7EE48420FCA0A200367724 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5B7EE48520FCA0A200367724 /* OAuth2Mac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OAuth2Mac.entitlements; sourceTree = ""; }; 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BioAuthentication.swift; sourceTree = ""; }; 5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BioAuthenticationSpec.swift; path = Auth0Tests/BioAuthenticationSpec.swift; sourceTree = SOURCE_ROOT; }; 5B9A54411E49E3AE004B5454 /* Auth0.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Auth0.plist; sourceTree = SOURCE_ROOT; }; @@ -671,7 +670,6 @@ 5B7EE47D20FCA0A100367724 /* ViewController.swift */, 5B7EE47F20FCA0A200367724 /* Assets.xcassets */, 5B7EE48120FCA0A200367724 /* Main.storyboard */, - 5B7EE48520FCA0A200367724 /* OAuth2Mac.entitlements */, 5B7EE48420FCA0A200367724 /* Info.plist */, 5C41F6E0244FA62200252548 /* Auth0.plist */, ); diff --git a/Auth0/Auth0.swift b/Auth0/Auth0.swift index 186abd8e..76f8cef3 100644 --- a/Auth0/Auth0.swift +++ b/Auth0/Auth0.swift @@ -24,7 +24,7 @@ public let defaultScope = "openid profile email" - returns: Auth0 Authentication API */ public func authentication(clientId: String, domain: String, session: URLSession = .shared) -> Authentication { - return Auth0Authentication(clientId: clientId, url: .a0_url(domain), session: session) + return Auth0Authentication(clientId: clientId, url: .httpsURL(from: domain), session: session) } /** @@ -122,7 +122,7 @@ public func users(token: String, session: URLSession = .shared, bundle: Bundle = - returns: Auth0 Management API v2 */ public func users(token: String, domain: String, session: URLSession = .shared) -> Users { - return Management(token: token, url: .a0_url(domain), session: session) + return Management(token: token, url: .httpsURL(from: domain), session: session) } func plistValues(bundle: Bundle) -> (clientId: String, domain: String)? { diff --git a/Auth0/Auth0Authentication.swift b/Auth0/Auth0Authentication.swift index fa799ffe..558ba13e 100644 --- a/Auth0/Auth0Authentication.swift +++ b/Auth0/Auth0Authentication.swift @@ -25,14 +25,14 @@ struct Auth0Authentication: Authentication { } func login(usernameOrEmail username: String, password: String, realm: String, audience: String?, scope: String) -> Request { - let resourceOwner = URL(string: "/oauth/token", relativeTo: self.url)! + let resourceOwner = URL(string: "oauth/token", relativeTo: self.url)! var payload: [String: Any] = [ "username": username, "password": password, "grant_type": "http://auth0.com/oauth/grant-type/password-realm", "client_id": self.clientId, "realm": realm - ] + ] payload["audience"] = audience payload["scope"] = scope return Request(session: session, @@ -45,7 +45,7 @@ struct Auth0Authentication: Authentication { } func loginDefaultDirectory(withUsername username: String, password: String, audience: String?, scope: String) -> Request { - let resourceOwner = URL(string: "/oauth/token", relativeTo: self.url)! + let resourceOwner = URL(string: "oauth/token", relativeTo: self.url)! var payload: [String: Any] = [ "username": username, "password": password, @@ -64,7 +64,7 @@ struct Auth0Authentication: Authentication { } func login(withOTP otp: String, mfaToken: String) -> Request { - let url = URL(string: "/oauth/token", relativeTo: self.url)! + let url = URL(string: "oauth/token", relativeTo: self.url)! let payload: [String: Any] = [ "otp": otp, "mfa_token": mfaToken, @@ -81,7 +81,7 @@ struct Auth0Authentication: Authentication { } func login(withOOBCode oobCode: String, mfaToken: String, bindingCode: String?) -> Request { - let url = URL(string: "/oauth/token", relativeTo: self.url)! + let url = URL(string: "oauth/token", relativeTo: self.url)! var payload: [String: Any] = [ "oob_code": oobCode, "mfa_token": mfaToken, @@ -103,7 +103,7 @@ struct Auth0Authentication: Authentication { } func login(withRecoveryCode recoveryCode: String, mfaToken: String) -> Request { - let url = URL(string: "/oauth/token", relativeTo: self.url)! + let url = URL(string: "oauth/token", relativeTo: self.url)! let payload: [String: Any] = [ "recovery_code": recoveryCode, "mfa_token": mfaToken, @@ -121,7 +121,7 @@ struct Auth0Authentication: Authentication { } func multifactorChallenge(mfaToken: String, types: [String]?, authenticatorId: String?) -> Request { - let url = URL(string: "/mfa/challenge", relativeTo: self.url)! + let url = URL(string: "mfa/challenge", relativeTo: self.url)! var payload: [String: String] = [ "mfa_token": mfaToken, "client_id": self.clientId @@ -186,7 +186,7 @@ struct Auth0Authentication: Authentication { "password": password, "connection": connection, "client_id": self.clientId - ] + ] payload["username"] = username payload["user_metadata"] = userMetadata if let rootAttributes = rootAttributes { @@ -195,7 +195,7 @@ struct Auth0Authentication: Authentication { } } - let createUser = URL(string: "/dbconnections/signup", relativeTo: self.url)! + let createUser = URL(string: "dbconnections/signup", relativeTo: self.url)! return Request(session: session, url: createUser, method: "POST", @@ -211,7 +211,7 @@ struct Auth0Authentication: Authentication { "connection": connection, "client_id": self.clientId ] - let resetPassword = URL(string: "/dbconnections/change_password", relativeTo: self.url)! + let resetPassword = URL(string: "dbconnections/change_password", relativeTo: self.url)! return Request(session: session, url: resetPassword, method: "POST", @@ -227,12 +227,12 @@ struct Auth0Authentication: Authentication { "connection": connection, "send": type.rawValue, "client_id": self.clientId - ] + ] if case .WebLink = type, !parameters.isEmpty { payload["authParams"] = parameters } - let start = URL(string: "/passwordless/start", relativeTo: self.url)! + let start = URL(string: "passwordless/start", relativeTo: self.url)! return Request(session: session, url: start, method: "POST", @@ -248,8 +248,8 @@ struct Auth0Authentication: Authentication { "connection": connection, "send": type.rawValue, "client_id": self.clientId - ] - let start = URL(string: "/passwordless/start", relativeTo: self.url)! + ] + let start = URL(string: "passwordless/start", relativeTo: self.url)! return Request(session: session, url: start, method: "POST", @@ -260,7 +260,7 @@ struct Auth0Authentication: Authentication { } func userInfo(withAccessToken accessToken: String) -> Request { - let userInfo = URL(string: "/userinfo", relativeTo: self.url)! + let userInfo = URL(string: "userinfo", relativeTo: self.url)! return Request(session: session, url: userInfo, method: "GET", @@ -274,7 +274,7 @@ struct Auth0Authentication: Authentication { let payload: [String: Any] = [ "client_id": self.clientId ] - let token = URL(string: "/oauth/token", relativeTo: self.url)! + let token = URL(string: "oauth/token", relativeTo: self.url)! return Request(session: session, url: token, method: "POST", @@ -300,7 +300,7 @@ struct Auth0Authentication: Authentication { "client_id": self.clientId ] payload["scope"] = scope - let oauthToken = URL(string: "/oauth/token", relativeTo: self.url)! + let oauthToken = URL(string: "oauth/token", relativeTo: self.url)! return Request(session: session, url: oauthToken, method: "POST", @@ -315,7 +315,7 @@ struct Auth0Authentication: Authentication { "token": refreshToken, "client_id": self.clientId ] - let oauthToken = URL(string: "/oauth/revoke", relativeTo: self.url)! + let oauthToken = URL(string: "oauth/revoke", relativeTo: self.url)! return Request(session: session, url: oauthToken, method: "POST", @@ -326,7 +326,7 @@ struct Auth0Authentication: Authentication { } func jwks() -> Request { - let jwks = URL(string: "/.well-known/jwks.json", relativeTo: self.url)! + let jwks = URL(string: ".well-known/jwks.json", relativeTo: self.url)! return Request(session: session, url: jwks, method: "GET", @@ -341,7 +341,7 @@ struct Auth0Authentication: Authentication { private extension Auth0Authentication { func login(username: String, otp: String, realm: String, audience: String?, scope: String) -> Request { - let url = URL(string: "/oauth/token", relativeTo: self.url)! + let url = URL(string: "oauth/token", relativeTo: self.url)! var payload: [String: Any] = [ "username": username, "otp": otp, diff --git a/Auth0/Auth0WebAuth.swift b/Auth0/Auth0WebAuth.swift index 365d3f76..7ef0856e 100644 --- a/Auth0/Auth0WebAuth.swift +++ b/Auth0/Auth0WebAuth.swift @@ -157,8 +157,8 @@ final class Auth0WebAuth: WebAuth { func clearSession(federated: Bool, callback: @escaping (Bool) -> Void) { let endpoint = federated ? - URL(string: "/v2/logout?federated", relativeTo: self.url)! : - URL(string: "/v2/logout", relativeTo: self.url)! + URL(string: "v2/logout?federated", relativeTo: self.url)! : + URL(string: "v2/logout", relativeTo: self.url)! let returnTo = URLQueryItem(name: "returnTo", value: self.redirectURL?.absoluteString) let clientId = URLQueryItem(name: "client_id", value: self.clientId) @@ -181,7 +181,7 @@ final class Auth0WebAuth: WebAuth { state: String?, organization: String?, invitation: String?) -> URL { - let authorize = URL(string: "/authorize", relativeTo: self.url)! + let authorize = URL(string: "authorize", relativeTo: self.url)! var components = URLComponents(url: authorize, resolvingAgainstBaseURL: true)! var items: [URLQueryItem] = [] var entries = defaults diff --git a/Auth0/NSURL+Auth0.swift b/Auth0/NSURL+Auth0.swift index 1602b0e0..fefeba69 100644 --- a/Auth0/NSURL+Auth0.swift +++ b/Auth0/NSURL+Auth0.swift @@ -1,20 +1,12 @@ import Foundation -public extension URL { - /** - Returns an Auth0 domain URL given a domain +extension URL { - - parameter domain: name of your Auth0 account - - - returns: URL of your Auth0 account - */ - static func a0_url(_ domain: String) -> URL { - let urlString: String - if !domain.hasPrefix("https") { - urlString = "https://\(domain)" - } else { - urlString = domain - } + static func httpsURL(from domain: String) -> URL { + let prefix = !domain.hasPrefix("https") ? "https://" : "" + let suffix = !domain.hasSuffix("/") ? "/" : "" + let urlString = "\(prefix)\(domain)\(suffix)" return URL(string: urlString)! } + } diff --git a/Auth0/Users.swift b/Auth0/Users.swift index a768c74e..64b38601 100644 --- a/Auth0/Users.swift +++ b/Auth0/Users.swift @@ -252,7 +252,7 @@ extension Users { extension Management: Users { func get(_ identifier: String, fields: [String], include: Bool) -> Request { - let userPath = "/api/v2/users/\(identifier)" + let userPath = "api/v2/users/\(identifier)" var component = components(baseURL: self.url as URL, path: userPath) let value = fields.joined(separator: ",") if !value.isEmpty { @@ -266,7 +266,7 @@ extension Management: Users { } func patch(_ identifier: String, attributes: UserPatchAttributes) -> Request { - let userPath = "/api/v2/users/\(identifier)" + let userPath = "api/v2/users/\(identifier)" let component = components(baseURL: self.url as URL, path: userPath) return Request(session: self.session, url: component.url!, method: "PATCH", handle: self.managementObject, payload: attributes.dictionary, headers: self.defaultHeaders, logger: self.logger, telemetry: self.telemetry) @@ -291,13 +291,13 @@ extension Management: Users { } private func link(_ identifier: String, payload: [String: Any]) -> Request<[ManagementObject], ManagementError> { - let identitiesPath = "/api/v2/users/\(identifier)/identities" + let identitiesPath = "api/v2/users/\(identifier)/identities" let url = components(baseURL: self.url as URL, path: identitiesPath).url! return Request(session: self.session, url: url, method: "POST", handle: self.managementObjects, payload: payload, headers: self.defaultHeaders, logger: self.logger, telemetry: self.telemetry) } func unlink(identityId: String, provider: String, fromUserId identifier: String) -> Request<[ManagementObject], ManagementError> { - let identityPath = "/api/v2/users/\(identifier)/identities/\(provider)/\(identityId)" + let identityPath = "api/v2/users/\(identifier)/identities/\(provider)/\(identityId)" let url = components(baseURL: self.url as URL, path: identityPath).url! return Request(session: self.session, url: url, method: "DELETE", handle: self.managementObjects, headers: self.defaultHeaders, logger: self.logger, telemetry: self.telemetry) } diff --git a/Auth0/WebAuth.swift b/Auth0/WebAuth.swift index 3cbbe089..d654b561 100644 --- a/Auth0/WebAuth.swift +++ b/Auth0/WebAuth.swift @@ -48,7 +48,7 @@ public func webAuth(session: URLSession = .shared, bundle: Bundle = Bundle.main) - returns: Auth0 WebAuth component */ public func webAuth(clientId: String, domain: String, session: URLSession = .shared) -> WebAuth { - return Auth0WebAuth(clientId: clientId, url: .a0_url(domain), session: session) + return Auth0WebAuth(clientId: clientId, url: .httpsURL(from: domain), session: session) } /// WebAuth Authentication using Auth0 diff --git a/Auth0Tests/Auth0Spec.swift b/Auth0Tests/Auth0Spec.swift index e77cdc0d..abc0849e 100644 --- a/Auth0Tests/Auth0Spec.swift +++ b/Auth0Tests/Auth0Spec.swift @@ -46,24 +46,74 @@ class Auth0Spec: QuickSpec { describe("endpoints") { - it("should return authentication endpoint with clientId and domain") { - let auth = Auth0.authentication(clientId: ClientId, domain: Domain) - expect(auth).toNot(beNil()) - expect(auth.clientId) == ClientId - expect(auth.url.absoluteString) == "https://\(Domain)" - } - + context("without trailing slash") { + + it("should return authentication endpoint with clientId and domain") { + let auth = Auth0.authentication(clientId: ClientId, domain: Domain) + expect(auth).toNot(beNil()) + expect(auth.clientId) == ClientId + expect(auth.url.absoluteString) == "https://\(Domain)/" + } + + 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) == "\(domain)/" + } + + it("should return authentication endpoint with domain url and a subpath") { + let domain = "https://mycustomdomain.com/foo" + let auth = Auth0.authentication(clientId: ClientId, domain: domain) + expect(auth.url.absoluteString) == "\(domain)/" + } + + it("should return authentication endpoint with domain url and subpaths") { + let domain = "https://mycustomdomain.com/foo/bar" + let auth = Auth0.authentication(clientId: ClientId, domain: domain) + expect(auth.url.absoluteString) == "\(domain)/" + } + + it("should return users endpoint") { + let users = Auth0.users(token: "token", domain: Domain) + expect(users.token) == "token" + expect(users.url.absoluteString) == "https://\(Domain)/" + } - 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) == domain } - it("should return users endopoint") { - let users = Auth0.users(token: "token", domain: Domain) - expect(users.token) == "token" - expect(users.url.absoluteString) == "https://\(Domain)" + context("with trailing slash") { + + it("should return authentication endpoint with clientId and domain") { + let auth = Auth0.authentication(clientId: ClientId, domain: "\(Domain)/") + expect(auth).toNot(beNil()) + expect(auth.clientId) == ClientId + expect(auth.url.absoluteString) == "https://\(Domain)/" + } + + 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) == domain + } + + it("should return authentication endpoint with domain url with a subpath") { + let domain = "https://mycustomdomain.com/foo/" + let auth = Auth0.authentication(clientId: ClientId, domain: domain) + expect(auth.url.absoluteString) == domain + } + + it("should return authentication endpoint with domain url with subpaths") { + let domain = "https://mycustomdomain.com/foo/bar/" + let auth = Auth0.authentication(clientId: ClientId, domain: domain) + expect(auth.url.absoluteString) == domain + } + + it("should return users endpoint") { + let users = Auth0.users(token: "token", domain: Domain) + expect(users.token) == "token" + expect(users.url.absoluteString) == "https://\(Domain)/" + } + } } @@ -75,13 +125,13 @@ class Auth0Spec: QuickSpec { 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.url.absoluteString) == "https://samples.auth0.com/" expect(auth.clientId) == "CLIENT_ID" } it("should return users endpoint with domain from plist") { let users = Auth0.users(token: "TOKEN", bundle: bundle) - expect(users.url.absoluteString) == "https://samples.auth0.com" + expect(users.url.absoluteString) == "https://samples.auth0.com/" } } diff --git a/Auth0Tests/ClaimValidatorsSpec.swift b/Auth0Tests/ClaimValidatorsSpec.swift index 24c45df8..19fca892 100644 --- a/Auth0Tests/ClaimValidatorsSpec.swift +++ b/Auth0Tests/ClaimValidatorsSpec.swift @@ -66,7 +66,7 @@ class ClaimValidatorsSpec: IDTokenValidatorBaseSpec { describe("iss validation") { var issValidator: IDTokenIssValidator! - let expectedIss = "\(URL.a0_url(domain).absoluteString)/" + let expectedIss = URL.httpsURL(from: domain).absoluteString beforeEach { issValidator = IDTokenIssValidator(issuer: expectedIss) diff --git a/Auth0Tests/IDTokenValidatorBaseSpec.swift b/Auth0Tests/IDTokenValidatorBaseSpec.swift index d08bd75d..29f5b96b 100644 --- a/Auth0Tests/IDTokenValidatorBaseSpec.swift +++ b/Auth0Tests/IDTokenValidatorBaseSpec.swift @@ -13,7 +13,7 @@ class IDTokenValidatorBaseSpec: QuickSpec { // Can't override the initWithInvocation: initializer, because NSInvocation is not available in Swift lazy var authentication = Auth0.authentication(clientId: clientId, domain: domain) - lazy var validatorContext = IDTokenValidatorContext(issuer: "\(URL.a0_url(domain).absoluteString)/", + lazy var validatorContext = IDTokenValidatorContext(issuer: URL.httpsURL(from: domain).absoluteString, audience: clientId, jwksRequest: authentication.jwks(), leeway: leeway, diff --git a/Auth0Tests/OAuth2GrantSpec.swift b/Auth0Tests/OAuth2GrantSpec.swift index 8ed8e0eb..77e810e6 100644 --- a/Auth0Tests/OAuth2GrantSpec.swift +++ b/Auth0Tests/OAuth2GrantSpec.swift @@ -11,7 +11,7 @@ class OAuth2GrantSpec: QuickSpec { override func spec() { - let domain = URL.a0_url("samples.auth0.com") + let domain = URL.httpsURL(from: "samples.auth0.com") let authentication = Auth0Authentication(clientId: "CLIENT_ID", url: domain) let idToken = generateJWT(iss: "\(domain.absoluteString)/", aud: [authentication.clientId]).string let nonce = "a1b2c3d4e5" @@ -82,7 +82,7 @@ class OAuth2GrantSpec: QuickSpec { describe("Authorization Code w/PKCE and idToken") { - let domain = URL.a0_url("samples.auth0.com") + let domain = URL.httpsURL(from: "samples.auth0.com") let method = "S256" let redirectURL = URL(string: "https://samples.auth0.com/callback")! var verifier: String! diff --git a/Auth0Tests/WebAuthSpec.swift b/Auth0Tests/WebAuthSpec.swift index 7e32ce33..4498fcd0 100644 --- a/Auth0Tests/WebAuthSpec.swift +++ b/Auth0Tests/WebAuthSpec.swift @@ -6,7 +6,7 @@ import SafariServices private let ClientId = "ClientId" private let Domain = "samples.auth0.com" -private let DomainURL = URL.a0_url(Domain) +private let DomainURL = URL.httpsURL(from: Domain) private let RedirectURL = URL(string: "https://samples.auth0.com/callback")! private let State = "state" @@ -28,8 +28,8 @@ class WebAuthSharedExamplesConfiguration: QuickConfiguration { it("should use domain \(domain)") { expect(components?.scheme) == "https" - expect(components?.host) == domain - expect(components?.path) == "/authorize" + expect(components?.host) == String(domain.split(separator: "/").first!) + expect(components?.path).to(endWith("/authorize")) } it("should have state parameter") { @@ -104,7 +104,43 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), "domain": Domain, "query": defaultQuery(), - ] + ] + } + + itBehavesLike(ValidAuthorizeURLExample) { + return [ + "url": newWebAuth() + .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), + "domain": "\(Domain)/foo", + "query": defaultQuery() + ] + } + + itBehavesLike(ValidAuthorizeURLExample) { + return [ + "url": newWebAuth() + .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), + "domain": "\(Domain)/foo/", + "query": defaultQuery() + ] + } + + itBehavesLike(ValidAuthorizeURLExample) { + return [ + "url": newWebAuth() + .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), + "domain": "\(Domain)/foo/bar", + "query": defaultQuery() + ] + } + + itBehavesLike(ValidAuthorizeURLExample) { + return [ + "url": newWebAuth() + .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), + "domain": "\(Domain)/foo/bar/", + "query": defaultQuery() + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -114,7 +150,7 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), "domain": Domain, "query": defaultQuery(withParameters: ["connection": "facebook"]), - ] + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -124,7 +160,7 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), "domain": Domain, "query": defaultQuery(withParameters: ["scope": "openid email"]), - ] + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -165,7 +201,7 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), "domain": Domain, "query": defaultQuery(withParameters: ["max_age": "10000"]), - ] + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -174,7 +210,7 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: "abc1234", invitation: nil), "domain": Domain, "query": defaultQuery(withParameters: ["organization": "abc1234"]), - ] + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -183,7 +219,7 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: "abc1234", invitation: "xyz6789"), "domain": Domain, "query": defaultQuery(withParameters: ["organization": "abc1234", "invitation": "xyz6789"]), - ] + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -194,7 +230,7 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: newDefaults, state: State, organization: nil, invitation: nil), "domain": Domain, "query": defaultQuery(withParameters: ["audience": "https://wwww.google.com"]), - ] + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -226,7 +262,7 @@ class WebAuthSpec: QuickSpec { .buildAuthorizeURL(withRedirectURL: RedirectURL, defaults: defaults, state: State, organization: nil, invitation: nil), "domain": Domain, "query": defaultQuery(withParameters: ["connection_scope": "user_friends,email"]), - ] + ] } itBehavesLike(ValidAuthorizeURLExample) { @@ -261,9 +297,28 @@ class WebAuthSpec: QuickSpec { let platform = "macos" #endif - it("should build with custom scheme") { + context("custom scheme") { + let bundleId = Bundle.main.bundleIdentifier! - expect(newWebAuth().redirectURL?.absoluteString) == "\(bundleId)://\(Domain)/\(platform)/\(bundleId)/callback" + + it("should build with the domain") { + expect(newWebAuth().redirectURL?.absoluteString) == "\(bundleId)://\(Domain)/\(platform)/\(bundleId)/callback" + } + + it("should build with the domain and a subpath") { + let subpath = "foo" + let uri = "\(bundleId)://\(Domain)/\(subpath)/\(platform)/\(bundleId)/callback" + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL.appendingPathComponent(subpath)) + expect(webAuth.redirectURL?.absoluteString) == uri + } + + it("should build with the domain and subpaths") { + let subpaths = "foo/bar" + let uri = "\(bundleId)://\(Domain)/\(subpaths)/\(platform)/\(bundleId)/callback" + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL.appendingPathComponent(subpaths)) + expect(webAuth.redirectURL?.absoluteString) == uri + } + } it("should build with a custom url") { diff --git a/OAuth2Mac/OAuth2Mac.entitlements b/OAuth2Mac/OAuth2Mac.entitlements deleted file mode 100644 index 625af03d..00000000 --- a/OAuth2Mac/OAuth2Mac.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - com.apple.security.network.client - - - diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md index 56fe01af..02fcce29 100644 --- a/V2_MIGRATION_GUIDE.md +++ b/V2_MIGRATION_GUIDE.md @@ -2,16 +2,6 @@ Guide to migrating from `1.x` to `2.x` -## Supported platform versions - -The deployment targets for each platform have been raised to: - -- iOS 12.0 -- macOS 10.15 -- Mac Catalyst 13.0 -- tvOS 12.0 -- watchOS 6.2 - ## Supported languages ### Swift @@ -22,6 +12,16 @@ The minimum supported Swift version is now 5.3. Auth0.swift no longer supports Objective-C. +## Supported platform versions + +The deployment targets for each platform have been raised to: + +- iOS 12.0 +- macOS 10.15 +- Mac Catalyst 13.0 +- tvOS 12.0 +- watchOS 6.2 + ## Supported JWT signature algorithms ID Tokens signed with the HS256 algorithm are no longer allowed. @@ -119,58 +119,66 @@ The `useUniversalLink()` method was removed as well, as Universal Links [cannot The method `enableTouchAuth(withTitle:cancelTitle:fallbackTitle:)` was removed. Use `enableBiometrics(withTitle:cancelTitle:fallbackTitle:evaluationPolicy:)` instead. -### `Auth0Error` +### Errors + +#### `Auth0Error` The `init(string: String?, statusCode: Int)` initializer was removed. -### `AuthenticationError` +#### `AuthenticationError` The `init(string: String?, statusCode: Int)` initializer was removed. -### `ManagementError` +#### `ManagementError` The `init(string: String?, statusCode: Int)` initializer was removed. +### Extensions + +#### `URL` + +The `a0_url(_:)` method is no longer public. + ## Errors Removed ### `WebAuthError` -The following cases were removed, as they are no longer necessary: +The following cases were removed, as they are no longer in use: - `.noNonceProvided` - `.invalidIdTokenNonce` ## Types changed -- `UserInfo` was changed from class to struct -- `Credentials` is now a `final` class that conforms to `Codable` instead of `JSONObjectPayload` -- `Auth0Error` was renamed to `Auth0APIError`, and `Auth0Error` is now a different protocol -- `AuthenticationError` was changed from class to struct, and it no longer conforms to `CustomNSError` -- `ManagementError` was changed from class to struct, and it no longer conforms to `CustomNSError` -- `WebAuthError` was changed from enum to struct -- `CredentialsManagerError` was changed from enum to struct +- `UserInfo` was changed from class to struct. +- `Credentials` is now a `final` class that conforms to `Codable` instead of `JSONObjectPayload`. +- `Auth0Error` was renamed to `Auth0APIError`, and `Auth0Error` is now a different protocol. +- `AuthenticationError` was changed from class to struct, and it no longer conforms to `CustomNSError`. +- `ManagementError` was changed from class to struct, and it no longer conforms to `CustomNSError`. +- `WebAuthError` was changed from enum to struct. +- `CredentialsManagerError` was changed from enum to struct. ## Type properties changed ### `AuthenticationError` struct -### Properties removed +#### Properties removed - `info: [String: Any]` is no longer public. Use the new subscript to access its values straight from the error; e.g. `error["code"]`. -## Properties renamed +#### Properties renamed -- `description` was renamed to `localizedDescription`, as `AuthenticationError` now conforms to `CustomStringConvertible` +- `description` was renamed to `localizedDescription`, as `AuthenticationError` now conforms to `CustomStringConvertible`. ### `ManagementError` struct -### Properties removed +#### Properties removed - `info: [String: Any]` is no longer public. Use the new subscript to access its values straight from the error; e.g. `error["code"]`. -## Properties renamed +#### Properties renamed -- `description` was renamed to `localizedDescription`, as `ManagementError` now conforms to `CustomStringConvertible` +- `description` was renamed to `localizedDescription`, as `ManagementError` now conforms to `CustomStringConvertible`. ### `WebAuthError` struct @@ -195,7 +203,7 @@ switch error { } ``` -### Properties removed +#### Properties removed - `.cannotDismissWebAuthController` - `.missingResponseParam` @@ -207,8 +215,8 @@ switch error { #### Properties renamed -- `.unknownError` was renamed to `.unknown` -- `.noBundleIdentifierFound` was renamed to `.noBundleIdentifier` +- `.unknownError` was renamed to `.unknown`. +- `.noBundleIdentifierFound` was renamed to `.noBundleIdentifier`. #### Properties added @@ -242,8 +250,8 @@ switch error { #### Properties renamed -- `.failedRefresh` was renamed to `.refreshFailed` -- `.touchFailed` was renamed to `.biometricsFailed` +- `.failedRefresh` was renamed to `.refreshFailed`. +- `.touchFailed` was renamed to `.biometricsFailed`. #### Properties added @@ -325,13 +333,16 @@ This means you can now provide your own storage layer to `CredentialsManager`. ```swift class CustomStore: CredentialsStorage { var store: [String : Data] = [:] + func getEntry(forKey: String) -> Data? { return store[forKey] } + func setEntry(_ data: Data, forKey: String) -> Bool { store[forKey] = data return true } + func deleteEntry(forKey: String) -> Bool { store[forKey] = nil return true