From 34ef34d79f9ae12fbc211ad462de5dbe7c1944fd Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 21 Nov 2025 11:34:02 +0700 Subject: [PATCH 1/4] Add new error type for failure to handle auth challenge --- ...ApplicationPasswordTutorialViewModel.swift | 2 + .../AuthenticationManager.swift | 4 +- .../SiteCredentialLoginUseCase.swift | 53 +++++++++++++++++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/Authentication/Application Password/ApplicationPasswordTutorialViewModel.swift b/WooCommerce/Classes/Authentication/Application Password/ApplicationPasswordTutorialViewModel.swift index b9bb8c54c0b..bfd4eb515cf 100644 --- a/WooCommerce/Classes/Authentication/Application Password/ApplicationPasswordTutorialViewModel.swift +++ b/WooCommerce/Classes/Authentication/Application Password/ApplicationPasswordTutorialViewModel.swift @@ -23,6 +23,8 @@ struct ApplicationPasswordTutorialViewModel { comment: "Reason for why the user could not login tin the application password tutorial screen") case .invalidCredentials: return error.localizedDescription + case .failedAuthenticationChallenge: + fatalError("Failure to handle authentication challenge is not eligible for application password flow.") } } } diff --git a/WooCommerce/Classes/Authentication/AuthenticationManager.swift b/WooCommerce/Classes/Authentication/AuthenticationManager.swift index e173b589914..d090be7e84b 100644 --- a/WooCommerce/Classes/Authentication/AuthenticationManager.swift +++ b/WooCommerce/Classes/Authentication/AuthenticationManager.swift @@ -363,7 +363,9 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate { let isAppPasswordAuthError = { switch error { - case SiteCredentialLoginError.genericFailure, SiteCredentialLoginError.invalidCredentials: + case SiteCredentialLoginError.genericFailure, + SiteCredentialLoginError.invalidCredentials, + SiteCredentialLoginError.failedAuthenticationChallenge: return false case is SiteCredentialLoginError: return true diff --git a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift index 8d48a94034f..82220541b44 100644 --- a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift +++ b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift @@ -15,6 +15,7 @@ enum SiteCredentialLoginError: LocalizedError { case inaccessibleLoginPage case inaccessibleAdminPage case unacceptableStatusCode(code: Int) + case failedAuthenticationChallenge case genericFailure(underlyingError: Error) /// Used for tracking error code @@ -26,7 +27,8 @@ enum SiteCredentialLoginError: LocalizedError { .invalidLoginResponse, .invalidCredentials, .loginFailed, - .unacceptableStatusCode: + .unacceptableStatusCode, + .failedAuthenticationChallenge: return NSError(domain: Self.errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]) case .genericFailure(let underlyingError): return underlyingError as NSError @@ -45,6 +47,8 @@ enum SiteCredentialLoginError: LocalizedError { return 401 case .unacceptableStatusCode(let code): return code + case .failedAuthenticationChallenge: + return -2 case .genericFailure(let underlyingError): return (underlyingError as NSError).code } @@ -64,6 +68,8 @@ enum SiteCredentialLoginError: LocalizedError { return message case .unacceptableStatusCode(let code): return String(format: Localization.unacceptableStatusCode, code) + case .failedAuthenticationChallenge: + return Localization.failedAuthenticationChallenge case .genericFailure: return "" } @@ -83,7 +89,8 @@ enum SiteCredentialLoginError: LocalizedError { comment: "Error message explaining login failure due to blocked WP Admin page" ) static let invalidLoginResponse = NSLocalizedString( - "Unable to login due to an unexpected response from your site. We are working on fixing this issue.", + "siteCredentialLoginError.invalidLoginResponse.message", + value: "Unable to login due to an unexpected response from your site.", comment: "Error message explaining login failure due to unexpected response." ) static let unacceptableStatusCode = NSLocalizedString( @@ -94,6 +101,11 @@ enum SiteCredentialLoginError: LocalizedError { "It seems the username or password you entered doesn't quite match. Double-check your credentials and try again.", comment: "Error message explaining login failure due to invalid credentials." ) + static let failedAuthenticationChallenge = NSLocalizedString( + "siteCredentialLoginError.failedAuthenticationChallenge.message", + value: "Unable to log in due to an unexpected security measure on your store. Please contact support for troubleshooting.", + comment: "Error message explaining login failure due to an unexpected authentication challenge." + ) } } @@ -104,12 +116,24 @@ enum SiteCredentialLoginError: LocalizedError { /// - If the request does not redirect or the redirect fails, login fails. /// Ref: pe5sF9-1iQ-p2 /// -final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol { +final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol, URLSessionTaskDelegate { private let siteURL: String private let cookieJar: HTTPCookieStorage private var successHandler: (() -> Void)? private var errorHandler: ((SiteCredentialLoginError) -> Void)? - private lazy var session = URLSession(configuration: .default) + + private var receivedAuthChallengeMethod: String? + + private lazy var session = { + var configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForResource = 60 + return URLSession(configuration: configuration, delegate: self, delegateQueue: nil) + }() + + static let supportedAuthChallengeMethods = [ + NSURLAuthenticationMethodServerTrust, + NSURLAuthenticationMethodHTTPBasic + ] init(siteURL: String, cookieJar: HTTPCookieStorage = HTTPCookieStorage.shared) { @@ -128,10 +152,14 @@ final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol { // Old cookies can make the login succeeds even with incorrect credentials // So we need to clear all cookies before login. clearAllCookies() + + receivedAuthChallengeMethod = nil + guard let loginRequest = buildLoginRequest(username: username, password: password) else { DDLogError("⛔️ Error constructing login requests") return } + Task { @MainActor in do { try await startLogin(with: loginRequest) @@ -139,7 +167,11 @@ final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol { } catch let error as SiteCredentialLoginError { errorHandler?(error) } catch { - errorHandler?(.genericFailure(underlyingError: error as NSError)) + if receivedAuthChallengeMethod != nil { + errorHandler?(.failedAuthenticationChallenge) + } else { + errorHandler?(.genericFailure(underlyingError: error as NSError)) + } } } } @@ -293,3 +325,14 @@ private extension String { return wholeMatch(of: regex) != nil } } + +// MARK: - URLSessionTaskDelegate +extension SiteCredentialLoginUseCase { + func urlSession(_ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + let authMethod = challenge.protectionSpace.authenticationMethod + receivedAuthChallengeMethod = authMethod + completionHandler(.performDefaultHandling, nil) + } +} From d02861a97e16ea6ea2720e3059d0224a6a1a9c58 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 21 Nov 2025 11:41:38 +0700 Subject: [PATCH 2/4] Add log for required auth challenge during login --- .../Classes/Authentication/SiteCredentialLoginUseCase.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift index 82220541b44..3af21b6641d 100644 --- a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift +++ b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift @@ -333,6 +333,7 @@ extension SiteCredentialLoginUseCase { completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { let authMethod = challenge.protectionSpace.authenticationMethod receivedAuthChallengeMethod = authMethod + DDLogWarn("⚠️ An authentication challenge is required for login: \(authMethod)") completionHandler(.performDefaultHandling, nil) } } From f2a859458b7370fcdd95d6d5cf0ec6baebaf06c9 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 21 Nov 2025 14:01:39 +0700 Subject: [PATCH 3/4] Remove unused property --- .../Classes/Authentication/SiteCredentialLoginUseCase.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift index 3af21b6641d..eb9b8be636a 100644 --- a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift +++ b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift @@ -130,11 +130,6 @@ final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol, U return URLSession(configuration: configuration, delegate: self, delegateQueue: nil) }() - static let supportedAuthChallengeMethods = [ - NSURLAuthenticationMethodServerTrust, - NSURLAuthenticationMethodHTTPBasic - ] - init(siteURL: String, cookieJar: HTTPCookieStorage = HTTPCookieStorage.shared) { self.siteURL = siteURL From 739da068ddb3a08ee98989eb112896e4994dd55c Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 21 Nov 2025 18:01:51 +0700 Subject: [PATCH 4/4] Cancel unhandled auth challenge --- .../Classes/Authentication/SiteCredentialLoginUseCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift index eb9b8be636a..32981635ffd 100644 --- a/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift +++ b/WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift @@ -329,6 +329,6 @@ extension SiteCredentialLoginUseCase { let authMethod = challenge.protectionSpace.authenticationMethod receivedAuthChallengeMethod = authMethod DDLogWarn("⚠️ An authentication challenge is required for login: \(authMethod)") - completionHandler(.performDefaultHandling, nil) + completionHandler(.cancelAuthenticationChallenge, nil) } }