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

feat: Support web-based LMS OAuth. #65

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

Kelketek
Copy link

Description

This pull request makes it possible to use the LMS's web login rather than the app's login form. This is necessary in the case that the client institution is using a custom authentication provider, and must piggy-back off the LMS's authentication flow to avoid writing a whole new custom login interface for these providers.

Testing instructions

The best way to test this is to use the version of the app built for Yam education.

  1. Register here: https://app.dev.yam-edu.com/
  2. Then load up this PR and build it: Yam Customizations yameducation/educationx-app-ios#2
  3. Load the app. You should immediately be brought to the login page.
  4. Login, and verify the app's main interface loads.
  5. Alternatively, hit Done to be brought back to a minimalist login page that will allow you to jump back into the authentication workflow.

Deadline

None

Author Concerns

One thing that's not working quite right in our client's application build is logout. This is because while the existing logout does, in fact, expire the LMS token, it doesn't log out the user from Auth0, the provider they're using. This means hitting the sign in button will log you right back in without prompting you for the password once more. It turns out that on the LMS, the logout button has a 'next' variable to a view that also logs the user out from Auth0. I've not yet determined if this is something we should also add as a tuneable variable-- having some custom logout URL. I'd have to see how that was implemented first.

My suspicion is that we should actually change what the LMS is doing rather than adding some additional logout action into the app code, but I'm not yet sure. cc @Cup0fCoffee

Reviewers

@Kelketek Kelketek requested a review from a team as a code owner August 17, 2023 00:26
.padding(.vertical, 40)
if viewModel.forceWebLogin {
// On first load, we should bring the user right to the web login
// interface rather than showing this view.

Choose a reason for hiding this comment

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

What do you think about this: instead of this boolean forceWebLogin which special-cases one login method, could we have a setting called presumeLoginMethod or forceLoginMethod which can optionally be set to one of the LoginMethod enum values?

Whether you call it presumeLoginMethod or forceLoginMethod depends on whether you allow the user to go "Back" and choose a different method, or if this is the only option available to them. Though I suppose the "force" could be achieved simply by disabling all but one of the login methods.

Copy link
Author

@Kelketek Kelketek Aug 18, 2023

Choose a reason for hiding this comment

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

@bradenmacdonald

I'm not sure how I feel about this suggestion-- for two reasons. The first is that there's a rendering effect I'm concerned about here. The forceWebLogin method is there not only to bring the user to the web authentication page, but to make sure that, on initial load, the sign in page isn't drawn. There needs to be some way to signal to the view not to render the button or text if we're going to be opening the Safari browser to perform the login. Otherwise you get this visual glitch where you launch the app, see the sign-in page, and then the browser opens, which can be alarming and confusing-- I know that if I saw that as a user, I'd wonder if I pressed something I shouldn't or something happened I didn't want.

What you suggest could notionally be compatible with that first reason, but it would still require special casing the web login because, presumably, any other login method would have its interfaces written into the Swift view, and thus would need to be hidden if we're presuming the web login.

The second reason I'm not sure about your suggestion is that I'm not sure any other login methods, other than direct login, are actually viable outside of a web view, or wouldn't otherwise require some other complex changes to the UI. For example if you were to sign in with Google, you'd still need to visit the LMS login page to do the same dance we're doing for the client with Auth0. Same with any SAML provider, as far as I can tell-- the existing login view assumes that the user will be logging into the LMS using a post to Django directly, but nearly all external authentication methods will require visiting some intermediary web page and getting redirected back, requiring a Safari window, or else some tighter, complex iOS integration I'm unaware of.

For reference, I initially attempted to do the integration following Auth0's guide on integrating with iOS. However I realized as I was working with it that while I could authenticate with Auth0, the tokens it would provide me only worked with their servers directly. I couldn't get a token for the LMS without visiting the LMS and getting redirected from it to a login page provided by Auth0. There might be some other way to do it but it escapes me for now, and I suspect this is the way it's intended to be done. After all, all the MFEs at present just redirect to the LMS for login and then redirect you back, rather than presenting a streamlined login view.

@Kelketek
Copy link
Author

@bradenmacdonald I've responded to one comment and addressed the other.

Today I attempted to work on the tests, but it looks like I can't run them properly on non-Apple silicon. The tests rely on autogenerated mocks which need to be refreshed in order to update them and run, and the Podfile installs a dependency for generating these-- SwiftyMocky, which is prebuilt for Apple Silicon only.

I attempted to compile it for x86_64 and it did compile, but when it ran on the needed directory, it failed compiling on some dependency that it only pulls in and builds at runtime. Searching around for the cause didn't yield much useful info-- I'd have to spelunk way down the rabbit hole to fix it, so it might just be best to find someone else with Apple Silicon who can complete the test portion or ask RaccoonGang for help here.

@Kelketek Kelketek changed the title [WIP] feat: Support web-based LMS OAuth. feat: Support web-based LMS OAuth. Aug 18, 2023
@Kelketek
Copy link
Author

@bradenmacdonald Unmarking as WIP because although it probably can't be merged as-is, it does need upstream feedback from RaccoonGang before it can be moved forward.

cc @IvanStepanok @volodymyr-chekyrta If either of you has some time :) Thanks!

