Skip to content

Conversation

@selanthiraiyan
Copy link
Contributor

@selanthiraiyan selanthiraiyan commented Dec 27, 2022

Closes: #8482

Description

As part of the REST API project we need the capability to retry a request in case of authentication failure.

The flow for a REST API request is described below,

  1. Send a request
  2. If an error occurs due to authentication failure for a REST request.
  3. Regenerate a new application password.
  4. Retry the REST API request again.

The request adapt and retry implementation is followed from https://github.com/wordpress-mobile/WordPressKit-iOS/blob/trunk/WordPressKit/Authenticator.swift

There are no changes to how a Jetpack or WPCOM request is handled. They will be authenticated using the new RequestAuthenticator. But there will be no retrying.

UI tests

As the network started validating the requests using Alamofire's validate method like this, the UI tests started failing for mocks without headers section.

The UI tests were failing due to incorrect Content-Type in the response. The headers section takes care of adding the Content-Type to the mock response.

To fix this, I added the headers part for all missing mock JSON files.

Testing instructions

  • Login into the app using WPCOM credentials. (Both Email and Store Address flows)
  • Do smoke testing to ensure that everything works as before.
  • We don't have any REST API requests migrated. So there is no testing required. Coupons endpoints will be migrated in a future PR.

Screenshots

NA


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

@selanthiraiyan selanthiraiyan added type: task An internally driven task. feature: login Related to any part of the log in or sign in flow, or authentication. labels Dec 27, 2022
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Dec 27, 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 pr8483-c66416d on your iPhone

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

@selanthiraiyan selanthiraiyan added this to the 11.8 milestone Dec 27, 2022
@selanthiraiyan selanthiraiyan marked this pull request as ready for review December 28, 2022 01:02
@selanthiraiyan selanthiraiyan requested a review from a team as a code owner December 28, 2022 01:02
@jaclync jaclync self-assigned this Dec 28, 2022
Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

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

Hi @selanthiraiyan 👋 Sorry for some preliminary questions before diving into deeper code review, since I don't know much about this project. Trying to clarify a few things:

  • What does the user provide when signing in with the application password for the REST API feature?
  • From the issue description:

WPOrg REST API requests should be authenticated using application password. If there is an authentication failure, we should generate application password and retry the REST API request.

What can trigger the original application password to fail/change, is it a common scenario? Just trying to understand why we need to handle this API failure case only for this new authentication method.

  • What does the adapter do? I'm not very familiar with the technical requirements, just wanted to get a full understanding of the changes

If there's a p2 or reference that explains the answers to my questions above, feel free to just drop the link here 🙂

@peril-woocommerce
Copy link

Warnings
⚠️ PR has more than 500 lines of code changing. Consider splitting into smaller PRs if possible.

Generated by 🚫 dangerJS

@selanthiraiyan
Copy link
Contributor Author

Thanks for the review, @jaclync!

I have addressed your comments. The PR is ready for the second pass of review. 🙏

@jaclync
Copy link
Contributor

jaclync commented Dec 29, 2022

https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/ has the details and screenshot about how the UI looks in wp-admin.

From the wp-admin UI here, it looks like it shows the name of the app with a CTA to revoke. If the user chooses to revoke the application password here, it feels like they intentionally want to remove access from the app? If that's the case, then it feels a bit intrusive how the app just creates another password. I'm sure this has been thought out before, just sharing my observations from the review.

Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

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

I think I have more understanding of the PR now 😄 thanks for the updates and explanations, I took another pass with some questions/suggestions.

let credentials: Credentials?

func convert(_ request: URLRequestConvertible) -> URLRequestConvertible {
guard let jetpackRequest = request as? JetpackRequest,
Copy link
Contributor

Choose a reason for hiding this comment

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

just wondering, do we only do the conversation from JetpackRequest? How about DotcomRequest and WordPressOrgRequest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. DotcomRequest are sent to WordPress.com servers, and it needs WPCOM authentication token. We cannot talk to WPCOM servers using the application password.
  2. WordPressOrgRequest will be sent through WordPressOrgNetwork instead of AlamofireNetwork.
    • WordPressOrgNetwork solely handles WordPressOrgRequest, and it doesn't use RequestConverter.

Copy link
Contributor

Choose a reason for hiding this comment

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

TIL, thanks for the explanation. Now I have another question 😅 what are the differences between WordPressOrgRequest and RESTRequest? from the asURLRequest implementation in both structs, the way it sets the URL feels pretty similar 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

WordPressOrgRequest talks to WordPress in general. https://developer.wordpress.org/rest-api/
RESTRequest talks to WooCommerce plugin. https://woocommerce.com/document/woocommerce-rest-api/

Just chiming in to clarify: I believe RESTRequest can be used to handle requests to all WordPress org and Woo requests. The difference between these two types is the way authentication is handled:

  • WordPressOrgRequest uses cookie authentication.
  • RESTRequest uses application password.
    We should probably come up with better naming for these and update the documentation for clearer explanations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the details, Huong!

RESTRequest has this extra parameter/value let wooApiVersion: WooAPIVersion when compared to WordPressOrgRequest.
I thought the wooApiVersion parameter makes it clear that we should use RESTRequest for the WooCommerce plugin's REST API-related requests. I looked again now and noticed that WooAPIVersion has a case none which is an empty string.

We should probably come up with better naming for these and update the documentation for clearer explanations.

Maybe we could have a single struct instead of two and use the WooAPIVersion.none case. What do you think?

I will create an issue after we finish discussing it here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we could have a single struct instead of two and use the WooAPIVersion.none case. What do you think?

This makes sense to me! A few things I notice:

  • We're using WordPressApiValidator in WordPressOrgRequest but this is not necessary. Since WordPressOrgRequest is required to conform to Request, we can use PlaceholderDataValidator instead.
  • WordPressOrgRequest is not using JSON encoding for POST and PUT requests like RESTRequest does, so unifying them with RESTRequest implementation will fix this too.

Let's use RESTRequest for both then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created an issue here #8512

}
case .failure(let error):
completion(nil, error)
let request = requestConverter.convert(request)
Copy link
Contributor

