Skip to content

Conversation

@itsmeichigo
Copy link
Contributor

@itsmeichigo itsmeichigo commented Dec 14, 2022

Closes: #8389

Description

This PR updates the Networking layer to support authentication with application passwords:

  • Created new request type RESTRequest. This requires a site URL to construct the endpoint URL for its path. It also accepts an optional fallback Jetpack request to be sent if application password is blocked for the given site.
  • Created a helper class RequestAuthenticator to update requests with the appropriate Authorization header if possible. This class handles fetching application password for REST requests and wrapping other request types with WPCOM tokens.
  • Updated AlamofireNetwork with a request authenticator to authenticate requests before sending them.
  • Updated DefaultStoresManager and AuthenticatedState to send an updated site ID to AlamofireNetwork when the selected store changes - to update the application password use case accordingly.

Testing instructions

No API endpoint has been migrated to use REST API yet, but please run the app and do a smoke test to make sure that API requests still work as before.

Screenshots

N/A


  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

@itsmeichigo itsmeichigo added the type: task An internally driven task. label Dec 14, 2022
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Dec 14, 2022

You can test the changes from this Pull Request by:
  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr8402-e66619a on your iPhone

If you need access to App Center, please ask a maintainer to add you.

return
}
let applicationPasswordUseCase = TemporaryApplicationPasswordUseCase(siteID: siteID, credentials: credentials)
requestAuthenticator.updateApplicationPasswordHandler(with: applicationPasswordUseCase)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: What do you think about just passing the siteID and let RequestAuthenticator take care of creating a new instance of the ApplicationPasswordUseCase?

I propose this because,

  1. RequestAuthenticator owns the usecase and we can also let it create the usecase.
  2. AlamofireNetwork doesn't have to know about the application passwords at all.

If we do this change we can also rename the methods func configureApplicationPasswordHandler and updateApplicationPasswordHandler accordingly. (Maybe just updateSiteID)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that was what I followed initially - but then I found that injecting the use case makes RequestAuthenticator more testable. Otherwise, I'm not sure how to test it with a mock use case. Please let me know if you have any suggestions regarding testability without an injected use case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that is a good point.

Can we use MockNetwork to mock the responses for unit testing? i.e. Inject a network into RequestAuthenticator and use that to initialize ApplicationPasswordUseCase

extension RESTRequest {
/// Updates the request headers with authentication information.
///
func updateRequest(with applicationPassword: ApplicationPassword) throws -> URLRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: I would prefer the name authenticateRequest(with to make the work of this method clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated as part of 3ede699.

Comment on lines 71 to 72
let username = "username"
let password = "password"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: Would it make sense to move the strings from this method to a private constants enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually incorrect, I meant to get the username and password from application password instead 😅. Updated in 3ede699.

restoreSessionSiteIfPossible()
ServiceLocator.pushNotesManager.reloadBadgeCount()

if let state = self.state as? AuthenticatedState {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Is self needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need self, because I want to reference the state property of the class.

import Alamofire
@testable import Networking

final class RequestAuthenticatorTests: XCTestCase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tests. 👏

return try await useCase.generateNewPassword()
}()
try await MainActor.run {
let updatedRequest = try restRequest.updateRequest(with: applicationPassword)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Should the updateRequest call happen in the main thread as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I got a failure when I didn't call updateRequest in the main thread - this was because the method makes a call to UserAgent.defaultUserAgent, which creates a WKWebView and that requires main thread.

Comment on lines 52 to 73
Task(priority: .medium) {
do {
let applicationPassword: ApplicationPassword = try await {
if let password = useCase.applicationPassword {
return password
}
return try await useCase.generateNewPassword()
}()
try await MainActor.run {
let updatedRequest = try restRequest.updateRequest(with: applicationPassword)
completion(updatedRequest)
}
} catch {
DDLogWarn("⚠️ Error generating application password and update request: \(error)")
// TODO: add Tracks
// Get the fallback Jetpack request to handle if possible.
let fallbackRequest = restRequest.fallbackRequest ?? request
await MainActor.run {
completion(createAuthenticatedRequestIfPossible(for: fallbackRequest))
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: What do you think about moving this to a private method named handleRESTReqest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in d1ea84c.


/// Attempts creating a request with WPCOM token if possible.
///
private func createAuthenticatedRequestIfPossible(for request: URLRequestConvertible) -> URLRequestConvertible {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: Consider naming it something like authenticateUsingWPCOMTokenIfPossible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also updated in d1ea84c.


/// Helper class to update requests with authorization header if possible.
///
final class RequestAuthenticator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever work extracting this into a separate class. 👏

@itsmeichigo itsmeichigo marked this pull request as ready for review December 21, 2022 05:53
@itsmeichigo
Copy link
Contributor Author

@selanthiraiyan I've updated this PR following our latest approach to use application password with cookie authentication and no fallback if the application password cannot be fetched. Please take another look, thanks!

upload.responseData { response in
completion(response.value, response.error)
requestAuthenticator.authenticateRequest(request) { [weak self] result in
guard let self else { return }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should call completion with an error when self isn't available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in aa6d8e9.

let useCase: ApplicationPasswordUseCase? = {
if let applicationPasswordUseCase {
return applicationPasswordUseCase
} else if let credentials,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can skip else if let credentials and directly write else if case let .wporg(username, password, siteAddress) = credentials

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 67b9316.

guard let jetpackRequest = request as? JetpackRequest,
jetpackRequest.availableAsRESTRequest,
let useCase = applicationPasswordUseCase,
case let .some(.wporg(_, _, siteAddress)) = credentials else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need some here? Can we write this as case let .wporg(_, _, siteAddress) = credentials

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also updated in 67b9316.

return DotcomValidator()
}

func createRESTRequest(with siteURL: String) -> RESTRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits:

  1. For consistency I would follow asURLRequest and use asRESTRequest as name.
  2. We could return optional here and make availableAsRESTRequest property private. i.e. Check availableAsRESTRequest and return nil if false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point 👍 Updated in e66619a.

@peril-woocommerce
Copy link

Warnings
⚠️ PR is not assigned to a milestone.

Generated by 🚫 dangerJS

@itsmeichigo itsmeichigo added this to the 11.8 milestone Dec 21, 2022
@itsmeichigo itsmeichigo merged commit fd4ec3a into trunk Dec 21, 2022
@itsmeichigo itsmeichigo deleted the feat/8389-network-update-for-rest-api branch December 21, 2022 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: task An internally driven task.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

REST API: Update AlamofireNetwork to handle requests with application password for authentication

4 participants