Copy link
Contributor

@volodymyr-chekyrta volodymyr-chekyrta left a comment

Choose a reason for hiding this comment

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

Hi, thank you for your efforts and input!
And I apologize for the delayed review.
This feature looks quite complex to me, so let's take it step by step.

Could we please start with changing the target branch to develop, I know now it's a bit confusing compared to the legacy repos, but we would like to follow the git flow with the main and develop branches.
I'm going to describe this approach in the repos asap or we'll do it as part of the OEP-64.

Comment on lines 76 to 135
func login(viewController: UIViewController) async {
/* OAuth web login. Used when we cannot use the built-in login form,
but need to let the LMS redirect us to the authentication provider.

An example service where this is needed is something like Auth0, which
redirects from the LMS to its own login page. That login page then redirects
back to the LMS for the issuance of a token that can be used for making
requests to the LMS, and then back to the redirect URL for the app. */
self.safariDelegate = WebLoginSafariDelegate(viewModel: self)
oauthswift = OAuth2Swift(
consumerKey: config.oAuthClientId,
consumerSecret: "", // No secret required
authorizeUrl: "\(config.baseURL)/oauth2/authorize/",
accessTokenUrl: "\(config.baseURL)/oauth2/access_token/",
responseType: "code"
)

oauthswift!.allowMissingStateCheck = true
let handler = SafariURLHandler(
viewController: viewController, oauthSwift: oauthswift!
)
handler.delegate = self.safariDelegate
oauthswift!.authorizeURLHandler = handler

// Trigger OAuth2 dance
guard let rwURL = URL(string: "\(Bundle.main.bundleIdentifier ?? "")://oauth2Callback") else { return }
oauthswift!.authorize(withCallbackURL: rwURL, scope: "", state: "") { result in
switch result {
case .success(let (credential, _, _)):
Task {
self.webLoginAttempted = true
let user = try await self.interactor.login(credential: credential)
self.analytics.setUserID("\(user.id)")
self.analytics.userLogin(method: .oauth2)
self.router.showMainScreen()
}
// Do your request
case .failure(let error):
self.webLoginAttempted = true
self.isShowProgress = false
self.errorMessage = error.localizedDescription
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This function can be problematic for several reasons.

  1. We are trying to encapsulate all navigation and ViewControllers logic inside of the Router entity.
    This helps us to free ourselves from UIKit, navigation logic and be able to change the navigation framework at any time and write tests easily.
    We could play with it and ask Router to provide us with configured SafariURLHandler instance or something like that.
  2. I recommend converting the closure style function
    oauthswift!.authorize(withCallbackURL: rwURL, scope: "", state: "") { result in
    to async style function
    await oauthswift!.authorize(...) or await oauthswift!.authorizeTask(...)
    we can reach it with some extension function and wrap oauthswift!.authorize to the withCheckedThrowingContinuation call.
  3. It's better to remove Task {} creating from the async function as it makes it hard/impossible to test with Unit testing.
  4. Last but not least, could you please extend the SignInViewModelTests.swift with tests for your new functions? This is a crucial part of the application's stability.
    I highly recommend running the generateAllMocks.sh script before you start creating tests. This script creates all mocks for tests if its need.

If you need any help, hit me on Slack or email me 📪

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, @volodymyr-chekyrta ! This is great feedback. I'll ping you as I make progress on it, as this appears to be the bulk of the changes to be made.

self.validator = validator
self.webLoginAttempted = false
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it needed to initialize webLoginAttempted with false on init if the variable is already initialized var webLoginAttempted: Bool = false?

Copy link
Author

Choose a reason for hiding this comment

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

Nope-- that got left in when I was trying a few things. I'll remove it.

@volodymyr-chekyrta volodymyr-chekyrta added the enhancement Relates to new features or improvements to existing features label Aug 21, 2023
Comment on lines 31 to 38
if viewModel.forceWebLogin {
// On first load, we should bring the user right to the web login
// interface rather than showing this view.
//
// If that login fails or the user escapes back, they'll be brought
// back to the view where any error message will be shown.
Task {
await webLogin()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

In my mind, a more SwiftUI way will put this code to the View .onAppear {}
What do you think about it?

Copy link
Author

Choose a reason for hiding this comment

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

@volodymyr-chekyrta I'm quite new to Swift, SwiftUI, and its idioms. This is my first Swift code, in fact :)

So, I couldn't tell you for sure. But I've looked up the docs, and I think you're probably right! I'll add this.

@volodymyr-chekyrta
Copy link
Contributor

Today I attempted to work on the tests, but it looks like I can't run them properly on non-Apple silicon. The tests rely on autogenerated mocks which need to be refreshed in order to update them and run, and the Podfile installs a dependency for generating these-- SwiftyMocky, which is prebuilt for Apple Silicon only.

I attempted to compile it for x86_64 and it did compile, but when it ran on the needed directory, it failed compiling on some dependency that it only pulls in and builds at runtime. Searching around for the cause didn't yield much useful info-- I'd have to spelunk way down the rabbit hole to fix it, so it might just be best to find someone else with Apple Silicon who can complete the test portion or ask RaccoonGang for help here.

@Kelketek if you have a non-Apple Silicon chip, try this
https://github.com/MakeAWishFoundation/SwiftyMocky#2-installing-swiftymocky-cli
Just run SwiftyMocky commands with Mint (tested on Intel i5)

@Kelketek Kelketek changed the base branch from main to develop August 21, 2023 21:40
@touchapp touchapp requested a review from rnr November 20, 2023 14:24
@e0d
Copy link

e0d commented Nov 21, 2023

@Kelketek was a product ticket created for this work? We're pushing to have all PRs tie back to a product ticket that is, ideally, prioritized before the development happens.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Nov 21, 2023
@openedx-webhooks
Copy link

Thanks for the pull request, @Kelketek! Please note that it may take us up to several weeks or months to complete a review and merge your PR.

Feel free to add as much of the following information to the ticket as you can:

  • supporting documentation
  • Open edX discussion forum threads
  • timeline information ("this must be merged by XX date", and why that is)
  • partner information ("this is a course on edx.org")
  • any other information that can help Product understand the context for the PR

All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here.

Please let us know once your PR is ready for our review and all tests are green.

@Kelketek
Copy link
Author

@e0d No, no product ticket was created for this work. It was discovered as necessary for a client when preparing a copy of the app for them. We did get the build working for them with these customizations, but due to other snags there is no anticipation of continuing development on this at this time.

Still, this is likely to be a fairly important thing to have for any team which is using anything other than direct auth to the platform (so, pretty much all SSO users). It could be rolled into this existing ticket, which this fix would solve.

To whoever that person is, there's one other bug that showed up later: It doesn't look like this implementation actually lets you log out. Likely, what is needed is the ability to clear all Safari state for the app in order to perform the logout. However I'm unfamiliar with the ecosystem to know how to do this off-hand (this is my first foray into Swift.)

@xitij2000 worked on a similar fix for Android. I believe it has the same features and limitations.

@mphilbrick211 mphilbrick211 added the product review PR requires product review before merging label Dec 11, 2023
@Kelketek
Copy link
Author

@e0d Looks like we've got another client who needs this, so I'm continuing work on it. I've created an issue here: #212

Let me know if you need anything else on that front.

@Kelketek Kelketek force-pushed the web_oauth branch 3 times, most recently from ee46d1a to 14ad52e Compare December 15, 2023 22:57
@marcotuts
Copy link

A few notes @Kelketek on my end - This is good to go on the product review side, though I would suggest that this be set up as a configurable feature flag defaulted to being off. This will keep the native login / register as the default in the mobile applications.

Additionally, there may be some need for conflict resolution and rebasing for the review to continue. @volodymyr-chekyrta can review again once these items are addressed (and any other items as well that came up in the review above that may be outstanding. )

thanks!

@marcotuts marcotuts self-requested a review April 3, 2024 15:18
Copy link

@marcotuts marcotuts left a comment

Choose a reason for hiding this comment

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

Product review only 👍

@volodymyr-chekyrta
Copy link
Contributor

@Kelketek, could you please resolve conflicts so I can proceed with review?

@Kelketek
Copy link
Author

Hi @volodymyr-chekyrta ! We're currently discussing internally how to handle the budget for finishing this PR. For context, we've now had two different clients who have needed this but haven't yet secured enough from either of them to get it across the finish line. We're trying to figure out what to do about it and how to get buy-in for the finishing work.

I'll update you as soon as I have a prediction on when we'll be able to rebase this and address your notes.

@volodymyr-chekyrta
Copy link
Contributor

Hi @volodymyr-chekyrta ! We're currently discussing internally how to handle the budget for finishing this PR. For context, we've now had two different clients who have needed this but haven't yet secured enough from either of them to get it across the finish line. We're trying to figure out what to do about it and how to get buy-in for the finishing work.

I'll update you as soon as I have a prediction on when we'll be able to rebase this and address your notes.

Makes sense to me 👍
Please let us know if we can help you from the engineering or any other perspective.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Relates to new features or improvements to existing features open-source-contribution PR author is not from Axim or 2U product review PR requires product review before merging
Projects
Status: Product Review
Development

Successfully merging this pull request may close these issues.

7 participants