Skip to content
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

MBL-1159: Exchange temporary token for OAuth token and login #1950

Merged
merged 4 commits into from
Feb 21, 2024

Conversation

amy-at-kickstarter
Copy link
Contributor

@amy-at-kickstarter amy-at-kickstarter commented Feb 20, 2024

📲 What

This implements the MVP of login with OAuth.

It works like this:

  1. Open a web authentication session
  2. If it completes successful, the website redirects to a URL which includes a tempoary authorization code
  3. We exchange that code and a secret for the real OAuth token
  4. We use that OAuth token to fetch the user's information
  5. ...and then save that to the app environment login.

🤔 Why

We are porting the app to use OAuth via a web authentication session to improve security.

👀 See

This snazzy demo!

better.mp4

@amy-at-kickstarter amy-at-kickstarter force-pushed the feat/adyer/MBL-1159/exchange-for-token branch from c60f91a to a66eb25 Compare February 20, 2024 16:43
return OAuth.createAuthorizationSession { [weak self] result in
switch result {
case .loggedIn:
self?.viewModel.inputs.environmentLoggedIn()
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 went back-and-forth on where to put which pieces of logic. I settled on having the actual login call live in OAuth.swift, but the view controller is responsible for updating the UI when the login is complete.

@@ -284,6 +284,9 @@ public protocol ServiceType {
/// Fetch the logged-in user's data.
func fetchUserSelf() -> SignalProducer<User, ErrorEnvelope>

/// Fetch the logged-in user's data.
func fetchUserSelf_combine(withToken: String) -> AnyPublisher<User, ErrorEnvelope>
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 will make more sense in context - AppEnvironment.login takes a token and a user. Our new endpoint returns the new oauth token but not the user; so I needed a way to fetch the current user manually, without actually being logged in. That resulted in this "fetch me the user, given this token" endpoint.

I've discussed this with @santiDotIO and @Arkariang , hopefully we'll be able to get the User in the original exchange call, and then I'll clean this up.

@@ -243,6 +248,9 @@ internal enum Route {
case .userSelf:
return (.GET, "/v1/users/self", [:], nil)

case let .userSelfWithToken(token):
return (.GET, "/v1/users/self", ["oauth_token": token], nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Parameters passed in here will override the defaults, which in this case will be missing the oauth_token.

authenticationError.code == .canceledLogin {
onComplete(.cancelled)
} else {
onComplete(.failure(errorMessage: Strings.Something_went_wrong_please_try_again()))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are all errors that aren't user-solvable, so giving them a generic error message.

func exchangeTokenForOAuthToken(params _: OAuthTokenExchangeParams)
-> AnyPublisher<OAuthTokenExchangeResponse, ErrorEnvelope> {
// TODO: Implement for testing.
Empty(completeImmediately: false).eraseToAnyPublisher()
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 need to come back and evaluate what of this flow I can test. It's all fairly tightly integrated with Apple's authentication controller. I could add tests just for these new endpoints, but since network tests go against these mocks anyways, it doesn't seem super valuable to me...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ifosli @scottkicks I'm open to suggestions for what seems worthwhile to test here.

@amy-at-kickstarter amy-at-kickstarter self-assigned this Feb 20, 2024
@amy-at-kickstarter amy-at-kickstarter marked this pull request as ready for review February 20, 2024 16:55
@amy-at-kickstarter amy-at-kickstarter marked this pull request as draft February 20, 2024 17:22
@amy-at-kickstarter
Copy link
Contributor Author

Converting to draft while I write some tests.

@@ -55,4 +65,67 @@ public struct OAuth {
return nil
}
}

internal static func handleRedirect(
Copy link
Contributor Author

@amy-at-kickstarter amy-at-kickstarter Feb 20, 2024

Choose a reason for hiding this comment

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

Pulled this out into its own method to make it testable, without needing to deal with ASWebAuthenticationSession in the tests. It's not the prettiest interface, but gets the job done.

guard error == nil else {
if let authenticationError = error as? ASWebAuthenticationSessionError,
authenticationError.code == .canceledLogin {
DispatchQueue.main.async {
Copy link
Contributor Author

@amy-at-kickstarter amy-at-kickstarter Feb 20, 2024

Choose a reason for hiding this comment

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

These dispatches are here to prevent us from releasing Zalgo - they ensure that the completion block is only ever called asynchronously, instead of a mix of synchronously and asynchronously.

@amy-at-kickstarter amy-at-kickstarter marked this pull request as ready for review February 20, 2024 19:05
message: errorMessage,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: Strings.login_errors_button_ok(), style: .cancel))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interestingly these strings existed, but were unused 🤷‍♀️

@amy-at-kickstarter amy-at-kickstarter force-pushed the feat/adyer/MBL-1159/exchange-for-token branch 2 times, most recently from c1bf6fa to 6baad91 Compare February 20, 2024 19:15
@testable import Library
import XCTest

final class OAuthTests: XCTestCase {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tests are all a little tautological (they really test the specific internal implementation), but I feel like I'd rather have them than not.

}
.sink { result in
if case let .failure(error) = result {
let message = error.errorMessages.first ?? Strings.login_errors_unable_to_log_in()
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Will the error message here be localized? If not, we may want to just keep the "unable to log in" error, even though it may be less helpful/more generic

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 question. I assumed that server error messages are localized, but I've never actually checked - is that assumption incorrect?

@amy-at-kickstarter amy-at-kickstarter force-pushed the feat/adyer/MBL-1159/exchange-for-token branch from 9aa43d3 to 0917b15 Compare February 21, 2024 19:22
@amy-at-kickstarter amy-at-kickstarter merged commit 03df52c into main Feb 21, 2024
5 checks passed
@amy-at-kickstarter amy-at-kickstarter deleted the feat/adyer/MBL-1159/exchange-for-token branch February 21, 2024 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants