bhubr / react-simple-oauth2-login

Simple React component for OAuth2 login - Supports Authorization Code and Implicit Grant flows.
MIT License
49 stars 31 forks source link

Support cross-origin auth flows by using the postMessage() system #20

Closed rsnyman closed 3 years ago

rsnyman commented 3 years ago

In my scenario, I have an API server on a different domain to my React frontend, and so using the Authorization Code workflow results in DOMExceptions (as seen in #19). By specifying isCrossOrigin and handling the resultant data via postMessage() gets around the issue (and is the recommended way to do it).

rsnyman commented 3 years ago

I'm not very familiar with OAuth2 or the correct way to handle the workflows, so I'll be happy to update this if there's a better way to achieve this. This was just what worked best for me.

bhubr commented 3 years ago

I think the silencing of the DOMException was already there in the original React GitHub Login project (which I almost shamelessly rehashed).

Which is not to say it was a good thing! But I'm not sure it really creates problems for cross-origin apps. I distinctly recall using "my" plugin for an app whose frontend app was deployed on GitHub Pages, while the Node.js backend was deployed on Heroku. Maybe you could tell me how it occurred for you? It's a naive question, I genuinely don't know how/why the original project creator decided to ignore this error.

Aside from that, having experienced that kind of cross-origin iframe issues in another context, I indeed think postMessage() is the best way around it!

rsnyman commented 3 years ago

I encountered this when running my application locally. I have a frontend running on http://localhost:3000 and the API running on http://localhost:8080. According to the browser, these two URLs are different domains (due to the differing port numbers).

In production, my application is running on different subdomains (app.domain.com vs api.domain.com), which browsers also treat as separate domains (see Same-origin Policy).

Whenever the PopupWindow poll method tries to access the content of the popup window, the DOMException is thrown. This results in the poll method being executed continually, but never actually completing, and the user is left with a window that displays the result of the server call, but never closes.

I'm not sure why your example worked, to be honest, perhaps you were using a different (or older) browser? This happens to me in Chrome.

bhubr commented 3 years ago

Thank you for taking the time to put in more details, I really appreciate it!

Indeed, I think your last guess is correct. I recently passed a tech test where I encountered CSP-related issues, which I didn't remember stumbling upon in the past. I guessed it was related to stricter enforcement of CSP by recent browsers, but didn't take time to dig deeper into this.

I have to run my example app again, and I'll probably be able to reproduce what you described. Thanks again!

rsnyman commented 3 years ago

You're welcome!

bhubr commented 3 years ago

I'm gonna ask you yet-more-details, if you don't mind!

I just tested both Implicit Grant and Auth Code flows in my example app, with a Spotify app and a GitHub app respectively... I also made sure I used the latest Chrome version (91.0.4472.77 )... And they worked as they used to. So I'm wondering, maybe there would be something related to a specific OAuth2 provider? Could you just tell me which one you used? If it's a publicly accessible one, I'll setup an OAuth app there, and try & reproduce what you got.

In the meantime, I'm gonna try other common providers, and maybe try all of this in production again, instead of doing it just locally.

rsnyman commented 3 years ago

Not a problem! I was running this against an internal GitLab instance. It's the latest version of GitLab, so if you wanted to try it against gitlab.com, I'm sure that'll work fine.

bhubr commented 3 years ago

Ok, thanks! Guess I'll try with a local, dockerized GitLab, I prefer trying to be as close to your setup as can be.

rsnyman commented 3 years ago

Here's the backend code that is being run: https://github.com/rsnyman/ibutsu-server/blob/login-controller/backend/ibutsu_server/controllers/login_controller.py#L76

The API server does a request to the OAuth2 provider in order to get the user details (so that we can display their name).

I went and had a look at your server code, and my code did that initially too (just returning the response), and the onSuccess was not firing, which is what lead me to discover this problem.

bhubr commented 3 years ago

What leaves me wondering is that (remembering from the top of my head), in an Auth Code workflow, onSuccess is fired before there's been any exchange between the React frontend and your server: that is, when GitLab's OAuth2 server sends back a code via the callback URL.

Maybe it's a bit misleading, because onSuccess would tend to indicate that it's fired when the whole workflow has succeeded, while it's actually fired when the first step of the workflow (obtaining a code) has. Likewise, onError is supposed to be fired when an error occurs during the first step.

Maybe I should emphasize that the error handling of the 2nd step of the OAuth2 Auth Code workflow is left out out the plugin's scope, as of now.

By the way, do really feel free to suggest any other thing that might improve it. It really started out as a "toy project", aiming to make my students' work easier when working on little projects based on APIs that required an access token. I basically just made the initial plugin a bit customizable, instead of being specific to GitHub... But I really didn't put that much time nor thought in making it a production-grade solution. That said, I did use it for small-scale apps in production, and believe it can be useful, so I'm really open to anything making it better and easier to use.

rsnyman commented 3 years ago

What leaves me wondering is that (remembering from the top of my head), in an Auth Code workflow, onSuccess is fired before there's been any exchange between the React frontend and your server: that is, when GitLab's OAuth2 server sends back a code via the callback URL.

This is not the behaviour that I'm seeing. I'll see if I can run your server and if see if I can reproduce the problem with your example server.

bhubr commented 3 years ago

Yep! In the meantime I'm gonna double-check on what I said, about how onSuccess is supposed to work.

bhubr commented 3 years ago

I set up a GitLab CE instance on one of my local computers, and had the Auth Code workflow working with the current version of the module... I tried console.logging the DOMException, but... Nothing! Gotta keep digging I guess!

I might have a "lead": when the OAuth server sends back a code via the callback URL, this redirection happens inside the iframe. What is supposed to happen then is:

  1. extract the code from the URL (as the code query string param for Auth Code flow, behind # in Implicit Grant)
  2. resolve the promise, sending back the extracted code to the parent window
  3. close the popup

I'm just thinking that there might be timing issues, that is, behaviour that might differ, depending on when setInterval triggers its callback. My guess is that there might be cases where the popup's URL still is on the OAuth server, while it tries to communicate with the parent window, which is the React app. Hence on a different origin. That said, I might be completely wrong, so before saying any more dubious stuff, I'm gonna get a good night's sleep!

bhubr commented 3 years ago

OK, tried it again:

  1. logged out from GitLab,
  2. set network to "Slow 3G" in Chrome's dev tools, in the example React app's main window
  3. opened the popup and also set "Slow 3G".

And indeed, I got the dreaded DOMException: the "polling loop" is running while I'm still on GitLab's login screen in the popup, throwing the exception every 500ms. I'm reasonably confident that it's a cross-origin issue between the OAuth2 server in the popup, and the parent window, and that it's quite difficult to avoid. I guess that's why it was silenced by the original author.

Still gotta understand why it kept the whole thing from working on your side. I'll try your app ASAP!

bhubr commented 3 years ago

OK, I think I got it. I think I was so used to my own specific use-case that I made a strong (and wrong) assumption, that your callback URL was on your frontend. You set up the callback URL to point to your server, didn't you? That would explain many things! Sorry it took me so long to realize that, though I think you at least hinted at it.

rsnyman commented 3 years ago

Yes, my callback URL is on my server. Sorry I wasn't clearer! That's why the domain is different.

rsnyman commented 3 years ago

@bhubr OK, I've updated this further. I tried to clarify the README a little further, and I also didn't want the error messages to just be silenced (otherwise users will not know what is happening), so I logged the error. I also specifically handled the cross-origin error and added a message for people to read the documentation for more information.

bhubr commented 3 years ago

Nice! Sorry I haven't been very active these days. Actually I've been trying to gather a few docs about OAuth2 flow, PKCE extension, etc. and what is the recommended way to implement the whole flow.

rsnyman commented 3 years ago

@bhubr is there anything else you'd like me to do with this PR?

WWWilder commented 3 years ago

[replying with a secondary account] Sorry for not getting in touch earlier! I kind of got lost in too many rabbit holes at once. I'll do my best to test it (and possibly amend the examples to showcase the new feature) asap.

tennox commented 3 years ago

I have encountered issues on android apps that open a separate browser app for the OAuth popup, and as it seems the current code does not work. I would guess that the system based on postMessage works better in these situations, so I will try this PR and report back.

tennox commented 3 years ago

Good read for this topic: https://medium.com/@jonnykalambay/progressive-web-apps-with-oauth-dont-repeat-my-mistake-16a4063ce113

tennox commented 3 years ago

So, I tried the PR (can confirm that it works :+1: ) but in all browsers that i've tested where the popup url checking strategy doesn't work, the postMessage strategy doesn't either.

So I might fallback to redirect-strategy afterall, in order to be able to support WebView/ browser apps that don't implement those APIs

bhubr commented 3 years ago

Hi! Just got back from holiday, I'll have a look at it today!

Le mer. 16 juin 2021 à 13:55, Manuel @.***> a écrit :

So, I tried the PR (can confirm that it works 👍 ) but in all browsers that i've tested where the popup url checking strategy doesn't work, the postMessage strategy doesn't either.

So I might fallback to redirect-strategy afterall, in order to be able to support WebView/ browser apps that don't implement those APIs

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/bhubr/react-simple-oauth2-login/pull/20#issuecomment-862316655, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADUQBNXE5T7X74VZXURNELTTTCGMRANCNFSM45QHBZUA .

-- Benoît HUBERT Développeur web : https://developpeur-web-toulouse.fr/ Tél. : 06 61 21 62 12