CMSgov / bluebutton-web-server

Blue Button API
https://sandbox.bluebutton.cms.gov
Other
40 stars 24 forks source link

Issues using developer portal to authorize against sandbox with python oauth client #507

Closed sverchdotgov closed 6 years ago

sverchdotgov commented 6 years ago

This may be a problem with my configuration/client, but I've tried to rule that out as best I can.

This is all using the developer portal to authorize.

I'm using the example here: http://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html#real-example. I copied that directly and it worked with github, but when I try to use that same code with the blue button developer sandbox I get this error:

$ python test_oauth_client.py 
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 780-654-211
127.0.0.1 - - [13/Feb/2018 14:50:37] "GET / HTTP/1.1" 302 -
127.0.0.1 - - [13/Feb/2018 14:50:39] "GET /callback?state=CMFhTJE3spyFfJh9F1dyqVjkuTzmaz&code=XXXXXXXXXXXXXXXX HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1997, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1985, in wsgi_app
    response = self.handle_exception(e)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1540, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/test_oauth_client.py", line 56, in callback
    authorization_response=request.url)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/requests_oauthlib/oauth2_session.py", line 244, in fetch_token
    self._client.parse_request_body_response(r.text, scope=self.scope)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 408, in parse_request_body_response
    self.token = parse_token_response(body, scope=scope)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 379, in parse_token_response
    validate_token_parameters(params)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 386, in validate_token_parameters
    raise_from_error(params.get('error'), params)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/errors.py", line 415, in raise_from_error
    raise cls(**kwargs)
InvalidGrantError: (invalid_grant)

I also intermittently get this:

127.0.0.1 - - [13/Feb/2018 14:54:13] "GET / HTTP/1.1" 302 -
127.0.0.1 - - [13/Feb/2018 14:54:15] "GET /callback?state=32e3bRXegRScJgUPDWT3bId4EUQTNK&code=XXXXXXXXXXXXX HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1997, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1985, in wsgi_app
    response = self.handle_exception(e)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1540, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/test_oauth_client.py", line 53, in callback
    authorization_response=request.url)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/requests_oauthlib/oauth2_session.py", line 244, in fetch_token
    self._client.parse_request_body_response(r.text, scope=self.scope)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 408, in parse_request_body_response
    self.token = parse_token_response(body, scope=scope)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 379, in parse_token_response
    validate_token_parameters(params)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 386, in validate_token_parameters
    raise_from_error(params.get('error'), params)
  File "/home/sverch/USDS/HHS/repos/bluebutton-web-server/env/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/errors.py", line 415, in raise_from_error
    raise cls(**kwargs)
AccessDeniedError: (access_denied)

I tried to change my client_id and client_secret to something obviously broken, and that caused a different error, so I think those are correct.

Here's the code:

from requests_oauthlib import OAuth2Session
from flask import Flask, request, redirect, session, url_for
from flask.json import jsonify
import os

app = Flask(__name__)

# https://stackoverflow.com/questions/27785375/testing-flask-oauthlib-locally-without-https
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

client_id = "XXXX"
client_secret = "XXXX"
authorization_base_url = 'https://sandbox.bluebutton.cms.gov/v1/o/authorize/'
token_url = 'https://sandbox.bluebutton.cms.gov/v1/o/token/'

@app.route("/")
def demo():
    """Step 1: User Authorization.

    Redirect the user/resource owner to the OAuth provider (i.e. Github)
    using an URL with a few key OAuth parameters.
    """
    github = OAuth2Session(client_id)
    authorization_url, state = github.authorization_url(authorization_base_url)

    # State is used to prevent CSRF, keep this for later.
    session['oauth_state'] = state
    return redirect(authorization_url)

# Step 2: User authorization, this happens on the provider.

@app.route("/callback", methods=["GET"])
def callback():
    """ Step 3: Retrieving an access token.

    The user has been redirected back from the provider to your registered
    callback URL. With this redirection comes an authorization code included
    in the redirect URL. We will use that to obtain an access token.
    """

    github = OAuth2Session(client_id, state=session['oauth_state'])
    token = github.fetch_token(token_url, client_secret=client_secret,
                               authorization_response=request.url)

    # At this point you can fetch protected resources but lets save
    # the token and show how this is done from a persisted token
    # in /profile.
    session['oauth_token'] = token

    return redirect(url_for('.profile'))

@app.route("/profile", methods=["GET"])
def profile():
    """Fetching a protected resource using an OAuth 2 token.
    """
    github = OAuth2Session(client_id, token=session['oauth_token'])
    return jsonify(github.get('http://localhost:8000/connect/userinfo').json())

if __name__ == "__main__":
    # This allows us to use a plain HTTP callback
    os.environ['DEBUG'] = "1"

    app.secret_key = os.urandom(24)
    app.run(debug=True)
aviars commented 6 years ago

@sverchdotgov This should work. The testclient uses basicly the same setup you have here in Flask. there could be issues with the server.,,,,investigating. In the meantime, do you have better luck with this?

https://github.com/CMSgov/bluebutton-sample-client-django

sverchdotgov commented 6 years ago

Thanks @aviars! I tried that out and it did work.

Looking at the URL that it sends me to for in the sandbox, I see that the django version that does work includes "redirect_uri", but my example does not. Taking out "redirect_uri" does in fact cause an "auth forbidden" error.

sverchdotgov commented 6 years ago

Hmm, although I explicitly added the redirect_uri and still get the access denied error.

sverchdotgov commented 6 years ago

So I was wondering why github worked without the redirect_uri, and found https://tools.ietf.org/html/rfc6749#section-3.1.2.3:

If multiple redirection URIs have been registered, if only part of the redirection URI has been registered, or if no redirection URI has been registered, the client MUST include a redirection URI with the authorization request using the "redirect_uri" request parameter.

sverchdotgov commented 6 years ago

@whytheplatypus ran this locally and figured out that you need to pass redirect_uri in every instantiation of the oauth session object. I tried that and it works now! Thanks @whytheplatypus!

sverchdotgov commented 6 years ago

Talked with @whytheplatypus in hipchat about the requirement of redirect_uri. Here's the summary:

Registering the redirect URIs at application registration time is what provides the security benefit, since that means attackers can't just redirect clients to random places. So requiring the redirect_uri when there's only one registered doesn't provide extra security because we can redirect to the known good place.

However, requiring it all the time does have the major downside that the python requests oauth client defaults to not sending it, which may make new users think the API is just completely broken (when the real thing is that it is not matching the spec in this specific case). That seems like it makes it a bigger deal to remove this requirement. @samgensburg-gov Thoughts? Should I file an issue somewhere?

samgensburg-gov commented 6 years ago

We fixed the redirect_uri requirement a while back, so we should be good.