-
Notifications
You must be signed in to change notification settings - Fork 121
REST API: Update AlamofireNetwork to support authentication with application passwords
#8402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
You can test the changes from this Pull Request by:
|
…ation password is blocked
…icatedState" This reverts commit 17cae6a.
| return | ||
| } | ||
| let applicationPasswordUseCase = TemporaryApplicationPasswordUseCase(siteID: siteID, credentials: credentials) | ||
| requestAuthenticator.updateApplicationPasswordHandler(with: applicationPasswordUseCase) |
There was a problem hiding this comment.
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,
RequestAuthenticatorowns the usecase and we can also let it create the usecase.AlamofireNetworkdoesn'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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| let username = "username" | ||
| let password = "password" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| 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)) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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. 👏
|
@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 } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nits:
- For consistency I would follow
asURLRequestand useasRESTRequestas name. - We could return optional here and make
availableAsRESTRequestproperty private. i.e. CheckavailableAsRESTRequestand return nil if false.
There was a problem hiding this comment.
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.
Generated by 🚫 dangerJS |
Closes: #8389
Description
This PR updates the Networking layer to support authentication with application passwords:
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.RequestAuthenticatorto 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.AlamofireNetworkwith a request authenticator to authenticate requests before sending them.UpdatedDefaultStoresManagerandAuthenticatedStateto send an updated site ID toAlamofireNetworkwhen 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
RELEASE-NOTES.txtif necessary.