Skip to content

Commit f74c1ae

Browse files
authored
Site credential login: Prevent redirect callback twice (#16262)
2 parents 7696da4 + a175e86 commit f74c1ae

File tree

1 file changed

+35
-53
lines changed

1 file changed

+35
-53
lines changed

WooCommerce/Classes/Authentication/SiteCredentialLoginUseCase.swift

Lines changed: 35 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,16 @@ enum SiteCredentialLoginError: LocalizedError {
100100
/// This use case handles site credential login without the need to use XMLRPC API.
101101
/// Steps for login:
102102
/// - Make a request to the site wp-login.php with a redirect to the nonce retrieval URL.
103-
/// - Upon redirect, cancel the request and verify if the redirect URL is the nonce retrieval URL.
104-
/// - If it is, make a request to retrieve nonce at that URL, the login succeeds if this is successful.
103+
/// - If the redirect succeeds with a nonce in the response, login is successful.
104+
/// - If the request does not redirect or the redirect fails, login fails.
105+
/// Ref: pe5sF9-1iQ-p2
105106
///
106107
final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol {
107108
private let siteURL: String
108109
private let cookieJar: HTTPCookieStorage
109110
private var successHandler: (() -> Void)?
110111
private var errorHandler: ((SiteCredentialLoginError) -> Void)?
111-
private lazy var session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
112+
private lazy var session = URLSession(configuration: .default)
112113

113114
init(siteURL: String,
114115
cookieJar: HTTPCookieStorage = HTTPCookieStorage.shared) {
@@ -134,10 +135,9 @@ final class SiteCredentialLoginUseCase: NSObject, SiteCredentialLoginProtocol {
134135
Task { @MainActor in
135136
do {
136137
try await startLogin(with: loginRequest)
138+
successHandler?()
137139
} catch let error as SiteCredentialLoginError {
138140
errorHandler?(error)
139-
} catch let nsError as NSError where nsError.domain == NSURLErrorDomain && nsError.code == -999 {
140-
/// login request is cancelled upon redirect, ignore this error
141141
} catch {
142142
errorHandler?(.genericFailure(underlyingError: error as NSError))
143143
}
@@ -160,10 +160,26 @@ private extension SiteCredentialLoginUseCase {
160160
throw SiteCredentialLoginError.invalidLoginResponse
161161
}
162162

163-
switch response.statusCode {
164-
case 404:
163+
/// The login request comes with a redirect header to nonce retrieval URL.
164+
/// If we get a response from this URL, that means the redirect is successful.
165+
/// We need to check the result of this redirect first to determine if login is successful.
166+
let isNonceUrl = response.url?.absoluteString.hasSuffix(Constants.wporgNoncePath) == true
167+
168+
switch (isNonceUrl, response.statusCode) {
169+
case (true, 200):
170+
if let nonceString = String(data: data, encoding: .utf8),
171+
nonceString.isValidNonce() {
172+
// success!
173+
return
174+
} else {
175+
throw SiteCredentialLoginError.invalidLoginResponse
176+
}
177+
case (true, 404):
178+
throw SiteCredentialLoginError.inaccessibleAdminPage
179+
case (false, 404):
165180
throw SiteCredentialLoginError.inaccessibleLoginPage
166-
case 200:
181+
case (false, 200):
182+
// 200 for the login URL, which means a failure
167183
guard let html = String(data: data, encoding: .utf8) else {
168184
throw SiteCredentialLoginError.invalidLoginResponse
169185
}
@@ -180,39 +196,6 @@ private extension SiteCredentialLoginUseCase {
180196
}
181197
}
182198

183-
@MainActor
184-
func checkRedirect(url: URL?) async {
185-
guard let url, url.absoluteString.hasSuffix(Constants.wporgNoncePath),
186-
let nonceRetrievalURL = URL(string: siteURL + Constants.adminPath + Constants.wporgNoncePath) else {
187-
errorHandler?(.invalidLoginResponse)
188-
return
189-
}
190-
do {
191-
let nonceRequest = try URLRequest(url: nonceRetrievalURL, method: .get)
192-
try await checkAdminPageAccess(with: nonceRequest)
193-
successHandler?()
194-
} catch let error as SiteCredentialLoginError {
195-
errorHandler?(error)
196-
} catch {
197-
errorHandler?(.genericFailure(underlyingError: error as NSError))
198-
}
199-
}
200-
201-
func checkAdminPageAccess(with nonceRequest: URLRequest) async throws {
202-
let (_, response) = try await session.data(for: nonceRequest)
203-
guard let response = response as? HTTPURLResponse else {
204-
throw SiteCredentialLoginError.invalidLoginResponse
205-
}
206-
switch response.statusCode {
207-
case 200:
208-
return // success 🎉
209-
case 404:
210-
throw SiteCredentialLoginError.inaccessibleAdminPage
211-
default:
212-
throw SiteCredentialLoginError.unacceptableStatusCode(code: response.statusCode)
213-
}
214-
}
215-
216199
func buildLoginRequest(username: String, password: String) -> URLRequest? {
217200
guard let loginURL = URL(string: siteURL + Constants.loginPath) else {
218201
return nil
@@ -239,18 +222,6 @@ private extension SiteCredentialLoginUseCase {
239222
}
240223
}
241224

242-
extension SiteCredentialLoginUseCase: URLSessionDataDelegate {
243-
func urlSession(_ session: URLSession,
244-
task: URLSessionTask,
245-
willPerformHTTPRedirection response: HTTPURLResponse,
246-
newRequest request: URLRequest) async -> URLRequest? {
247-
// Disables redirect and check if the redirect is correct
248-
task.cancel()
249-
await checkRedirect(url: request.url)
250-
return nil
251-
}
252-
}
253-
254225
extension SiteCredentialLoginUseCase {
255226
enum Constants {
256227
static let loginPath = "/wp-login.php"
@@ -310,4 +281,15 @@ private extension String {
310281
func hasInvalidCredentialsPattern() -> Bool {
311282
contains("document.querySelector('form').classList.add('shake')")
312283
}
284+
285+
/// Validates if the string matches the expected nonce format.
286+
/// A valid nonce should contain at least 2 alphanumeric characters.
287+
///
288+
func isValidNonce() -> Bool {
289+
guard let regex = try? Regex("^[0-9a-zA-Z]{2,}$") else {
290+
DDLogError("⚠️ Invalid regex pattern")
291+
return false
292+
}
293+
return wholeMatch(of: regex) != nil
294+
}
313295
}

0 commit comments

Comments
 (0)