Include authorization in a oauth2session for requests-oauthlib - python

From reading various documents it seems like authorization is optionally required by oauth2 providers for refresh token requests. I'm working with the FitBit API that appears to require authorization.
I'm following the instructions here for refreshing a token with requests-oauthlib:
https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#refreshing-tokens
Some setup code (not what I am using, but you get the idea:
>>> token = {
... 'access_token': 'eswfld123kjhn1v5423',
... 'refresh_token': 'asdfkljh23490sdf',
... 'token_type': 'Bearer',
... 'expires_in': '-30', # initially 3600, need to be updated by you
... }
>>> client_id = r'foo'
>>> refresh_url = 'https://provider.com/token'
>>> protected_url = 'https://provider.com/secret'
>>> # most providers will ask you for extra credentials to be passed along
>>> # when refreshing tokens, usually for authentication purposes.
>>> extra = {
... 'client_id': client_id,
... 'client_secret': r'potato',
... }
>>> # After updating the token you will most likely want to save it.
>>> def token_saver(token):
... # save token in database / session
from requests_oauthlib import OAuth2Session
client = OAuth2Session(client_id, token=token, auto_refresh_url=refresh_url,
auto_refresh_kwargs=extra, token_updater=token_saver)
r = client.get(protected_url)
However, with this call I'm getting:
MissingTokenError: (missing_token) Missing access token parameter.
I know my token is expired, but why isn't the refresh working?

The library is broken in this regard. See #379.
You can work around it something like this:
def _wrap_refresh(func):
def wrapper(*args, **kwargs):
kwargs['auth'] = (client_id, client_secret)
kwargs.pop('allow_redirects', None)
return func(*args, **kwargs)
return wrapper
client = OAuth2Session(client_id, token=token,
auto_refresh_url=refresh_url,
token_updater=token_saver)
client.refresh_token = _wrap_refresh(client.refresh_token)

Edit: There is still some useful info below but overriding the auth function means that my actual API requests are now failing (i.e. below is not a correct answer) I'm not sure how I got the one request I tried last time to work. It may have just returned an error (in json) rather than throwing an error, and I just assumed no raised error meant it was actually working. See a correct workaround by OrangeDog (until the library is fixed).
Well, I examined the FitBit server response, just before the MissingTokenError was being thrown. It turns out I was getting an error saying that the authentication was incorrect.
This is perhaps a useful point on its own to dwell on for a sec. The MissingTokenError seems to occur when the response doesn't contain the expected token. If you can debug and look at the response more closely, you may find the server is providing a bit more detail as to why your request was malformed. I went to the location of the error and added a print statement. This allowed me to see the JSON message from FitBit. Anyway, this approach may be useful for others getting the MissingTokenError.
if not 'access_token' in params:
print(params)
raise MissingTokenError(description="Missing access token parameter.")
Anyway, after some further debugging, the authentication was not set. Additionally my client id and secret were being posted in the body (this may not have been a problem). In the FitBit examples, the client id and secret were not posted in the body but were passed via authentication. So I needed the client to pass authentication to FitBit.
So then the question was, how do I authenticate. The documentation is currently lacking in this respect. However, looking at the session object I found a .auth property that was being set and a reference to an issue (#278). In that issue a workaround is provided (shown below with my code) for manual authentication setting: https://github.com/requests/requests-oauthlib/issues/278
Note, the oauth session inherits from the requests session, so for someone that knows requests really well, this may be obvious ...
Anyway, the solution was just to set the auth parameter after initializing the session. Since FitBit doesn't need the client id and secret in the body, I removed passing in the extras as well (again, this may be a minor issue and not really impact things):
import os
import json
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
client_id = ""
client_secret = ""
with open("tokens.json", "r") as read_file:
token = json.load(read_file)
save_file_path = os.path.abspath('tokens.json')
refresh_url = 'https://api.fitbit.com/oauth2/token'
def token_saver(token):
with open(save_file_path, "w") as out_file:
json.dump(token, out_file, indent = 6)
#Note, I've removed the 'extras' input
client = OAuth2Session(client_id, token=token, auto_refresh_url=refresh_url, token_updater=token_saver)
#This was the magic line ...
auth = HTTPBasicAuth(client_id, client_secret)
client.auth = auth
url = 'https://api.fitbit.com/1.2/user/-/sleep/date/2021-01-01-2021-01-23.json'
wtf = client.get(url)
OK, I think I copied that code correctly, it is currently a bit of a mess on my end. The key part was simply the line of:
client.auth = auth
After the client was initiated.
Note, my token contains an expires_at field. I don't think the session handles the expires_in in terms of exact timing. In other words, I think expires_in only causes a refresh if its value is less than 0. I don't think it looks at the time the object was created and starts a timer or sets a property to know what expires_in is relative to. The expires_at field on the other hand seems to provide (I think) a field that is checked to ensure that the token hasn't expired at the time of the request, since expires_at is a real world, non-relative, time. Here's my token dict (with fake tokens and user_id):
{'access_token': '1234',
'expires_in': 28800,
'refresh_token': '5678',
'scope': ['heartrate',
'profile',
'settings',
'nutrition',
'location',
'weight',
'activity',
'sleep',
'social'],
'token_type': 'Bearer',
'user_id': 'abcd',
'expires_at': 1611455442.4566112}

Related

Stormpath failed to find resource

In order to authenticate in Flask with Stormpath, the user sends an access token in the request's header.
headers = {
'Authorization': 'Bearer <token_id>'
}
I validate it with:
authenticator = ApiRequestAuthenticator(application)
uri = 'bla_dont_care.com'
result = authenticator.authenticate(headers=request.headers, http_method='GET', uri=uri, body={}, scopes=[])
is_valid = result.account is not None
The authenticator is not being initiated in every request but saved in memory.
My response time raised from 40ms to 450ms. I read that the Stormpath SDK caches the API calls specifically for this.
Trying to debug this, I did see that the cache get 2 misses per 1 request here. The key it failes to find is https://api.stormpath.com/v1/applications/VJiXDWTWnYdNgrFe9BoI3/accounts/6TGIMWF47DO0XUK96M07XEMG1. It fails to find the resource here.
What account is it looking for? 6TGIMWF47DO0XUK96M07XEMG1 is not the account sent. How can I debug this?

gae appengine + auth0 localhost

Goodmorning,
i have some trouble in wrinting a callback method that it's used to login to my web app (hosted in gae)
#app.route('/callback')
def callback_handling():
env = os.environ
code = request.args.get('code')
json_header={'Content-Type': 'application/x-www-form-urlencoded'}
token_url = "https://{domain}/oauth/token".format(domain='mydomain')
token_payload = {
'client_id': 'myid',
'client_secret': 'mysecret',
'redirect_uri': 'http://localhost:8080/callback',
'code': code,
'grant_type': 'authorization_code'
}
encoded = urllib.urlencode(token_payload)
user_url = "https://{domain}/userinfo?access_token={access_token}"\
.format(access_token=token_info['access_token'])
user_info = urlfetch.Fetch(user_url, method=urlfetch.GET, headers=json_header)
session['profile'] = user_info
return redirect('/dashboard')
i costantly get error 401 unathorized..from debug console any ideas to fix?..reading over the internet i understand that i must use urlfetch from gae because is the only lib allowed in gae (requests does not in localhost)..
In your code, is this : "https://{domain}/userinfo" your own endpoint or is this some third-party that you are trying to auth with?
Either way the 401 unauthorised seems legit, and being thrown by the endpoint, If "https://{domain}/userinfo" is pointing to your own endpoint then being able to see that logic would help, alternatively if it is a third-party I would recommend testing your request with something like postman, to see what your request looks like, headers etc. and make sure that it matches what the endpoint is expecting.
Hope this helps.

How to use oauth2 to access StackExchange API?

I'm following the instructions mentioned here: https://api.stackexchange.com/docs/authentication
But since there is no code provided, I'm not able to understand the flow correctly.
I've been trying to get the authentication part done using two methods below but I have hit a deadend.
1)
import requests
from pprint import pprint
resp = requests.get('https://stackexchange.com/oauth/dialog?client_id=6667&scope=private_info&redirect_uri=https://stackexchange.com/oauth/login_success/')
pprint(vars(resp))
2)
import oauth2 as oauth
from pprint import pprint
url = 'https://www.stackexchange.com'
request_token_url = '%s/oauth/' % url
access_token_url = '%s/' % url
consumer = oauth.Consumer(key='mykey',
secret='mysecret')
client = oauth.Client(consumer)
response, content = client.request(request_token_url, 'GET')
print(response, content)
I'm not sure how to go forward from here? I need to use the access token that is returned and use it to query the API. A sample code would really really help! Thanks.
EDIT: This is the code I'm using currently:
from requests_oauthlib import OAuth2Session
from pprint import pprint
client_id = 'x'
client_secret = 'x'
redirect_uri = 'https://stackexchange.com/oauth/login_success'
scope = 'no_expiry'
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope)
pprint(vars(oauth))
authorization_url, state = oauth.authorization_url('https://stackexchange.com/oauth/dialog')
print(authorization_url)
Instead of having to click on the authorization_url and get the token, is there a way I can directly fetch the token within the script itself?
Of the two methods you used, the first is the recommended method for desktop applications. It is probably correct.
OAuth is intended to force the user to go to a specific webpage and acknowledge that they are giving permission (usually through clicking a button) for an application to access their data. The HTTP responses you print are merely the webpage where a user needs to click accept.
To get a feeling for the flow, put the first address (https://stackexchange.com/oauth/dialog?client_id=6667&scope=&redirect_uri=https://stackexchange.com/oauth/login_success/) in the address bar and click accept on the loaded page. The access_token will be in the URL right after that.
If you are making the application only for yourself, the access_token can be copied into your Python script. The token expires in one day; if that is too short add no_expiry to scope to make it last forever. DO NOT share the token with anyone else, since it gives them access to details of your account! Each user of the script must generate their own token.
Test the access_token by inserting in your app's key and the access_token you just obtained into the url: https://api.stackexchange.com/2.2/me?key=key&site=stackoverflow&order=desc&sort=reputation&access_token=&filter=default
If you need a more automated, integrated, user-friendly solution, I would look at selenium webdriver to open a browser window and get the resulting credentials.
Just one minor correction on Marc's answer. If you want the access token to last forever, you should add no_expiry instead of no_expire.

google+ api moments.insert 401 Unauthorized with python-requests

I have successfully inserted a moment with this procedure before and am trying to figure out if the API is currently broken or i'm making a mistake.
I have obtained a valid access_token with request_visible_actions=http://schema.org/AddAction as seen in the partial url below from the auth stage as well as plus.login scope:
https://accounts.google.com/o/oauth2/auth?access_type=offline&request_visible_actions=http%3A%2F%2Fschema.org%2FAddAction&
the request code:
import requests
import json
moment = {"type":"http://schema.org/AddAction",
"object": {"id": "object-id-1",
"type":"http://schema.org/AddAction",
"name": "The Google+ Platform",
"description": "A page that describes just how awesome Google+ is!",
"image": "https://developers.google.com/+/plugins/snippet/examples/thing.png"}}
url ='https://www.googleapis.com/plus/v1/people/me/moments/vault'
params = {'access_token': 'MY_VALID_ACCESS_TOKEN'}
r = requests.request("POST",url=url, params=params, json=moment, headers=headers)
print r.url
print r.json()
and the result:
https://www.googleapis.com/plus/v1/people/me/moments/vault?access_token=MY_ACCESS_TOKEN
{u'error': {u'code': 401,
u'message':
u'Unauthorized',
u'errors': [{u'domain': u'global', u'message': u'Unauthorized', u'reason': u'unauthorized'}]}}
I tried to verify with the tokeninfo endpoint that request_visible_actions are present, but it's not listed there.
confirmed granted scopes:
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/plus.profile.emails.read',
'https://www.googleapis.com/auth/plus.moments.write',
'https://www.googleapis.com/auth/plus.me',
'https://www.googleapis.com/auth/plus.profile.agerange.read',
'https://www.googleapis.com/auth/plus.profile.language.read',
'https://www.googleapis.com/auth/plus.circles.members.read'
I am able to get moments/vault with the same token so i know it's valid.
Any ideas here?
The procedure shown in the question does in fact work as long as request_visible_actions=space separated list of app activity types was included when the user makes the first connection to your app and the app activity type you are trying to post was included. If you want to update the list or add the moment.insert permission after initial authentication, the user must first disconnect the app from their google+ settings and reconnect.
It seems this is a bug with google's implementation of request_visible_actions and an incremental authorization strategy.
request_visible_actions seems to only be accepted during initial authorization and disregarded after that. This makes incrementally adding this parameter impossible.
Further more, request_visible_actions is not available when calling the tokeninfo endpoint, which leads me to believe that request_visible_actions is somehow dissociated from token auth and merely an attribute assigned to the user's app connection on instantiation.
Posted to exising moments.instert 401 issue:

403 when retrieving a WSDL via Python SUDS

I can't seem to get SUDS to download a WSDL that requires Basic auth credentials. My code is simple:
wsdl_url = 'https://example.com/ChangeRequest.do?WSDL'
self.client = Client(wsdl_url, username=username, password=password)
I've also tried:
from suds.transport.https import HttpAuthenticated
wsdl_url = 'https://example.com/ChangeRequest.do?WSDL'
credentials = dict(username=username, password=password)
t = HttpAuthenticated(**credentials)
self.client = Client(url=wsdl_url, transport=t)
In both cases, the service returns a 403 Forbidden error. I can go down into the SUDS code in http.py and add this line to the call:
u2request.add_header('Authorization','Basic xxxxxxxxxxxxxxxxxxxx')
This works. What am I doing wrong to get SUDS to pass my credentials when downloading the WSDL?
Note: I try to connect to the WSDL directly using both Chrome's Postman plugin and SoapUI, and the service works as well. So I know the credentials are correct.
I encountered a similar issue (suds v0.4, wsdl, 403), and found out that it was because the server I'm trying to access blocks any requests with the header User-Agent set like Python-urllib* (suds is using urllib2, hence the default header). Explicitly change the header solves the issue.
Particular to my solution: I overrode the open method of a transport class, and set client options, like the following code snippet. Note that we need to explicitly set for open and subsequent requests separately. Please advice better ways to circumvent this if you know any. And hope this post could help save someone's time in the future.
import urllib2
import suds
from suds.transport.https import HttpAuthenticated
from suds.transport import TransportError
URL = 'https://example.com/ChangeRequest.do?WSDL'
class HttpHeaderModify(HttpAuthenticated):
def open(self, request):
try:
url = request.url
u2request = urllib2.Request(url, headers={'User-Agent': 'Mozilla'})
self.proxy = self.options.proxy
return self.u2open(u2request)
except urllib2.HTTPError, e:
raise TransportError(str(e), e.code, e.fp)
transport = HttpHeaderModify()
client = Client(URL, transport=transport, timeout=10)
# Subsequent requests' header needs to be set again here. The overridden transport
# class only handles opening of the client.
client.set_options(headers={'User-Agent': 'Mozilla'})
P.S. Though my problem may not be the same, searching for "403 suds" pops up this SO question, so I decide just post my solution here.
reference post that gave me the right direction: https://bitbucket.org/jurko/suds/issues/27/client-request-for-wsdl-does-not-use-given
I used to have this issue before and compare with the soap UI header.
Found that suds missing to include the header (Host).
client.set_options(headers={'Host': 'value'})
And issue fixed.

Categories