flask-dance icon indicating copy to clipboard operation
flask-dance copied to clipboard

OAuth2 MismatchingStateError when Provider redirects to Consumer with Auth Code

Open xscheck opened this issue 5 years ago • 3 comments

Rather than overloading #293 and per recommendation, opening this thread for the specific issue I am experiencing.

My Android app (Client) is handing all the redirects from my Python REST API server (Consumer) and eventually forwarding the auth request to authenticate to Google (Provider). Using Flask-Dance for the OAuth2 authorization workflow. Here's the debug output, appropriately adjusted to remove sensitive info:

DEBUG:flask_dance.consumer.oauth2:client_id = ... DEBUG:requests_oauthlib.oauth2_session:Generated new state mmMg0Y23gPV9J7OL6dkZdfYLCkQ0wp. DEBUG:flask_dance.consumer.oauth2:state = mmMg0Y23gPV9J7OL6dkZdfYLCkQ0wp DEBUG:flask_dance.consumer.oauth2:redirect URL = https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=...&redirect_uri=http%3A%2F%2Frdfakedomain.com%3A5000%2Flogin%2Fgoogle%2Fauthorized&scope=profile+email&state=mmMg0Y23gPV9J7OL6dkZdfYLCkQ0wp&access_type=offline&prompt=consent

After successfully logging in via a browser popup on Android, Google redirects to Consume using /login/google/authorized, with the correct "state" as provided above. However, the "state" FD has is now different: DEBUG:flask_dance.consumer.oauth2:next_url = /login/success DEBUG:flask_dance.consumer.oauth2:state = WDWsoDGHHrk4sAZn1y1I5c2Xm3fTRp DEBUG:flask_dance.consumer.oauth2:client_id = ... DEBUG:flask_dance.consumer.oauth2:client_secret = ...

And incidentally, no matter how many times I run this test, the "state" where it fails with oauthlib.oauth2.rfc6749.errors.MismatchingStateError is always "WDWsoDGHHrk4sAZn1y1I5c2Xm3fTRp", which is hardly random.

Appreciate any insights.

xscheck avatar Apr 22 '20 03:04 xscheck

Update from investigation. The OAuth2 dance works just fine from a browser. When i compare the debug output, the browser-based interaction is calling my /dashboard API twice, which causes the "state" to be generated twice (the first one is simply "discarded" / doesn't get far enough along). I can put the gory details here if anyone is reviewing this.

Essentially, the browser starts the dance, but does not redirect to https://accounts.google.com... the first time: instead call the REST API again, which starts a "new dance" (new "state" token), and then proceeds to complete the dance, login, retrieve token and render the result from /dashboard.

xscheck avatar Apr 22 '20 21:04 xscheck

I have another data point from my investigation. It seems flask.session is getting clobbered during the OAuth2 dance, between the call to login() and the call to authorized() - it is between these calls the Client successfully logs into the Provider.

In FD's oauth2.py, at the end of login(self), before the return statement, I added a debug statement to print flask.sesion: DEBUG:flask_dance.consumer.oauth2:login-- flask session <SecureCookieSession {'google_oauth_state': 'joR3PLl3fVyajk6DdRqiPmxoTKP4Ti', 'next_url': '/dashboard/1'}>

And added similar at the start of authorized(self), the first statement in this function, produces: DEBUG:flask_dance.consumer.oauth2:authorized-- flask session <SecureCookieSession {'google_oauth_state': 'WDWsoDGHHrk4sAZn1y1I5c2Xm3fTRp', 'next_url': None}>

xscheck avatar Apr 27 '20 06:04 xscheck

Yet another finding, though at this point, I may be in the weeds. I am comparing the flow that I see from the Browser (Safari), which is successful as I stated previously versus the flow from my Android app. One thing consistently missing from the Android flow that I see in the Browser flow is the call to the blueprint's teardown_session.

flask/app.py is calling this function from do_teardown_request(self, exc=_sentinel).

And the teardown_session is missing from Android's flow right after the redirect to Google to authenticate (by popping up a browser from the Android app). I am believing Flask expects a response or such from Android, that it is not receiving - whatever it is is causing Flask to not call do_teardown_request after it sends the https://accounts.google.com/o/oauth2/auth?... request.

Perhaps I am chasing a dead-end?

xscheck avatar May 04 '20 05:05 xscheck