Skip to content

Commit 557fdad

Browse files
committed
Added verbose logging to the login polling.
- Removed user-defined device name from user agent string because since iOS 16 only a generic one is returned for privacy reasons anway. This also makes the user agent string consistent with other requests from the iOS app so server logs can be filtered correctly. - Updated web view to use a private browser session which does not persist any data on disk but only keeps them in memory for the lifetime of the web view. - Updated variable names to remove redundancy and improve overall code legibility. - Added a lot of logging statement to NCLoginProvider. - Added some documentation comments on NCLoginProvider. - Updated NextcloudKit reference to 6.0.10. Signed-off-by: Iva Horn <[email protected]>
1 parent 5bb32e2 commit 557fdad

File tree

5 files changed

+158
-57
lines changed

5 files changed

+158
-57
lines changed

Brand/Intro/NCIntroViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ class NCIntroViewController: UIViewController, UICollectionViewDataSource, UICol
189189
@IBAction func signupWithProvider(_ sender: Any) {
190190
if let viewController = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLoginProvider") as? NCLoginProvider {
191191
viewController.controller = self.controller
192-
viewController.urlBase = NCBrandOptions.shared.linkloginPreferredProviders
192+
viewController.initialURLString = NCBrandOptions.shared.linkloginPreferredProviders
193193
self.navigationController?.pushViewController(viewController, animated: true)
194194
}
195195
}

