diff --git a/WooCommerce/Classes/Authentication/WebAuth/AuthenticationService.swift b/WooCommerce/Classes/Authentication/WebAuth/AuthenticationService.swift deleted file mode 100644 index f0713c67988..00000000000 --- a/WooCommerce/Classes/Authentication/WebAuth/AuthenticationService.swift +++ /dev/null @@ -1,222 +0,0 @@ -import AutomatticTracks -import Foundation - -/// Full copy of the same file from WP-iOS -/// Couple of crash logging calls replaced with WC-iOS counterparts -/// Consider moving it to WordPressAuthenticator lib -/// https://github.com/wordpress-mobile/WordPress-iOS/blob/9b1e03b/WordPress/Classes/Services/AuthenticationService.swift - -class AuthenticationService { - - static let wpComLoginEndpoint = "https://wordpress.com/wp-login.php" - - enum RequestAuthCookieError: Error, LocalizedError { - case wpcomCookieNotReturned - - public var errorDescription: String? { - switch self { - case .wpcomCookieNotReturned: - return "Response to request for auth cookie for WP.com site failed to return cookie." - } - } - } - - // MARK: - Self Hosted - - func loadAuthCookiesForSelfHosted( - into cookieJar: CookieJar, - loginURL: URL, - username: String, - password: String, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void) { - - cookieJar.hasWordPressSelfHostedAuthCookie(for: loginURL, username: username) { hasCookie in - guard !hasCookie else { - success() - return - } - - self.getAuthCookiesForSelfHosted(loginURL: loginURL, username: username, password: password, success: { cookies in - cookieJar.setCookies(cookies) { - success() - } - - cookieJar.hasWordPressSelfHostedAuthCookie(for: loginURL, username: username) { hasCookie in - print("Has cookie: \(hasCookie)") - } - }) { error in - // Make sure this error scenario isn't silently ignored. - ServiceLocator.crashLogging.logError(error) - - // Even if getting the auth cookies fail, we'll still try to load the URL - // so that the user sees a reasonable error situation on screen. - // We could opt to create a special screen but for now I'd rather users report - // the issue when it happens. - failure(error) - } - } - } - - func getAuthCookiesForSelfHosted( - loginURL: URL, - username: String, - password: String, - success: @escaping (_ cookies: [HTTPCookie]) -> Void, - failure: @escaping (Error) -> Void) { - - let headers = [String: String]() - let parameters = [ - "log": username, - "pwd": password, - "rememberme": "true" - ] - - requestAuthCookies( - from: loginURL, - headers: headers, - parameters: parameters, - success: success, - failure: failure) - } - - // MARK: - WP.com - - func loadAuthCookiesForWPCom( - into cookieJar: CookieJar, - username: String, - authToken: String, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void) { - - cookieJar.hasWordPressComAuthCookie( - username: username, - atomicSite: false) { hasCookie in - - guard !hasCookie else { - // The stored cookie can be stale but we'll try to use it and refresh it if the request fails. - success() - return - } - - self.getAuthCookiesForWPCom(username: username, authToken: authToken, success: { cookies in - cookieJar.setCookies(cookies) { - - cookieJar.hasWordPressComAuthCookie(username: username, atomicSite: false) { hasCookie in - guard hasCookie else { - failure(RequestAuthCookieError.wpcomCookieNotReturned) - return - } - success() - } - - } - }) { error in - // Make sure this error scenario isn't silently ignored. - ServiceLocator.crashLogging.logError(error) - - // Even if getting the auth cookies fail, we'll still try to load the URL - // so that the user sees a reasonable error situation on screen. - // We could opt to create a special screen but for now I'd rather users report - // the issue when it happens. - failure(error) - } - } - } - - func getAuthCookiesForWPCom( - username: String, - authToken: String, - success: @escaping (_ cookies: [HTTPCookie]) -> Void, - failure: @escaping (Error) -> Void) { - - let loginURL = URL(string: AuthenticationService.wpComLoginEndpoint)! - let headers = [ - "Authorization": "Bearer \(authToken)" - ] - let parameters = [ - "log": username, - "rememberme": "true" - ] - - requestAuthCookies( - from: loginURL, - headers: headers, - parameters: parameters, - success: success, - failure: failure) - } - - // MARK: - Request Construction - - private func requestAuthCookies( - from url: URL, - headers: [String: String], - parameters: [String: String], - success: @escaping (_ cookies: [HTTPCookie]) -> Void, - failure: @escaping (Error) -> Void) { - - // We don't want these cookies persisted in other sessions - let session = URLSession(configuration: .ephemeral) - var request = URLRequest(url: url) - - request.httpMethod = "POST" - request.httpBody = body(withParameters: parameters) - - headers.forEach { (key, value) in - request.setValue(value, forHTTPHeaderField: key) - } -// request.setValue(WPUserAgent.wordPress(), forHTTPHeaderField: "User-Agent") - - let task = session.dataTask(with: request) { data, response, error in - if let error = error { - DispatchQueue.main.async { - failure(error) - } - return - } - - // The following code is a bit complicated to read, apologies. - // We're retrieving all cookies from the "Set-Cookie" header manually, and combining - // those cookies with the ones from the current session. The reason behind this is that - // iOS's URLSession processes the cookies from such header before this callback is executed, - // whereas OHTTPStubs.framework doesn't (the cookies are left in the header fields of - // the response). The only way to combine both is to just add them together here manually. - // - // To know if you can remove this, you'll have to test this code live and in our unit tests - // and compare the session cookies. - let responseCookies = self.cookies(from: response, loginURL: url) - let cookies = (session.configuration.httpCookieStorage?.cookies ?? [HTTPCookie]()) + responseCookies - DispatchQueue.main.async { - success(cookies) - } - } - - task.resume() - } - - private func body(withParameters parameters: [String: String]) -> Data? { - var queryItems = [URLQueryItem]() - - for parameter in parameters { - let queryItem = URLQueryItem(name: parameter.key, value: parameter.value) - queryItems.append(queryItem) - } - - var components = URLComponents() - components.queryItems = queryItems - - return components.percentEncodedQuery?.data(using: .utf8) - } - - // MARK: - Response Parsing - - private func cookies(from response: URLResponse?, loginURL: URL) -> [HTTPCookie] { - guard let httpResponse = response as? HTTPURLResponse, - let headers = httpResponse.allHeaderFields as? [String: String] else { - return [] - } - - return HTTPCookie.cookies(withResponseHeaderFields: headers, for: loginURL) - } -} diff --git a/WooCommerce/Classes/Authentication/WebAuth/CookieJar.swift b/WooCommerce/Classes/Authentication/WebAuth/CookieJar.swift deleted file mode 100644 index cc795cd5c35..00000000000 --- a/WooCommerce/Classes/Authentication/WebAuth/CookieJar.swift +++ /dev/null @@ -1,230 +0,0 @@ -import Foundation -import WebKit - -/// Full copy of the same file from WP-iOS -/// Consider moving it to WordPressAuthenticator lib -/// https://github.com/wordpress-mobile/WordPress-iOS/blob/9b1e03b/WordPress/Classes/Utility/CookieJar.swift - -/// Provides a common interface to look for a logged-in WordPress cookie in different -/// cookie storage systems. -/// -@objc protocol CookieJar { - func getCookies(url: URL, completion: @escaping ([HTTPCookie]) -> Void) - func getCookies(completion: @escaping ([HTTPCookie]) -> Void) - func hasWordPressSelfHostedAuthCookie(for url: URL, username: String, completion: @escaping (Bool) -> Void) - func hasWordPressComAuthCookie(username: String, atomicSite: Bool, completion: @escaping (Bool) -> Void) - func removeCookies(_ cookies: [HTTPCookie], completion: @escaping () -> Void) - func removeWordPressComCookies(completion: @escaping () -> Void) - func setCookies(_ cookies: [HTTPCookie], completion: @escaping () -> Void) -} - -// As long as CookieJar is @objc, we can't have shared methods in protocol -// extensions, as it needs to be accessible to Obj-C. -// Whenever we migrate enough code so this doesn't need to be called from Swift, -// a regular CookieJar protocol with shared implementation on an extension would suffice. -// -// Also, although you're not supposed to use this outside this file, it can't be private -// since we're subclassing HTTPCookieStorage (which conforms to this) in MockCookieJar in -// the test target, and the swift compiler will crash when doing that ¯\_(ツ)_/¯ -// -// https://bugs.swift.org/browse/SR-2370 -// -protocol CookieJarSharedImplementation: CookieJar { -} - -extension CookieJarSharedImplementation { - func _hasWordPressComAuthCookie(username: String, atomicSite: Bool, completion: @escaping (Bool) -> Void) { - let url = URL(string: "https://wordpress.com/")! - - return _hasWordPressAuthCookie(for: url, username: username, atomicSite: atomicSite, completion: completion) - } - - func _hasWordPressAuthCookie(for url: URL, username: String, atomicSite: Bool, completion: @escaping (Bool) -> Void) { - getCookies(url: url) { (cookies) in - let cookie = cookies - .contains(where: { cookie in - return cookie.isWordPressLoggedIn(username: username, atomic: atomicSite) - }) - - completion(cookie) - } - } - - func _removeWordPressComCookies(completion: @escaping () -> Void) { - getCookies { [unowned self] (cookies) in - self.removeCookies(cookies.filter({ $0.domain.hasSuffix(".wordpress.com") }), completion: completion) - } - } -} - -extension HTTPCookieStorage: CookieJarSharedImplementation { - func getCookies(url: URL, completion: @escaping ([HTTPCookie]) -> Void) { - completion(cookies(for: url) ?? []) - } - - func getCookies(completion: @escaping ([HTTPCookie]) -> Void) { - completion(cookies ?? []) - } - - func hasWordPressComAuthCookie(username: String, atomicSite: Bool, completion: @escaping (Bool) -> Void) { - _hasWordPressComAuthCookie(username: username, atomicSite: atomicSite, completion: completion) - } - - func hasWordPressSelfHostedAuthCookie(for url: URL, username: String, completion: @escaping (Bool) -> Void) { - _hasWordPressAuthCookie(for: url, username: username, atomicSite: false, completion: completion) - } - - func removeCookies(_ cookies: [HTTPCookie], completion: @escaping () -> Void) { - cookies.forEach(deleteCookie(_:)) - completion() - } - - func removeWordPressComCookies(completion: @escaping () -> Void) { - _removeWordPressComCookies(completion: completion) - } - - func setCookies(_ cookies: [HTTPCookie], completion: @escaping () -> Void) { - for cookie in cookies { - setCookie(cookie) - } - - completion() - } -} - -extension WKHTTPCookieStore: CookieJarSharedImplementation { - func getCookies(url: URL, completion: @escaping ([HTTPCookie]) -> Void) { - - // This fixes an issue with `getAllCookies` not calling its completion block (related: https://stackoverflow.com/q/55565188) - // - adds timeout so the above failure will eventually return - // - waits for the cookies on a background thread so that: - // 1. we are not blocking the main thread for UI reasons - // 2. cookies seem to never load when main thread is blocked (perhaps they dispatch to the main thread later on) - - DispatchQueue.global(qos: .userInitiated).async { - let group = DispatchGroup() - group.enter() - - var urlCookies: [HTTPCookie] = [] - - DispatchQueue.main.async { - self.getAllCookies { (cookies) in - urlCookies = cookies.filter({ (cookie) in - return cookie.matches(url: url) - }) - group.leave() - } - } - - let result = group.wait(timeout: .now() + .seconds(2)) - if result == .timedOut { - DDLogWarn("Time out waiting for WKHTTPCookieStore to get cookies") - } - - DispatchQueue.main.async { - completion(urlCookies) - } - } - } - - func getCookies(completion: @escaping ([HTTPCookie]) -> Void) { - getAllCookies(completion) - } - - func hasWordPressComAuthCookie(username: String, atomicSite: Bool, completion: @escaping (Bool) -> Void) { - _hasWordPressComAuthCookie(username: username, atomicSite: atomicSite, completion: completion) - } - - func hasWordPressSelfHostedAuthCookie(for url: URL, username: String, completion: @escaping (Bool) -> Void) { - _hasWordPressAuthCookie(for: url, username: username, atomicSite: false, completion: completion) - } - - func removeCookies(_ cookies: [HTTPCookie], completion: @escaping () -> Void) { - let group = DispatchGroup() - cookies - .forEach({ [unowned self] (cookie) in - group.enter() - self.delete(cookie, completionHandler: { - group.leave() - }) - }) - let result = group.wait(timeout: .now() + .seconds(2)) - if result == .timedOut { - DDLogWarn("Time out waiting for WKHTTPCookieStore to remove cookies") - } - completion() - } - - func removeWordPressComCookies(completion: @escaping () -> Void) { - _removeWordPressComCookies(completion: completion) - } - - func setCookies(_ cookies: [HTTPCookie], completion: @escaping () -> Void) { - guard let cookie = cookies.last else { - return completion() - } - - DispatchQueue.main.async { - self.setCookie(cookie) { [weak self] in - self?.setCookies(cookies.dropLast(), completion: completion) - } - } - } -} - -#if DEBUG - func __removeAllWordPressComCookies() { - var jars = [CookieJarSharedImplementation]() - jars.append(HTTPCookieStorage.shared) - jars.append(WKWebsiteDataStore.default().httpCookieStore) - - let group = DispatchGroup() - jars.forEach({ jar in - group.enter() - jar.removeWordPressComCookies { - group.leave() - } - }) - _ = group.wait(timeout: .now() + .seconds(5)) - } -#endif - -private let atomicLoggedInCookieNamePrefix = "wordpress_logged_in_" -private let loggedInCookieName = "wordpress_logged_in" - -private extension HTTPCookie { - func isWordPressLoggedIn(username: String, atomic: Bool) -> Bool { - guard !atomic else { - return isWordPressLoggedInAtomic(username: username) - } - - return isWordPressLoggedIn(username: username) - } - - private func isWordPressLoggedIn(username: String) -> Bool { - return name.hasPrefix(loggedInCookieName) - && value.components(separatedBy: "%").first == username - } - - private func isWordPressLoggedInAtomic(username: String) -> Bool { - return name.hasPrefix(atomicLoggedInCookieNamePrefix) - && value.components(separatedBy: "|").first == username - } - - func matches(url: URL) -> Bool { - guard let host = url.host else { - return false - } - - let matchesDomain: Bool - if domain.hasPrefix(".") { - matchesDomain = host.hasSuffix(domain) - || host == domain.dropFirst() - } else { - matchesDomain = host == domain - } - return matchesDomain - && url.path.hasPrefix(path) - && (!isSecure || (url.scheme == "https")) - } -} diff --git a/WooCommerce/Classes/Authentication/WebAuth/RequestAuthenticator.swift b/WooCommerce/Classes/Authentication/WebAuth/RequestAuthenticator.swift deleted file mode 100644 index f4332bb25c4..00000000000 --- a/WooCommerce/Classes/Authentication/WebAuth/RequestAuthenticator.swift +++ /dev/null @@ -1,269 +0,0 @@ -import AutomatticTracks -import struct Networking.Site - -/// Partial copy of the same file from WP-iOS -/// Trimmed down and adapted for WC -/// https://github.com/wordpress-mobile/WordPress-iOS/blob/9b1e03b/WordPress/Classes/Utility/Networking/RequestAuthenticator.swift - -/// Authenticator for requests to self-hosted sites, wp.com sites, including private -/// sites and atomic sites. -/// -class RequestAuthenticator: NSObject { - - enum DotComAuthenticationType { - case regular - case regularMapped(siteID: Int) - case atomic(loginURL: String) - case privateAtomic(blogID: Int) - } - - enum WPNavigationActionType { - case reload - case allow - } - - enum Credentials { - case dotCom(username: String, authToken: String, authenticationType: DotComAuthenticationType) - case siteLogin(loginURL: URL, username: String, password: String) - } - - fileprivate let credentials: Credentials - - // MARK: - Services - - private let authenticationService: AuthenticationService - - // MARK: - Initializers - - init(credentials: Credentials, authenticationService: AuthenticationService = AuthenticationService()) { - self.credentials = credentials - self.authenticationService = authenticationService - } - - convenience init?(site: Site, username: String, token: String) { - var authenticationType: DotComAuthenticationType = .regular - - if site.isWordPressComStore { - authenticationType = .atomic(loginURL: site.loginURL) - // TODO: consider private atomic case - } - - self.init(credentials: .dotCom(username: username, authToken: token, authenticationType: authenticationType)) - } - - /// Potentially rewrites a request for authentication. - /// - /// This method will call the completion block with the request to be used. - /// - /// - Warning: On WordPress.com, this uses a special redirect system. It - /// requires the web view to call `interceptRedirect(request:)` before - /// loading any request. - /// - /// - Parameters: - /// - url: the URL to be loaded. - /// - cookieJar: a CookieJar object where the authenticator will look - /// for existing cookies. - /// - completion: this will be called with either the request for - /// authentication, or a request for the original URL. - /// - @objc func request(url: URL, cookieJar: CookieJar, completion: @escaping (URLRequest) -> Void) { - switch self.credentials { - case .dotCom(let username, let authToken, let authenticationType): - requestForWPCom( - url: url, - cookieJar: cookieJar, - username: username, - authToken: authToken, - authenticationType: authenticationType, - completion: completion) - case .siteLogin(let loginURL, let username, let password): - requestForSelfHosted( - url: url, - loginURL: loginURL, - cookieJar: cookieJar, - username: username, - password: password, - completion: completion) - } - } - - private func requestForWPCom(url: URL, - cookieJar: CookieJar, - username: String, - authToken: String, - authenticationType: DotComAuthenticationType, - completion: @escaping (URLRequest) -> Void) { - - switch authenticationType { - case .regular: - requestForWPCom( - url: url, - cookieJar: cookieJar, - username: username, - authToken: authToken, - completion: completion) - case .atomic(let loginURL): - requestForAtomicWPCom( - url: url, - loginURL: loginURL, - cookieJar: cookieJar, - username: username, - authToken: authToken, - completion: completion) - case .regularMapped, .privateAtomic: - // not supported - return - } - } - - private func requestForSelfHosted(url: URL, - loginURL: URL, - cookieJar: CookieJar, - username: String, - password: String, - completion: @escaping (URLRequest) -> Void) { - - func done() { - let request = URLRequest(url: url) - completion(request) - } - - authenticationService.loadAuthCookiesForSelfHosted(into: cookieJar, loginURL: loginURL, username: username, password: password, success: { - done() - }) { [weak self] error in - // Make sure this error scenario isn't silently ignored. - self?.logErrorIfNeeded(error) - - // Even if getting the auth cookies fail, we'll still try to load the URL - // so that the user sees a reasonable error situation on screen. - // We could opt to create a special screen but for now I'd rather users report - // the issue when it happens. - done() - } - } - - private func requestForAtomicWPCom(url: URL, - loginURL: String, - cookieJar: CookieJar, - username: String, - authToken: String, - completion: @escaping (URLRequest) -> Void) { - - func done() { - // For non-private Atomic sites, proxy the request through wp-login like Calypso does. - // If the site has SSO enabled auth should happen and we get redirected to our preview. - // If SSO is not enabled wp-admin prompts for credentials, then redirected. - var components = URLComponents(string: loginURL) - var queryItems = components?.queryItems ?? [] - queryItems.append(URLQueryItem(name: "redirect_to", value: url.absoluteString)) - components?.queryItems = queryItems - let requestURL = components?.url ?? url - - let request = URLRequest(url: requestURL) - completion(request) - } - - authenticationService.loadAuthCookiesForWPCom(into: cookieJar, username: username, authToken: authToken, success: { - done() - }) { [weak self] error in - // Make sure this error scenario isn't silently ignored. - self?.logErrorIfNeeded(error) - - // Even if getting the auth cookies fail, we'll still try to load the URL - // so that the user sees a reasonable error situation on screen. - // We could opt to create a special screen but for now I'd rather users report - // the issue when it happens. - done() - } - } - - private func requestForWPCom(url: URL, cookieJar: CookieJar, username: String, authToken: String, completion: @escaping (URLRequest) -> Void) { - - func done() { - let request = URLRequest(url: url) - completion(request) - } - - authenticationService.loadAuthCookiesForWPCom(into: cookieJar, username: username, authToken: authToken, success: { - done() - }) { [weak self] error in - // Make sure this error scenario isn't silently ignored. - self?.logErrorIfNeeded(error) - - // Even if getting the auth cookies fail, we'll still try to load the URL - // so that the user sees a reasonable error situation on screen. - // We could opt to create a special screen but for now I'd rather users report - // the issue when it happens. - done() - } - } - - private func logErrorIfNeeded(_ error: Swift.Error) { - let nsError = error as NSError - - switch nsError.code { - case NSURLErrorTimedOut, NSURLErrorNotConnectedToInternet: - return - default: - ServiceLocator.crashLogging.logError(error) - } - } -} - -private extension RequestAuthenticator { - static let wordPressComLoginUrl = URL(string: "https://wordpress.com/wp-login.php")! -} - -extension RequestAuthenticator { - func isLogin(url: URL) -> Bool { - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - components?.queryItems = nil - - return components?.url == RequestAuthenticator.wordPressComLoginUrl - } -} - -// MARK: Navigation Validator -extension RequestAuthenticator { - /// Validates that the navigation worked as expected then provides a recommendation on if the screen should reload or not. - func decideActionFor(response: URLResponse, cookieJar: CookieJar, completion: @escaping (WPNavigationActionType) -> Void) { - switch self.credentials { - case .dotCom(let username, _, let authenticationType): - decideActionForWPCom(response: response, cookieJar: cookieJar, username: username, authenticationType: authenticationType, completion: completion) - case .siteLogin: - completion(.allow) - } - } - - private func decideActionForWPCom(response: URLResponse, - cookieJar: CookieJar, - username: String, - authenticationType: DotComAuthenticationType, - completion: @escaping (WPNavigationActionType) -> Void) { - - guard didEncouterRecoverableChallenge(response) else { - completion(.allow) - return - } - - cookieJar.removeWordPressComCookies { - completion(.reload) - } - } - - private func didEncouterRecoverableChallenge(_ response: URLResponse) -> Bool { - guard let url = response.url?.absoluteString else { - return false - } - - if url.contains("r-login.wordpress.com") || url.contains("wordpress.com/log-in?") { - return true - } - - guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { - return false - } - - return 400 <= statusCode && statusCode < 500 - } -} diff --git a/WooCommerce/Classes/Authentication/WebAuth/WebKitViewController.swift b/WooCommerce/Classes/Authentication/WebAuth/WebKitViewController.swift index f09aac46b17..0b1b82a78cc 100644 --- a/WooCommerce/Classes/Authentication/WebAuth/WebKitViewController.swift +++ b/WooCommerce/Classes/Authentication/WebAuth/WebKitViewController.swift @@ -7,27 +7,7 @@ import WordPressShared /// Partial copy of the same file from WP-iOS /// https://github.com/wordpress-mobile/WordPress-iOS/blob/c027ccf05ba839d658f8496e62b7bfdae6608a10/WordPress/Classes/Utility/WebKitViewController.swift -protocol WebKitAuthenticatable { - var authenticator: RequestAuthenticator? { get } - func authenticatedRequest(for url: URL, on webView: WKWebView, completion: @escaping (URLRequest) -> Void) -} - -extension WebKitAuthenticatable { - func authenticatedRequest(for url: URL, on webView: WKWebView, completion: @escaping (URLRequest) -> Void) { - guard let authenticator = authenticator else { - return completion(URLRequest(url: url)) - } - - DispatchQueue.main.async { - let cookieStore = webView.configuration.websiteDataStore.httpCookieStore - authenticator.request(url: url, cookieJar: cookieStore) { (request) in - completion(request) - } - } - } -} - -class WebKitViewController: UIViewController, WebKitAuthenticatable { +class WebKitViewController: UIViewController { @objc let webView: WKWebView @objc let progressView = WebProgressView() @objc let titleView = NavigationTitleView() @@ -77,7 +57,6 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { }() @objc let url: URL? - @objc let authenticator: RequestAuthenticator? @objc var secureInteraction = false @objc var customTitle: String? private let opensNewInSafari: Bool @@ -92,9 +71,6 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { static let frameLoadInterrupted = 102 } - /// Precautionary variable that's in place to make sure the web view doesn't run into an endless loop of reloads if it encounters an error. - private var hasAttemptedAuthRecovery = false - @objc init(configuration: WebViewControllerConfiguration) { let config = WKWebViewConfiguration() // The default on iPad is true. We want the iPhone to be true as well. @@ -104,7 +80,6 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { url = configuration.url secureInteraction = configuration.secureInteraction customTitle = configuration.customTitle - authenticator = configuration.authenticator linkBehavior = configuration.linkBehavior opensNewInSafari = configuration.opensNewInSafari onClose = configuration.onClose @@ -119,7 +94,6 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { self.url = url secureInteraction = parent.secureInteraction customTitle = parent.customTitle - authenticator = parent.authenticator linkBehavior = parent.linkBehavior opensNewInSafari = parent.opensNewInSafari super.init(nibName: nil, bundle: nil) @@ -198,9 +172,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { return } - authenticatedRequest(for: url, on: webView) { [weak self] (request) in - self?.webView.load(request) - } + webView.load(URLRequest(url: url)) } // MARK: Navigation bar setup @@ -427,13 +399,6 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { extension WebKitViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - - // Allow request if it is to `wp-login` for 2fa - if let url = navigationAction.request.url, authenticator?.isLogin(url: url) == true { - decisionHandler(.allow) - return - } - // Check for link protocols such as `tel:` and set the correct behavior if let url = navigationAction.request.url, let scheme = url.scheme { let linkProtocols = ["tel", "sms", "mailto"] @@ -457,24 +422,7 @@ extension WebKitViewController: WKNavigationDelegate { ServiceLocator.analytics.track(event: .ProductDetail.previewFailed(statusCode: response.statusCode)) } - guard navigationResponse.isForMainFrame, let authenticator = authenticator, !hasAttemptedAuthRecovery else { - decisionHandler(.allow) - return - } - - let cookieStore = webView.configuration.websiteDataStore.httpCookieStore - authenticator.decideActionFor(response: navigationResponse.response, cookieJar: cookieStore) { [unowned self] action in - switch action { - case .reload: - decisionHandler(.cancel) - - /// We've cleared the stored cookies so let's try again. - self.hasAttemptedAuthRecovery = true - self.loadWebViewRequest() - case .allow: - decisionHandler(.allow) - } - } + decisionHandler(.allow) } } diff --git a/WooCommerce/Classes/Authentication/WebAuth/WebViewControllerConfiguration.swift b/WooCommerce/Classes/Authentication/WebAuth/WebViewControllerConfiguration.swift index 15561507490..b23ce63eb7f 100644 --- a/WooCommerce/Classes/Authentication/WebAuth/WebViewControllerConfiguration.swift +++ b/WooCommerce/Classes/Authentication/WebAuth/WebViewControllerConfiguration.swift @@ -12,15 +12,10 @@ class WebViewControllerConfiguration: NSObject { /// The behavior to use for allowing links to be loaded by the web view based var linkBehavior = LinkBehavior.all @objc var customTitle: String? - @objc var authenticator: RequestAuthenticator? var onClose: (() -> Void)? @objc init(url: URL?) { self.url = url super.init() } - - func authenticate(site: Site, username: String, token: String) { - self.authenticator = RequestAuthenticator(site: site, username: username, token: token) - } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 933598a1846..5a2de2cc3d4 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1179,13 +1179,7 @@ AEA622B427466B78002A9B57 /* BottomSheetOrderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA622B327466B78002A9B57 /* BottomSheetOrderType.swift */; }; AEA622B727468790002A9B57 /* AddOrderCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA622B627468790002A9B57 /* AddOrderCoordinatorTests.swift */; }; AEACCB6D2785FF4A000D01F0 /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACCB6C2785FF4A000D01F0 /* NavigationRow.swift */; }; - AEB4DB97290AE74B00AE4340 /* CookieJar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DB96290AE74B00AE4340 /* CookieJar.swift */; }; AEB4DB99290AE8F300AE4340 /* MockCookieJar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DB98290AE8F300AE4340 /* MockCookieJar.swift */; }; - AEB4DB9B290AE92800AE4340 /* CookieJarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DB9A290AE92800AE4340 /* CookieJarTests.swift */; }; - AEB4DB9D290AE94600AE4340 /* WKCookieJarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DB9C290AE94600AE4340 /* WKCookieJarTests.swift */; }; - AEB4DB9F290AEA6100AE4340 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DB9E290AEA6100AE4340 /* AuthenticationService.swift */; }; - AEB4DBA1290AEC0D00AE4340 /* RequestAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DBA0290AEC0D00AE4340 /* RequestAuthenticator.swift */; }; - AEB4DBA3290AEDB900AE4340 /* RequestAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DBA2290AEDB900AE4340 /* RequestAuthenticatorTests.swift */; }; AEB73C0C25CD734200A8454A /* AttributePickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB73C0B25CD734200A8454A /* AttributePickerViewModel.swift */; }; AEB73C1725CD8E5800A8454A /* AttributePickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB73C1625CD8E5800A8454A /* AttributePickerViewModelTests.swift */; }; AEBFD13F28E7655F00F598C6 /* StoreInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEBFD13E28E7655F00F598C6 /* StoreInfoView.swift */; }; @@ -3122,13 +3116,7 @@ AEA622B327466B78002A9B57 /* BottomSheetOrderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetOrderType.swift; sourceTree = ""; }; AEA622B627468790002A9B57 /* AddOrderCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOrderCoordinatorTests.swift; sourceTree = ""; }; AEACCB6C2785FF4A000D01F0 /* NavigationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRow.swift; sourceTree = ""; }; - AEB4DB96290AE74B00AE4340 /* CookieJar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieJar.swift; sourceTree = ""; }; AEB4DB98290AE8F300AE4340 /* MockCookieJar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCookieJar.swift; sourceTree = ""; }; - AEB4DB9A290AE92800AE4340 /* CookieJarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieJarTests.swift; sourceTree = ""; }; - AEB4DB9C290AE94600AE4340 /* WKCookieJarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKCookieJarTests.swift; sourceTree = ""; }; - AEB4DB9E290AEA6100AE4340 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; - AEB4DBA0290AEC0D00AE4340 /* RequestAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAuthenticator.swift; sourceTree = ""; }; - AEB4DBA2290AEDB900AE4340 /* RequestAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAuthenticatorTests.swift; sourceTree = ""; }; AEB73C0B25CD734200A8454A /* AttributePickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributePickerViewModel.swift; sourceTree = ""; }; AEB73C1625CD8E5800A8454A /* AttributePickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributePickerViewModelTests.swift; sourceTree = ""; }; AEBFD13E28E7655F00F598C6 /* StoreInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInfoView.swift; sourceTree = ""; }; @@ -6644,9 +6632,6 @@ AEB4DB95290AE72D00AE4340 /* WebAuth */ = { isa = PBXGroup; children = ( - AEB4DB96290AE74B00AE4340 /* CookieJar.swift */, - AEB4DB9E290AEA6100AE4340 /* AuthenticationService.swift */, - AEB4DBA0290AEC0D00AE4340 /* RequestAuthenticator.swift */, AE3AA88A290C30B900BE422D /* WebViewControllerConfiguration.swift */, AE3AA888290C303B00BE422D /* WebKitViewController.swift */, AE3AA88C290C30E800BE422D /* WebProgressView.swift */, @@ -6765,9 +6750,6 @@ 4590B651261C8D1E00A6FCE0 /* WeightFormatterTests.swift */, B96B536A2816ECFC00F753E6 /* CardPresentPluginsDataProviderTests.swift */, EEADF625281A65A9001B40F1 /* DefaultShippingValueLocalizerTests.swift */, - AEB4DB9A290AE92800AE4340 /* CookieJarTests.swift */, - AEB4DB9C290AE94600AE4340 /* WKCookieJarTests.swift */, - AEB4DBA2290AEDB900AE4340 /* RequestAuthenticatorTests.swift */, ); path = Tools; sourceTree = ""; @@ -9980,7 +9962,6 @@ D8D15F83230A17A000D48B3F /* ServiceLocator.swift in Sources */, 317F679826420E9D00BA2A7A /* CardReaderSettingsViewModelsOrderedList.swift in Sources */, DE279BA426E9C4DC002BA963 /* ShippingLabelPackagesForm.swift in Sources */, - AEB4DB9F290AEA6100AE4340 /* AuthenticationService.swift in Sources */, CE583A0421076C0100D73C1C /* NewNoteViewController.swift in Sources */, CECC759723D607C900486676 /* OrderItemRefund+Woo.swift in Sources */, 03EF250228C615A5006A033E /* InPersonPaymentsMenuViewModel.swift in Sources */, @@ -10249,7 +10230,6 @@ B54FBE552111F70700390F57 /* ResultsController+UIKit.swift in Sources */, CE2409F1215D12D30091F887 /* WooNavigationController.swift in Sources */, 0294F8AB25E8A12C005B537A /* WooTabNavigationController.swift in Sources */, - AEB4DBA1290AEC0D00AE4340 /* RequestAuthenticator.swift in Sources */, 029F29FA24D93E9E004751CA /* EditableProductModel.swift in Sources */, 31FC8CE927B476BA004B9456 /* CardReaderSettingsResultsControllers.swift in Sources */, D449C52926DFBCCC00D75B02 /* WhatsNewHostingController.swift in Sources */, @@ -10327,7 +10307,6 @@ 45C8B2582313FA570002FA77 /* CustomerNoteTableViewCell.swift in Sources */, 02C3FACE282A93020095440A /* WooAnalyticsEvent+Dashboard.swift in Sources */, DE67D46726B98FD000EFE8DB /* Publisher+WithLatestFrom.swift in Sources */, - AEB4DB97290AE74B00AE4340 /* CookieJar.swift in Sources */, 7493BB8E2149852A003071A9 /* TopPerformersHeaderView.swift in Sources */, D88CA756237CE515005D2F44 /* UITabBar+Appearance.swift in Sources */, 45A8DA402664E40B00308FBE /* EmptyState.swift in Sources */, @@ -10752,7 +10731,6 @@ 02AB40822784297C00929CF3 /* ProductTableViewCellViewModelTests.swift in Sources */, 02829BAA288FA8B300951E1E /* MockUserNotification.swift in Sources */, D85B8336222FCDA1002168F3 /* StatusListTableViewCellTests.swift in Sources */, - AEB4DBA3290AEDB900AE4340 /* RequestAuthenticatorTests.swift in Sources */, 3198A1E82694DC7200597213 /* MockKnownReadersProvider.swift in Sources */, DEC51B04276B30F6009F3DF4 /* SystemStatusReportViewModelTests.swift in Sources */, 26B119C224D1CD3500FED5C7 /* WooConstantsTests.swift in Sources */, @@ -10954,7 +10932,6 @@ CCB366AF274518EC007D437A /* EditableOrderViewModelTests.swift in Sources */, 020BE76B23B4A380007FE54C /* AztecUnderlineFormatBarCommandTests.swift in Sources */, D83F5937225B402E00626E75 /* TitleAndEditableValueTableViewCellTests.swift in Sources */, - AEB4DB9D290AE94600AE4340 /* WKCookieJarTests.swift in Sources */, 773077F3251E954300178696 /* ProductDownloadFileViewModelTests.swift in Sources */, 2602A64A27BDC80200B347F1 /* RemoteOrderSynchronizerTests.swift in Sources */, DE4B3B2E269455D400EEF2D8 /* MockShipmentActionStoresManager.swift in Sources */, @@ -11064,7 +11041,6 @@ 02ECD1E124FF496200735BE5 /* PaginationTrackerTests.swift in Sources */, 45E9A6EB24DAFC3E00A600E8 /* ProductReviewsViewModelTests.swift in Sources */, 268EC46426D3F9C100716F5C /* EditCustomerNoteViewModelTests.swift in Sources */, - AEB4DB9B290AE92800AE4340 /* CookieJarTests.swift in Sources */, 453770D12431FF4700AC718D /* ProductSettingsViewModelTests.swift in Sources */, 2619FA2C25C897930006DAFF /* AddAttributeOptionsViewModelTests.swift in Sources */, 020BE77523B4A7EC007FE54C /* AztecSourceCodeFormatBarCommandTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Tools/CookieJarTests.swift b/WooCommerce/WooCommerceTests/Tools/CookieJarTests.swift deleted file mode 100644 index 2ad17df2cf8..00000000000 --- a/WooCommerce/WooCommerceTests/Tools/CookieJarTests.swift +++ /dev/null @@ -1,67 +0,0 @@ -import XCTest -import WebKit -@testable import WooCommerce - -class CookieJarTests: XCTestCase { - var mockCookieJar = MockCookieJar() - var cookieJar: CookieJar { - return mockCookieJar - } - let wordPressComLoginURL = URL(string: "https://wordpress.com/wp-login.php")! - - override func setUp() { - super.setUp() - mockCookieJar = MockCookieJar() - } - - func testGetCookies() { - addCookies() - - let expectation = self.expectation(description: "getCookies completion called") - cookieJar.getCookies(url: wordPressComLoginURL) { (cookies) in - XCTAssertEqual(cookies.count, 2) - expectation.fulfill() - } - waitForExpectations(timeout: 1, handler: nil) - } - - func testHasCookieMatching() { - addCookies() - - let expectation = self.expectation(description: "hasCookie completion called") - cookieJar.hasWordPressComAuthCookie(username: "testuser", atomicSite: false) { (matches) in - XCTAssertTrue(matches) - expectation.fulfill() - } - waitForExpectations(timeout: 1, handler: nil) - - } - func testHasCookieNotMatching() { - addCookies() - - let expectation = self.expectation(description: "hasCookie completion called") - cookieJar.hasWordPressComAuthCookie(username: "anotheruser", atomicSite: false) { (matches) in - XCTAssertFalse(matches) - expectation.fulfill() - } - waitForExpectations(timeout: 1, handler: nil) - } - - func testRemoveCookies() { - addCookies() - - let expectation = self.expectation(description: "removeCookies completion called") - cookieJar.removeWordPressComCookies { [mockCookieJar] in - XCTAssertEqual(mockCookieJar.cookies?.count, 1) - expectation.fulfill() - } - waitForExpectations(timeout: 1, handler: nil) - } -} - -private extension CookieJarTests { - func addCookies() { - mockCookieJar.setWordPressComCookie(username: "testuser") - mockCookieJar.setWordPressCookie(username: "testuser", domain: "example.com") - } -} diff --git a/WooCommerce/WooCommerceTests/Tools/RequestAuthenticatorTests.swift b/WooCommerce/WooCommerceTests/Tools/RequestAuthenticatorTests.swift deleted file mode 100644 index 0db1811f60f..00000000000 --- a/WooCommerce/WooCommerceTests/Tools/RequestAuthenticatorTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -import XCTest -@testable import WooCommerce - -private extension URLRequest { - var httpBodyString: String? { - return httpBody.flatMap({ data in - return String(data: data, encoding: .utf8) - }) - } -} - -class RequestAuthenticatorTests: XCTestCase { - let dotComLoginURL = URL(string: "https://wordpress.com/wp-login.php")! - let dotComUser = "comuser" - let dotComToken = "comtoken" - let siteLoginURL = URL(string: "https://example.com/wp-login.php")! - let siteUser = "siteuser" - let sitePassword = "x>73R9&9;r&ju9$J499FmZ?2*Nii/?$8" - let sitePasswordEncoded = "x%3E73R9%269;r%26ju9$J499FmZ?2*Nii/?$8" - - var dotComAuthenticator: RequestAuthenticator { - return RequestAuthenticator(credentials: .dotCom(username: dotComUser, authToken: dotComToken, authenticationType: .regular)) - } - - var siteAuthenticator: RequestAuthenticator { - return RequestAuthenticator( - credentials: .siteLogin(loginURL: siteLoginURL, username: siteUser, password: sitePassword)) - } - - func testUnauthenticatedDotComRequestWithCookie() { - let url = URL(string: "https://example.wordpress.com/some-page/")! - let authenticator = dotComAuthenticator - - let cookieJar = MockCookieJar() - cookieJar.setWordPressComCookie(username: dotComUser) - var authenticatedRequest: URLRequest? = nil - authenticator.request(url: url, cookieJar: cookieJar) { - authenticatedRequest = $0 - } - guard let request = authenticatedRequest else { - XCTFail("The authenticator should return a valid request") - return - } - XCTAssertEqual(request.url, url) - XCTAssertNil(request.value(forHTTPHeaderField: "Authorization")) - } - - func testDecideActionForNavigationResponse() { - let url = URL(string: "https://example.wordpress.com/some-page/")! - let authenticator = dotComAuthenticator - let cookieJar = MockCookieJar() - cookieJar.setWordPressComCookie(username: dotComUser) - - let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - - let expectation = self.expectation(description: "Action Should be decided") - authenticator.decideActionFor(response: response, cookieJar: cookieJar) { action in - XCTAssertEqual(action, RequestAuthenticator.WPNavigationActionType.allow) - expectation.fulfill() - } - - waitForExpectations(timeout: 0.2) - } - - func testDecideActionForNavigationResponse_RemoteLoginError() { - let url = URL(string: "https://r-login.wordpress.com/remote-login.php?action=auth")! - let authenticator = dotComAuthenticator - let cookieJar = MockCookieJar() - cookieJar.setWordPressComCookie(username: dotComUser) - - let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - - let expectation = self.expectation(description: "Action Should be decided") - authenticator.decideActionFor(response: response, cookieJar: cookieJar) { action in - XCTAssertEqual(action, RequestAuthenticator.WPNavigationActionType.reload) - expectation.fulfill() - } - - waitForExpectations(timeout: 0.2) - } - - func testDecideActionForNavigationResponse_ClientError() { - let url = URL(string: "https://example.wordpress.com/some-page/")! - let authenticator = dotComAuthenticator - let cookieJar = MockCookieJar() - cookieJar.setWordPressComCookie(username: dotComUser) - - let response = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)! - - let expectation = self.expectation(description: "Action Should be decided") - authenticator.decideActionFor(response: response, cookieJar: cookieJar) { action in - XCTAssertEqual(action, RequestAuthenticator.WPNavigationActionType.reload) - expectation.fulfill() - } - - waitForExpectations(timeout: 0.2) - } -} diff --git a/WooCommerce/WooCommerceTests/Tools/WKCookieJarTests.swift b/WooCommerce/WooCommerceTests/Tools/WKCookieJarTests.swift deleted file mode 100644 index 424c859e2a0..00000000000 --- a/WooCommerce/WooCommerceTests/Tools/WKCookieJarTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -import XCTest -import WebKit -@testable import WooCommerce - -class WKCookieJarTests: XCTestCase { - var wkCookieStore = WKWebsiteDataStore.nonPersistent().httpCookieStore - var cookieJar: CookieJar { - return wkCookieStore - } - let wordPressComLoginURL = URL(string: "https://wordpress.com/wp-login.php")! - - override func setUp() { - super.setUp() - wkCookieStore = WKWebsiteDataStore.nonPersistent().httpCookieStore - addCookies() - } - - override func tearDown() { - super.tearDown() - } - - func testGetCookies() { - let expectation = self.expectation(description: "getCookies completion called") - cookieJar.getCookies(url: wordPressComLoginURL) { (cookies) in - XCTAssertEqual(cookies.count, 1, "Should be one cookie for wordpress.com") - expectation.fulfill() - } - waitForExpectations(timeout: 5, handler: nil) - } - - func testHasCookieMatching() { - let expectation = self.expectation(description: "hasCookie completion called") - cookieJar.hasWordPressComAuthCookie(username: "testuser", atomicSite: false) { (matches) in - XCTAssertTrue(matches, "Cookies should exist for wordpress.com + testuser") - expectation.fulfill() - } - waitForExpectations(timeout: 5, handler: nil) - - } - func testHasCookieNotMatching() { - let expectation = self.expectation(description: "hasCookie completion called") - cookieJar.hasWordPressComAuthCookie(username: "anotheruser", atomicSite: false) { (matches) in - XCTAssertFalse(matches, "Cookies should not exist for wordpress.com + anotheruser") - expectation.fulfill() - } - waitForExpectations(timeout: 5, handler: nil) - } - - func testRemoveCookies() { - let expectation = self.expectation(description: "removeCookies completion called") - cookieJar.removeWordPressComCookies { [wkCookieStore] in - wkCookieStore.getAllCookies { cookies in - XCTAssertEqual(cookies.count, 1) - expectation.fulfill() - } - } - waitForExpectations(timeout: 5, handler: nil) - } -} - -private extension WKCookieJarTests { - func addCookies() { - wkCookieStore.setWordPressComCookie(username: "testuser") - wkCookieStore.setWordPressCookie(username: "testuser", domain: "example.com") - } -}