Skip to content

Commit 7b8a062

Browse files
authored
Merge pull request #7787 from woocommerce/issue/7370-cookie-authentication-webview
Login: Handle cookie authentication in Authenticated web view
2 parents 2ea2163 + 1a012ac commit 7b8a062

File tree

7 files changed

+82
-26
lines changed

7 files changed

+82
-26
lines changed

WooCommerce/Classes/Authentication/AuthenticatedWebViewController.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Combine
22
import UIKit
33
import WebKit
4+
import struct WordPressAuthenticator.WordPressOrgCredentials
45

56
/// A web view which is authenticated for WordPress.com, when possible.
67
///
@@ -26,8 +27,13 @@ final class AuthenticatedWebViewController: UIViewController {
2627
/// Strong reference for the subscription to update progress bar
2728
private var subscriptions: Set<AnyCancellable> = []
2829

29-
init(viewModel: AuthenticatedWebViewModel) {
30+
/// Optional credentials for authenticating with WP.org
31+
///
32+
private let wporgCredentials: WordPressOrgCredentials?
33+
34+
init(viewModel: AuthenticatedWebViewModel, wporgCredentials: WordPressOrgCredentials? = nil) {
3035
self.viewModel = viewModel
36+
self.wporgCredentials = wporgCredentials
3137
super.init(nibName: nil, bundle: nil)
3238
}
3339

@@ -87,9 +93,6 @@ private extension AuthenticatedWebViewController {
8793
}
8894

8995
func startLoading() {
90-
guard let url = viewModel.initialURL else {
91-
return
92-
}
9396
webView.publisher(for: \.estimatedProgress)
9497
.sink { [weak self] progress in
9598
if progress == 1 {
@@ -102,10 +105,29 @@ private extension AuthenticatedWebViewController {
102105

103106
webView.publisher(for: \.url)
104107
.sink { [weak self] url in
105-
self?.viewModel.handleRedirect(for: url)
108+
guard let self else { return }
109+
let initialURL = self.viewModel.initialURL
110+
// avoids infinite loop if the initial url happens to be the nonce retrieval path.
111+
if url?.absoluteString.contains(WKWebView.wporgNoncePath) == true,
112+
initialURL?.absoluteString.contains(WKWebView.wporgNoncePath) != true {
113+
self.loadContent()
114+
} else {
115+
self.viewModel.handleRedirect(for: url)
116+
}
106117
}
107118
.store(in: &subscriptions)
108119

120+
if let wporgCredentials, let request = try? webView.authenticateForWPOrg(with: wporgCredentials) {
121+
webView.load(request)
122+
} else {
123+
loadContent()
124+
}
125+
}
126+
127+
private func loadContent() {
128+
guard let url = viewModel.initialURL else {
129+
return
130+
}
109131
if let credentials = ServiceLocator.stores.sessionManager.defaultCredentials {
110132
webView.authenticateForWPComAndRedirect(to: url, credentials: credentials)
111133
} else {

WooCommerce/Classes/Authentication/AuthenticatedWebViewModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import WebKit
3+
import WordPressAuthenticator
34

45
/// Abstracts different configurations and logic for web view controllers
56
/// which are authenticated for WordPress.com, where possible

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,9 @@ private extension AuthenticationManager {
553553
with credentials: AuthenticatorCredentials,
554554
in navigationController: UINavigationController,
555555
onDismiss: @escaping () -> Void) {
556-
let viewModel = JetpackErrorViewModel(siteURL: siteURL, onJetpackSetupCompletion: { [weak self] authorizedEmailAddress in
556+
let viewModel = JetpackErrorViewModel(siteURL: siteURL,
557+
siteCredentials: credentials.wporg,
558+
onJetpackSetupCompletion: { [weak self] authorizedEmailAddress in
557559
guard let self = self else { return }
558560
// Resets the referenced site since the setup completed now.
559561
self.currentSelfHostedSite = nil
@@ -586,10 +588,13 @@ private extension AuthenticationManager {
586588
}
587589

588590
/// The error screen to be displayed when the user enters a site
589-
/// without Jetpack in the site discovery flow
591+
/// without Jetpack in the site discovery flow.
592+
/// More about this flow: pe5sF9-mz-p2.
590593
///
591594
func jetpackErrorUI(for siteURL: String, with matcher: ULAccountMatcher, in navigationController: UINavigationController) -> UIViewController {
592-
let viewModel = JetpackErrorViewModel(siteURL: siteURL, onJetpackSetupCompletion: { [weak self] authorizedEmailAddress in
595+
let viewModel = JetpackErrorViewModel(siteURL: siteURL,
596+
siteCredentials: nil,
597+
onJetpackSetupCompletion: { [weak self] authorizedEmailAddress in
593598
guard let self = self else { return }
594599

595600
// Tries re-syncing to get an updated store list
@@ -667,6 +672,7 @@ private extension AuthenticationManager {
667672
}
668673

669674
/// Appropriate error to display for a site when entered from the site discovery flow.
675+
/// More about this flow: pe5sF9-mz-p2
670676
///
671677
func errorUI(for site: WordPressComSiteInfo, in navigationController: UINavigationController) -> UIViewController {
672678
guard site.isWP else {

WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackErrorViewModel.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import WordPressUI
88
/// an error when Jetpack is not installed or is not connected
99
struct JetpackErrorViewModel: ULErrorViewModel {
1010
private let siteURL: String
11+
private let siteCredentials: WordPressOrgCredentials?
1112
private let analytics: Analytics
1213
private let jetpackSetupCompletionHandler: (String?) -> Void
1314
private let authentication: Authentication
1415

1516
init(siteURL: String?,
17+
siteCredentials: WordPressOrgCredentials?,
1618
analytics: Analytics = ServiceLocator.analytics,
1719
authentication: Authentication = ServiceLocator.authenticationManager,
1820
onJetpackSetupCompletion: @escaping (String?) -> Void) {
1921
self.siteURL = siteURL ?? Localization.yourSite
22+
self.siteCredentials = siteCredentials
2023
self.analytics = analytics
2124
self.authentication = authentication
2225
self.jetpackSetupCompletionHandler = onJetpackSetupCompletion
@@ -62,8 +65,10 @@ struct JetpackErrorViewModel: ULErrorViewModel {
6265
return
6366
}
6467

65-
let viewModel = JetpackSetupWebViewModel(siteURL: siteURL, analytics: analytics, onCompletion: jetpackSetupCompletionHandler)
66-
let connectionController = AuthenticatedWebViewController(viewModel: viewModel)
68+
let viewModel = JetpackSetupWebViewModel(siteURL: siteURL,
69+
analytics: analytics,
70+
onCompletion: jetpackSetupCompletionHandler)
71+
let connectionController = AuthenticatedWebViewController(viewModel: viewModel, wporgCredentials: siteCredentials)
6772
viewController.navigationController?.show(connectionController, sender: nil)
6873
}
6974

@@ -104,8 +109,8 @@ private extension JetpackErrorViewModel {
104109
comment: "Button linking to webview that explains what Jetpack is"
105110
+ "Presented when logging in with a site address that does not have a valid Jetpack installation")
106111

107-
static let primaryButtonTitle = NSLocalizedString("Install Jetpack",
108-
comment: "Action button for installing Jetpack."
112+
static let primaryButtonTitle = NSLocalizedString("Set up Jetpack",
113+
comment: "Action button for setting up Jetpack."
109114
+ "Presented when logging in with a site address that does not have a valid Jetpack installation")
110115

111116
static let secondaryButtonTitle = NSLocalizedString("Log In With Another Account",

WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackSetupWebViewModel.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ final class JetpackSetupWebViewModel: AuthenticatedWebViewModel {
1515
/// The email address that the user uses to authorize Jetpack
1616
private var authorizedEmailAddress: String?
1717

18-
init(siteURL: String, analytics: Analytics = ServiceLocator.analytics, onCompletion: @escaping (String?) -> Void) {
18+
init(siteURL: String,
19+
analytics: Analytics = ServiceLocator.analytics,
20+
onCompletion: @escaping (String?) -> Void) {
1921
self.siteURL = siteURL
2022
self.analytics = analytics
2123
self.completionHandler = onCompletion

WooCommerce/Classes/Extensions/WKWebView+Authenticated.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import Alamofire
22
import Foundation
33
import WebKit
4+
import struct WordPressAuthenticator.WordPressOrgCredentials
45
import struct Yosemite.Credentials
56
import class Networking.UserAgent
67

78
/// An extension to authenticate WPCom automatically
89
///
910
extension WKWebView {
11+
static let wporgNoncePath = "/admin-ajax.php?action=rest-nonce"
12+
13+
/// Cookie authentication following WordPressKit implementation:
14+
/// https://github.com/wordpress-mobile/WordPressKit-iOS/blob/trunk/WordPressKit/Authenticator.swift
15+
///
16+
func authenticateForWPOrg(with credentials: WordPressOrgCredentials) throws -> URLRequest {
17+
var request = try URLRequest(url: credentials.loginURL.asURL(), method: .post)
18+
request.httpShouldHandleCookies = true
19+
20+
let redirectLink = (credentials.adminURL + Self.wporgNoncePath)
21+
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
22+
23+
let parameters = ["log": credentials.username,
24+
"pwd": credentials.password,
25+
"redirect_to": redirectLink ?? ""]
26+
27+
return try URLEncoding.default.encode(request, with: parameters)
28+
}
29+
1030
func authenticateForWPComAndRedirect(to url: URL, credentials: Credentials?) {
1131
customUserAgent = UserAgent.defaultUserAgent
1232
do {

WooCommerce/WooCommerceTests/Authentication/JetpackErrorViewModelTests.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
2222

2323
func test_viewmodel_provides_expected_image() {
2424
// Given
25-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url) { _ in }
25+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil) { _ in }
2626

2727
// When
2828
let image = viewModel.image
@@ -33,7 +33,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
3333

3434
func test_viewmodel_provides_expected_visibility_for_auxiliary_button() {
3535
// Given
36-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url) { _ in }
36+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil) { _ in }
3737

3838
// When
3939
let isVisible = viewModel.isAuxiliaryButtonHidden
@@ -44,7 +44,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
4444

4545
func test_viewmodel_provides_expected_title_for_auxiliary_button() {
4646
// Given
47-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url) { _ in }
47+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil) { _ in }
4848

4949
// When
5050
let auxiliaryButtonTitle = viewModel.auxiliaryButtonTitle
@@ -55,7 +55,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
5555

5656
func test_viewmodel_provides_expected_title_for_primary_button() {
5757
// Given
58-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url) { _ in }
58+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil) { _ in }
5959

6060
// When
6161
let primaryButtonTitle = viewModel.primaryButtonTitle
@@ -66,7 +66,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
6666

6767
func test_viewmodel_provides_expected_title_for_secondary_button() {
6868
// Given
69-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url) { _ in }
69+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil) { _ in }
7070

7171
// When
7272
let secondaryButtonTitle = viewModel.secondaryButtonTitle
@@ -77,15 +77,15 @@ final class JetpackErrorViewModelTests: XCTestCase {
7777

7878
func test_viewmodel_provides_expected_title_for_right_bar_button_item() {
7979
// Given
80-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url) { _ in }
80+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil) { _ in }
8181

8282
// Then
8383
XCTAssertEqual(viewModel.rightBarButtonItemTitle, Expectations.helpBarButtonItemTitle)
8484
}
8585

8686
func test_viewModel_logs_an_event_when_viewDidLoad_is_triggered() throws {
8787
// Given
88-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, analytics: analytics) { _ in }
88+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil, analytics: analytics) { _ in }
8989

9090
assertEmpty(analyticsProvider.receivedEvents)
9191

@@ -99,7 +99,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
9999

100100
func test_viewModel_logs_an_event_when_install_jetpack_button_is_tapped() throws {
101101
// Given
102-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, analytics: analytics) { _ in }
102+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil, analytics: analytics) { _ in }
103103

104104
assertEmpty(analyticsProvider.receivedEvents)
105105

@@ -113,7 +113,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
113113

114114
func test_viewModel_logs_an_event_when_the_what_is_jetpack_button_is_tapped() throws {
115115
// Given
116-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, analytics: analytics) { _ in }
116+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil, analytics: analytics) { _ in }
117117

118118
assertEmpty(analyticsProvider.receivedEvents)
119119

@@ -128,7 +128,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
128128
func test_viewModel_invokes_present_support_when_the_help_button_is_tapped() throws {
129129
// Given
130130
let mockAuthentication = MockAuthentication()
131-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, authentication: mockAuthentication) { _ in }
131+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil, authentication: mockAuthentication) { _ in }
132132

133133
// When
134134
viewModel.didTapRightBarButtonItem(in: UIViewController())
@@ -140,7 +140,7 @@ final class JetpackErrorViewModelTests: XCTestCase {
140140
func test_viewModel_sends_correct_screen_value_in_present_support_method() throws {
141141
// Given
142142
let mockAuthentication = MockAuthentication()
143-
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, authentication: mockAuthentication) { _ in }
143+
let viewModel = JetpackErrorViewModel(siteURL: Expectations.url, siteCredentials: nil, authentication: mockAuthentication) { _ in }
144144

145145
// When
146146
viewModel.didTapRightBarButtonItem(in: UIViewController())
@@ -158,8 +158,8 @@ private extension JetpackErrorViewModelTests {
158158
static let whatIsJetpack = NSLocalizedString("What is Jetpack?",
159159
comment: "Button linking to webview that explains what Jetpack is"
160160
+ "Presented when logging in with a site address that does not have a valid Jetpack installation")
161-
static let primaryButtonTitle = NSLocalizedString("Install Jetpack",
162-
comment: "Action button for installing Jetpack."
161+
static let primaryButtonTitle = NSLocalizedString("Set up Jetpack",
162+
comment: "Action button for setting up Jetpack."
163163
+ "Presented when logging in with a site address that does not have a valid Jetpack installation")
164164

165165
static let secondaryButtonTitle = NSLocalizedString("Log In With Another Account",

0 commit comments

Comments
 (0)