Nextcloud.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6233,8 +6233,8 @@
62336233
isa = XCRemoteSwiftPackageReference;
62346234
repositoryURL = "https://github.com/nextcloud/NextcloudKit";
62356235
requirement = {
6236-
kind = exactVersion;
6237-
version = 6.0.10;
6236+
kind = upToNextMajorVersion;
6237+
minimumVersion = 6.0.10;
62386238
};
62396239
};
62406240
F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {

iOSClient/Login/NCLogin.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,9 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate {
328328
NextcloudKit.shared.getLoginFlowV2(serverUrl: url, options: loginOptions) { [self] token, endpoint, login, _, error in
329329
// Login Flow V2
330330
if error == .success, let token, let endpoint, let login {
331+
NextcloudKit.shared.nkCommonInstance.writeLog(info: "Successfully received login flow information.")
331332
let safariVC = NCLoginProvider()
332-
safariVC.urlBase = login
333+
safariVC.initialURLString = login
333334
safariVC.uiColor = textColor
334335
safariVC.delegate = self
335336
safariVC.startPolling(loginFlowV2Token: token, loginFlowV2Endpoint: endpoint, loginFlowV2Login: login)
@@ -419,12 +420,16 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate {
419420
}
420421
}
421422

423+
// MARK: - NCShareAccountsDelegate
424+
422425
extension NCLogin: NCShareAccountsDelegate {
423426
func selected(url: String, user: String) {
424427
isUrlValid(url: url, user: user)
425428
}
426429
}
427430

431+
// MARK: - UIDocumentPickerDelegate
432+
428433
extension NCLogin: ClientCertificateDelegate, UIDocumentPickerDelegate {
429434
func didAskForClientCertificate() {
430435
let alertNoCertFound = UIAlertController(title: NSLocalizedString("_no_client_cert_found_", comment: ""), message: NSLocalizedString("_no_client_cert_found_desc_", comment: ""), preferredStyle: .alert)
@@ -466,6 +471,8 @@ extension NCLogin: ClientCertificateDelegate, UIDocumentPickerDelegate {
466471
}
467472
}
468473

474+
// MARK: - NCLoginProviderDelegate
475+
469476
extension NCLogin: NCLoginProviderDelegate {
470477
func onBack() {
471478
loginButton.isEnabled = true

iOSClient/Login/NCLoginProvider.swift

Lines changed: 139 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// SPDX-FileCopyrightText: Nextcloud GmbH
2+
// SPDX-FileCopyrightText: 2025 Iva Horn
23
// SPDX-FileCopyrightText: 2025 Milen Pivchev
34
// SPDX-License-Identifier: GPL-3.0-or-later
45

@@ -7,91 +8,113 @@ import UIKit
78
import NextcloudKit
89
import FloatingPanel
910

11+
protocol NCLoginProviderDelegate: AnyObject {
12+
///
13+
/// Called when the back button is tapped in the login provider view.
14+
///
15+
func onBack()
16+
}
17+
18+
///
19+
/// View which presents the web view to login at a Nextcloud instance.
20+
///
1021
class NCLoginProvider: UIViewController {
11-
var webView: WKWebView?
12-
let utility = NCUtility()
22+
var logger = NextcloudKit.shared.nkCommonInstance
23+
var webView: WKWebView!
1324
var titleView: String = ""
14-
var urlBase = ""
25+
var initialURLString = ""
1526
var uiColor: UIColor = .white
1627
weak var delegate: NCLoginProviderDelegate?
1728
var controller: NCMainTabBarController?
1829

30+
///
31+
/// A polling loop active in the background to check for the current status of the login flow.
32+
///
1933
var pollingTask: Task<Void, any Error>?
2034

2135
// MARK: - View Life Cycle
2236

2337
override func viewDidLoad() {
2438
super.viewDidLoad()
39+
logger.writeLog(info: "Login provider view did load.")
40+
let configuration = WKWebViewConfiguration()
41+
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
2542

26-
webView = WKWebView(frame: CGRect.zero, configuration: WKWebViewConfiguration())
27-
if let webView {
28-
webView.navigationDelegate = self
29-
view.addSubview(webView)
43+
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
44+
webView.customUserAgent = userAgent
3045

31-
webView.translatesAutoresizingMaskIntoConstraints = false
32-
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
33-
webView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
34-
webView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
35-
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
46+
if #available(iOS 16.4, *) {
47+
webView.isInspectable = true
3648
}
3749

50+
webView.navigationDelegate = self
51+
view.addSubview(webView)
52+
53+
webView.translatesAutoresizingMaskIntoConstraints = false
54+
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
55+
webView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
56+
webView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
57+
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
58+
59+
self.webView = webView
60+
3861
let navigationItemBack = UIBarButtonItem(image: UIImage(systemName: "arrow.left"), style: .done, target: self, action: #selector(goBack(_:)))
3962
navigationItemBack.tintColor = uiColor
4063
navigationItem.leftBarButtonItem = navigationItemBack
4164
}
4265

4366
override func viewDidAppear(_ animated: Bool) {
4467
super.viewDidAppear(animated)
68+
logger.writeLog(info: "Login provider appeared.")
4569

46-
if let url = URL(string: urlBase),
47-
let webView {
48-
HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)
49-
50-
WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
51-
WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: records, completionHandler: {
52-
self.loadWebPage(webView: webView, url: url)
53-
})
54-
}
55-
} else {
70+
guard let url = URL(string: initialURLString) else {
5671
let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_login_url_error_")
5772
NCContentPresenter().showError(error: error, priority: .max)
73+
return
5874
}
5975

60-
if let host = URL(string: urlBase)?.host {
76+
if let host = url.host {
6177
titleView = host
78+
6279
if let activeTableAccount = NCManageDatabase.shared.getActiveTableAccount(), NCKeychain().getPassword(account: activeTableAccount.account).isEmpty {
6380
titleView = NSLocalizedString("_user_", comment: "") + " " + activeTableAccount.userId + " " + NSLocalizedString("_in_", comment: "") + " " + host
6481
}
6582
}
6683

84+
loadWebPage(url: url)
6785
self.title = titleView
6886
}
6987

7088
override func viewDidDisappear(_ animated: Bool) {
7189
super.viewDidDisappear(animated)
90+
logger.writeLog(info: "Login provider view did disappear.")
91+
7292
NCActivityIndicator.shared.stop()
7393

94+
guard pollingTask != nil else {
95+
return
96+
}
97+
98+
logger.writeLog(info: "Cancelling existing polling task because view did disappear...")
7499
pollingTask?.cancel()
75100
pollingTask = nil
76101
}
77102

78-
func loadWebPage(webView: WKWebView, url: URL) {
103+
// MARK: - Navigation
104+
105+
private func loadWebPage(url: URL) {
79106
let language = NSLocale.preferredLanguages[0] as String
80107
var request = URLRequest(url: url)
81108

82-
if let deviceName = "\(UIDevice.current.name) (\(NCBrandOptions.shared.brand) iOS)".cString(using: .utf8),
83-
let deviceUserAgent = String(cString: deviceName, encoding: .ascii) {
84-
webView.customUserAgent = deviceUserAgent
85-
} else {
86-
webView.customUserAgent = userAgent
87-
}
88-
89109
request.addValue("true", forHTTPHeaderField: "OCS-APIRequest")
90110
request.addValue(language, forHTTPHeaderField: "Accept-Language")
91111

92112
webView.load(request)
93113
}
94114

115+
///
116+
/// Dismiss the login web view from the hierarchy.
117+
///
95118
@objc func goBack(_ sender: Any?) {
96119
delegate?.onBack()
97120

@@ -102,41 +125,88 @@ class NCLoginProvider: UIViewController {
102125
}
103126
}
104127

128+
// MARK: - Polling
129+
130+
///
131+
/// Start checking the status of the login flow in the background periodally.
132+
///
105133
func startPolling(loginFlowV2Token: String, loginFlowV2Endpoint: String, loginFlowV2Login: String) {
106-
pollingTask = poll(loginFlowV2Token: loginFlowV2Token, loginFlowV2Endpoint: loginFlowV2Endpoint, loginFlowV2Login: loginFlowV2Login)
134+
logger.writeLog(info: "Starting polling at \(loginFlowV2Endpoint) with token \(loginFlowV2Token)")
135+
pollingTask = createPollingTask(token: loginFlowV2Token, endpoint: loginFlowV2Endpoint)
136+
logger.writeLog(info: "Polling task created.")
107137
}
108138

109-
private func getPollResponse(loginFlowV2Token: String, loginFlowV2Endpoint: String, loginOptions: NKRequestOptions) async -> (urlBase: String, loginName: String, appPassword: String)? {
139+
///
140+
/// Fetch the server response and process it.
141+
///
142+
private func poll(token: String, endpoint: String, options: NKRequestOptions) async -> (urlBase: String, loginName: String, appPassword: String)? {
110143
await withCheckedContinuation { continuation in
111-
NextcloudKit.shared.getLoginFlowV2Poll(token: loginFlowV2Token, endpoint: loginFlowV2Endpoint, options: loginOptions) { server, loginName, appPassword, _, error in
112-
if error == .success, let urlBase = server, let user = loginName, let appPassword {
113-
continuation.resume(returning: (urlBase, user, appPassword))
114-
} else {
144+
NextcloudKit.shared.getLoginFlowV2Poll(token: token, endpoint: endpoint, options: options) { [weak self] server, loginName, appPassword, _, error in
145+
guard let self else {
146+
return
147+
}
148+
149+
guard error == .success else {
150+
logger.writeLog(error: "Login poll result for token \"\(token)\" is not successful!")
151+
continuation.resume(returning: nil)
152+
return
153+
}
154+
155+
guard let urlBase = server else {
156+
logger.writeLog(error: "Login poll response field for server for token \"\(token)\" is nil!")
157+
continuation.resume(returning: nil)
158+
return
159+
}
160+
161+
guard let user = loginName else {
162+
logger.writeLog(error: "Login poll response field for user name for token \"\(token)\" is nil!")
163+
continuation.resume(returning: nil)
164+
return
165+
}
166+
167+
guard let appPassword = appPassword else {
168+
logger.writeLog(error: "Login poll response field for app password for token \"\(token)\" is nil!")
115169
continuation.resume(returning: nil)
170+
return
116171
}
172+
173+
logger.writeLog(info: "Returning login poll response for \"\(user)\" on \"\(urlBase)\" for token \"\(token)\".")
174+
continuation.resume(returning: (urlBase, user, appPassword))
117175
}
118176
}
119177
}
120178

179+
///
180+
/// Handle the values acquired by polling successfully.
181+
///
121182
private func handleGrant(urlBase: String, loginName: String, appPassword: String) async {
183+
logger.writeLog(info: "Handling login grant values for \(loginName) on \(urlBase)")
184+
122185
await withCheckedContinuation { continuation in
123186
if controller == nil {
187+
logger.writeLog(info: "View controller is still undefined, will resolve root view controller of first window.")
124188
controller = UIApplication.shared.firstWindow?.rootViewController as? NCMainTabBarController
125189
}
126190

127191
NCAccount().createAccount(viewController: self, urlBase: urlBase, user: loginName, password: appPassword, controller: controller) {
128-
continuation.resume()
192+
self.logger.writeLog(info: "Account creation for \(loginName) on \(urlBase) completed based on login grant values.")
193+
continuation.resume()
129194
}
130195
}
131196
}
132197

133-
private func poll(loginFlowV2Token: String, loginFlowV2Endpoint: String, loginFlowV2Login: String) -> Task<Void, any Error> {
134-
let loginOptions = NKRequestOptions(customUserAgent: userAgent)
198+
///
199+
/// Set up the `Task` which frequently checks the server.
200+
///
201+
private func createPollingTask(token: String, endpoint: String) -> Task<Void, any Error> {
202+
let options = NKRequestOptions(customUserAgent: userAgent)
135203
var grantValues: (urlBase: String, loginName: String, appPassword: String)?
136204

137205
return Task { @MainActor in
138206
repeat {
139-
grantValues = await getPollResponse(loginFlowV2Token: loginFlowV2Token, loginFlowV2Endpoint: loginFlowV2Endpoint, loginOptions: loginOptions)
207+
try Task.checkCancellation()
208+
209+
grantValues = await poll(token: token, endpoint: endpoint, options: options)
140210
try await Task.sleep(nanoseconds: 1_000_000_000) // .seconds() is not supported on iOS 15 yet.
141211
} while grantValues == nil
142212

@@ -145,32 +215,48 @@ class NCLoginProvider: UIViewController {
145215
}
146216

147217
await handleGrant(urlBase: grantValues.urlBase, loginName: grantValues.loginName, appPassword: grantValues.appPassword)
218+
logger.writeLog(info: "Polling task completed.")
148219
}
149220
}
150221
}
151222

223+
// MARK: - WKNavigationDelegate
224+
152225
extension NCLoginProvider: WKNavigationDelegate {
153226
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
154-
guard let url = webView.url else { return }
155-
let urlString: String = url.absoluteString.lowercased()
227+
logger.writeLog(info: "Web view did receive server redirect for provisional navigation.")
228+
229+
guard let currentWebViewURL = webView.url else {
230+
logger.writeLog(error: "Web view does not have a URL after receiving a server redirect for provisional navigation!")
231+
return
232+
}
233+
234+
let currentWebViewURLString: String = currentWebViewURL.absoluteString.lowercased()
235+
236+
// Prevent HTTP redirects.
237+
if initialURLString.lowercased().hasPrefix("https://") && currentWebViewURLString.hasPrefix("http://") {
238+
logger.writeLog(error: "Web view redirect degrades session from HTTPS to HTTP and must be cancelled!")
156239

157-
// prevent http redirection
158-
if urlBase.lowercased().hasPrefix("https://") && urlString.lowercased().hasPrefix("http://") {
159240
let alertController = UIAlertController(title: NSLocalizedString("_error_", comment: ""), message: NSLocalizedString("_prevent_http_redirection_", comment: ""), preferredStyle: .alert)
241+
160242
alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in
161243
_ = self.navigationController?.popViewController(animated: true)
162244
}))
245+
163246
self.present(alertController, animated: true)
247+
164248
return
165249
}
166250

167-
// Login via provider
168-
if urlString.hasPrefix(NCBrandOptions.shared.webLoginAutenticationProtocol) == true && urlString.contains("login") == true {
251+
// Login via provider.
252+
if currentWebViewURLString.hasPrefix(NCBrandOptions.shared.webLoginAutenticationProtocol) && currentWebViewURLString.contains("login") {
253+
logger.writeLog(info: "Web view redirect to provider login URL detected.")
254+
169255
var server: String = ""
170256
var user: String = ""
171257
var password: String = ""
258+
let keyValue = currentWebViewURL.path.components(separatedBy: "&")
172259

173-
let keyValue = url.path.components(separatedBy: "&")
174260
for value in keyValue {
175261
if value.contains("server:") { server = value }
176262
if value.contains("user:") { user = value }
@@ -192,6 +278,8 @@ extension NCLoginProvider: WKNavigationDelegate {
192278
}
193279

194280
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
281+
logger.writeLog(info: "Web view did receive authentication challenge.")
282+
195283
DispatchQueue.global().async {
196284
if let serverTrust = challenge.protectionSpace.serverTrust {
197285
completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust))
@@ -202,18 +290,17 @@ extension NCLoginProvider: WKNavigationDelegate {
202290
}
203291

204292
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
293+
logger.writeLog(info: "Web view will allow navigation to \(navigationAction.request.url?.absoluteString ?? "nil")")
205294
decisionHandler(.allow)
206295
}
207296

208297
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
298+
logger.writeLog(info: "Web view did start provisional navigation.")
209299
NCActivityIndicator.shared.startActivity(backgroundView: self.view, style: .medium, blurEffect: false)
210300
}
211301

212302
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
303+
logger.writeLog(info: "Web view did finish navigation to \(webView.url?.absoluteString ?? "nil")")
213304
NCActivityIndicator.shared.stop()
214305
}
215306
}
216-
217-
protocol NCLoginProviderDelegate: AnyObject {
218-
func onBack()
219-
}

0 commit comments

Comments
 (0)