在 requests-oauthlib 的 oauth2session 中包含授权

Include authorization in a oauth2session for requests-oauthlib

从阅读各种文档来看,似乎 oauth2 提供者可选地需要授权来请求刷新令牌。我正在使用似乎需要授权的 FitBit API。

我按照此处的说明使用 requests-oauthlib 刷新令牌: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#refreshing-tokens

一些设置代码(不是我正在使用的,但你明白了:

>>> 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)

但是,通过这个调用我得到:

MissingTokenError: (missing_token) Missing access token parameter.

我知道我的令牌已过期,但为什么刷新不起作用?

编辑:下面仍然有一些有用的信息,但是覆盖 auth 函数意味着我的实际 API 请求现在失败了(即下面不是正确的答案) 我不确定我是如何得到我上次尝试工作的一个请求的。它可能只是返回了一个错误(在 json 中)而不是抛出错误,我只是假设没有出现错误意味着它实际上在工作。 查看 OrangeDog 的正确解决方法(直到修复库)。

嗯,我检查了 FitBit 服务器的响应,就在抛出 MissingTokenError 之前。结果我收到一个错误,说身份验证不正确。

这可能是一个很有用的观点,值得仔细研究一下。 MissingTokenError 似乎在响应不包含预期令牌时发生。如果您可以更仔细地调试和查看响应,您可能会发现服务器提供了更多关于请求格式错误原因的详细信息。我去了错误的位置并添加了打印语句。这使我能够看到来自 FitBit 的 JSON 消息。无论如何,这种方法可能对其他人获得 MissingTokenError 有用。

    if not 'access_token' in params:
        print(params)
        raise MissingTokenError(description="Missing access token parameter.")

反正调试了一番,还是没有设置认证。此外,我的客户端 ID 和密码已发布在正文中(这可能不是问题)。在 FitBit 示例中,客户端 ID 和密码未发布在正文中,而是通过身份验证传递。所以我需要客户端将身份验证传递给 FitBit。

那么问题来了,我如何进行身份验证。目前缺乏这方面的文档。但是,查看会话对象时,我发现了一个正在设置的 .auth 属性 和对问题的引用 (#278)。在那个问题中,为手动身份验证设置提供了一个解决方法(如下所示,我的代码):https://github.com/requests/requests-oauthlib/issues/278

请注意,oauth 会话继承自请求会话,因此对于真正了解请求的人来说,这可能是显而易见的...

无论如何,解决办法就是在初始化会话后设置auth参数。由于 FitBit 不需要主体中的客户端 ID 和密码,我也删除了额外的传递(同样,这可能是一个小问题,不会真正影响事情):

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)

好的,我想我正确地复制了那个代码,目前我这边有点乱。关键部分只是一行:

client.auth = auth

客户端启动后。

请注意,我的令牌包含一个 expires_at 字段。我不认为会话在确切时间方面处理 expires_in。换句话说,我认为 expires_in 仅在其值小于 0 时才会导致刷新。我认为它不会查看对象的创建时间并启动计时器或将 属性 设置为知道 expires_in 是相对于什么的。另一方面,expires_at 字段似乎提供(我认为)一个字段,该字段被检查以确保令牌在请求时尚未过期,因为 expires_at 是一个真实世界,非亲属,时间。这是我的令牌字典(带有假令牌和 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}

图书馆在这方面有问题。参见 #379

您可以像这样解决它:

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)