Choose a reason for hiding this comment

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

I see, now I understand why it has to be converted here - URLRequestConvertible is only available here and not after sessionManager.request(request) where the retry takes place, and the conversion requires the site URL from the credentials so it's not possible at the JetpackRequest call sites. I can't think of other solutions given these constraints, the only thought I had is to move the authentication step to the URLRequestConvertible level but I'm not sure if that works with the retry and what's the best practice.

@selanthiraiyan
Copy link
Contributor Author

From the wp-admin UI here, it looks like it shows the name of the app with a CTA to revoke. If the user chooses to revoke the application password here, it feels like they intentionally want to remove access from the app? If that's the case, then it feels a bit intrusive how the app just creates another password. I'm sure this has been thought out before, just sharing my observations from the review.

I started an internal discussion about this, and we will update this if we decide to follow a different flow. p1672283352691059-slack-C046HDZL87J

@selanthiraiyan
Copy link
Contributor Author

Thanks, @jaclync. I addressed your comments. Kindly take another look when you can. 🙏

Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

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

Thanks for all the updates and clarifications, LGTM now 👍 sorry it took a while, since the changes are now 1000+ 😆

One non-blocking suggestion is to unit test RequestProcessor on the retry and adaption behavior. given the current PR size, feel free to consider it for the future.

Comment on lines +26 to +28
return try? DefaultApplicationPasswordUseCase(username: username,
password: password,
siteAddress: siteAddress)
Copy link
Contributor

Choose a reason for hiding this comment

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

does it make sense to validate these two URLs during the login step so that we don't have to handle this error scenario here? If the URLs are invalid from the beginning, the app doesn't seem usable since it can never generate an application password.

3. **RESTRequest**

Represents a REST API request which will be used to contact to the site directly. (Skipping Jetpack tunnel)
These requests are authenticated using application passwords.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe can mention AuthenticatedRESTRequest here?

Suggested change
These requests are authenticated using application passwords.
These requests are then authenticated using an application password using `AuthenticatedRESTRequest`.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 9464bd1

Comment on lines 184 to 185
Injects application password and a custom user-agent header into anything that conforms to the URLConvertible protocol. Usually wraps up
a RESTRequest.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "anything that conforms to the URLConvertible protocol" might not hold anymore?

Suggested change
Injects application password and a custom user-agent header into anything that conforms to the URLConvertible protocol. Usually wraps up
a RESTRequest.
Injects application password and a custom user-agent header into anything that conforms to the URLConvertible protocol and can be authenticated with an application password. For example, a RESTRequest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 9464bd1

func test_jetpack_request_is_returned_when_credentials_not_available() {
// Given
let converter = RequestConverter(credentials: nil)
let jetpackRequest = JetpackRequest(wooApiVersion: .mark1, method: .get, siteID: 123, path: "test", availableAsRESTRequest: false)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: does it make sense to set availableAsRESTRequest to true to enable this conversion in any case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in d0b7993

Comment on lines 14 to 15
let request = try converter.convert(jetpackRequest).asURLRequest()
let updatedRequest = try authenticator.authenticate(request)
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 the conversion necessary since availableAsRESTRequest is false already and RequestConverter has been tested separately?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 7ccf5a4

let updatedRequest = try authenticator.authenticate(request)

// Then
XCTAssertTrue(updatedRequest is UnauthenticatedRequest)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: does it make sense to recover this assertion? or has the type of request changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We will be working with URLRequest here. So I had to remove the type checking the assertion and check the header fields directly instead.

authenticator.authenticateRequest(request) { result in
updatedRequest = try? result.get()
}
let request = try converter.convert(jetpackRequest).asURLRequest()
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: similar to before, the request can be passed directly (JetpackRequest or RESTRequest) without the converter so that this is testing just one thing (RequestAuthenticator)

Same for other test cases in this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point. Done in 7ccf5a4

Comment on lines 36 to 40
guard case let .wpcom(_, authToken, _) = credentials else {
XCTFail("Missing credentials.")
return
}

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: do we still need Credentials? it looks like we can just pass an auth token string to the next line directly.

Same for other test cases in this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in c66416d

@selanthiraiyan
Copy link
Contributor Author

Thank you for your patience and the detailed review, Jaclyn! ❤️ 🙇
I have addressed your recent comments, and I will go ahead and merge this now. Kindly take another look if you get the time. 🙏

One non-blocking suggestion is to unit test RequestProcessor on the retry and adaption behavior. given the current PR size, feel free to consider it for the future.

I created an issue for this. #8507

@selanthiraiyan selanthiraiyan merged commit fe3f198 into trunk Dec 30, 2022
@selanthiraiyan selanthiraiyan deleted the feat/8482-alamofire-retry-adapt-request branch December 30, 2022 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: login Related to any part of the log in or sign in flow, or authentication. type: task An internally driven task.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REST API] Introduce retrier and adaptor for AlamofireNetwork

5 participants