authlib
authlib copied to clipboard
Refreshing the token will not updated refresh_token when using authlib.integrations.starlette_client
Describe the bug
When you use authlib.integrations.starlette_client. And you make a request.
the authlib.integrations.starlette_client will use the refresh token if you acces token is expired.
But if authorization server gives you a new refresh token. You should start using the new refresh token.
authlib.integrations.starlette_client will do that for this first request with the expired token.
But because authlib.integrations.starlette_client does not update the token you used in place.
your second request will use the same expired token as the first request.
So it will try to refresh again. this seems unnecessary But this second refresh will also use the old refresh token, not the one it got from the first one. The (oauth rfc)[https://datatracker.ietf.org/doc/html/rfc6749#section-6]
The authorization server MAY issue a new refresh token, in which case the client MUST discard the old refresh token and replace it with the new refresh token. The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client. If a new refresh token is issued, the refresh token scope MUST be identical to that of the refresh token included by the client in the request.
And some server will not allow you to do this.
Error Stacks
INFO: Started server process [96666]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: None:0 - "GET /login HTTP/1.1" 302 Found
INFO: None:0 - "GET /redirect?code=stampNL001.7haR%21IAAAANspwF7PIOViHpzH2INQGXsnRJqMTLZJ-vaCjb5BmCg08QEAAAGaJPmMw8RTPGtsw2KPfKyolmso4R_qaX7i2gFDWbDkCHxBtb7cdUvHP5TPzTbuPsDAnWtlkKmIxfvXoiryjyp98yDVyEPUD5Ow0qAP0sfNzPMTXKTMWbxlvGk_d557omRZo7_L97_7QJxzQpeA6ukSc6529402st5HXnNsDBivsSq5c_jigHG_wRZVmnElPNHGqTZvewcswAcRNSIvYjxEmKwxtzDFOOChBpAwLJa51hUfO6Q0qrA2779knQDPlV0VUjUE5tRoDQvplbSi1onoksdc_qZMNfoIgLOBLq4BCh2JXHpBRbikjmE-6kd133R-PiQ12s9jLLVfu5R7hS1FbEUG0x9jvx1JyDJ9YhoMTW6hiXTPtx62_wmWVslGELG8AlM1voB1pgF4XYiSsFA5PWVVwFaQWoGC_BN6KDVKvZuXANTnffIUc540L3WJz2NPs4-IW56BmZElCBjqwvUSjrTh4muNnJC2oleMF1Pp1d3nE_JcsQsRrZpH8eWK54L3exupV_b81oNvR-7W98GJaGUeljt0zA4LGS4opESsgpGUL5tk3g9Eb4i4Kd8a5Ks0xOZs9AwNeiFK-o5202ctrM0RBo4IrynKWYwbaMXa8auByTq0fZ9FecJZKoj2o7csXo4slb_B46v-Z02JYbNe&state=Jc9pxFjTmFlNqibicRHZ1MXOb8zeAX HTTP/1.1" 200 OK
2023-06-05 15:10:57
2023-06-05 15:10:57
INFO: None:0 - "GET /user HTTP/1.1" 200 OK
2023-06-05 15:10:57
2023-06-05 15:10:57
INFO: None:0 - "GET /user HTTP/1.1" 200 OK
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/home/rens/.local/lib/python3.11/site-packages/uvicorn/protocols/http/h11_impl.py", line 429, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/fastapi/applications.py", line 276, in __call__
await super().__call__(scope, receive, send)
File "/home/rens/.local/lib/python3.11/site-packages/starlette/applications.py", line 122, in __call__
await self.middleware_stack(scope, receive, send)
File "/home/rens/.local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 184, in __call__
raise exc
File "/home/rens/.local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "/home/rens/.local/lib/python3.11/site-packages/starlette/middleware/sessions.py", line 86, in __call__
await self.app(scope, receive, send_wrapper)
File "/home/rens/.local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
raise exc
File "/home/rens/.local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
await self.app(scope, receive, sender)
File "/home/rens/.local/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
raise e
File "/home/rens/.local/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "/home/rens/.local/lib/python3.11/site-packages/starlette/routing.py", line 718, in __call__
await route.handle(scope, receive, send)
File "/home/rens/.local/lib/python3.11/site-packages/starlette/routing.py", line 276, in handle
await self.app(scope, receive, send)
File "/home/rens/.local/lib/python3.11/site-packages/starlette/routing.py", line 66, in app
response = await func(request)
^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/fastapi/routing.py", line 237, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/fastapi/routing.py", line 163, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/werk/exact/example-oauth2/main.py", line 62, in get_user
response2 = await oauth.exact.get('v1/current/Me',
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/authlib/integrations/base_client/async_app.py", line 86, in request
return await _http_request(self, session, method, url, token, kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/authlib/integrations/base_client/async_app.py", line 144, in _http_request
return await session.request(method, url, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/authlib/integrations/httpx_client/oauth2_client.py", line 85, in request
await self.ensure_active_token(self.token)
File "/home/rens/.local/lib/python3.11/site-packages/authlib/integrations/httpx_client/oauth2_client.py", line 112, in ensure_active_token
await self.refresh_token(url, refresh_token=refresh_token)
File "/home/rens/.local/lib/python3.11/site-packages/authlib/integrations/httpx_client/oauth2_client.py", line 148, in _refresh_token
token = self.parse_response_token(resp)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/rens/.local/lib/python3.11/site-packages/authlib/oauth2/client.py", line 340, in parse_response_token
raise self.oauth_error_class(
authlib.integrations.base_client.errors.OAuthError: unauthorized_client: Old refresh token used.
To Reproduce
I talk to exact-online api that does not allow reuse of refresh tokens. This is but unusual, but confirming to Oauth 2 rfc.
This the code i used to reproduce this error. (full code can be found here: main.py.txt)
@app.get("/user")
async def get_user(request: Request): # W: Missing function or method docstring
token = request.session["token"]
expires_at = datetime.fromtimestamp(int(token["expires_at"]))
print(expires_at)
response = await oauth.exact.get('v1/current/Me', #
token=token,
headers={'Accept': 'application/json'})
expires_at = datetime.fromtimestamp(int(token["expires_at"]))
print(expires_at)
response2 = await oauth.exact.get('v1/current/Me',
token=token,
headers={'Accept': 'application/json'}) #
return response2.json()
-
First i request a acces token (by accessing /login via browser)
-
Then I access
/userthis works: This will make 2 request to v1/current/Me exact online It also print that acces token will expire at 2023-06-05 15:10:57. (see log/stacktrace) -
i will wait until token is expired
-
Then I access
/useragain this does not work. First request gets a response. You get error "Old refresh token used." on the second request. I also see thatexpires_atbefore the second request still is the same. So i assume the token is not updated.
I know that part of the problem is that I use the same token twice. But there is no way to get the new token. if the refresh is done. Or do i miss something?
Expected behavior
I expect that either:
-
token is updated in place by
oauth.exact.getOr part of return value ofoauth.exact.get -
Or token is managed by the inner client. So it is always send token of the client. when user does set token explicitly. And you have way to update this token. This issue might be relevant #422
- OS: Arch Linux
- Python Version: Python 3.11.3
- Authlib Version: 1.2.0
- starlette Version: 0.26.1
- fastapi version: 0.95.1
- uvicorn Version: 0.21.1
Additional context
Hope I made it clear what te problem is. I understand its not easy for other to test this with exact online api. But i assume it works the same with others. But they will problem not complain about reusing a refresh token.
Let me know if i can help with something?
I've encountered this as well. This issue is not relegated to starlette or any particular client/provider.
My current workaround is to reload the token from storage after the refresh has happened, but in many cases that leads to some gnarly code. Like, rereading the token from the db before every.single.api call. Not great for performance when I need to make hundreds or thousands of calls.
Anything new here?