lepture / authlib

The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS,JWE,JWK,JWA,JWT included.
https://authlib.org/
BSD 3-Clause "New" or "Revised" License
4.39k stars 436 forks source link

Support async functions in compliance hooks. #619

Open caarmen opened 6 months ago

caarmen commented 6 months ago

Is your feature request related to a problem? Please describe.

It's not possible to execute an additional request when using automatic token refresh.

Describe the solution you'd like

Be able to define an async function as a compliance hook. Example:

def withings_compliance_fix(session: AsyncOAuth2Client):

    async def _fix_refresh_token_request(url, headers, body):
        async with httpx.AsyncClient() as client:
            nonce_response = await client.post (...) # call Withings api to get a nonce
            signing_params = # use the nonce to add the additional required parameters
            body = add_params_to_qs(body, signing_params)
            return url, headers, body

    session.register_compliance_hook(
        "refresh_token_request", _fix_refresh_token_request
    )

oauth.register(
    name="withings",
    ...
    compliance_fix=withings_compliance_fix,
)

This fails in client.py:refresh_token():

        for hook in self.compliance_hook['refresh_token_request']:
            url, headers, body = hook(url, headers, body) # <---- here 👀

Error: TypeError: cannot unpack non-iterable coroutine object

Describe alternatives you've considered

Alternative 1: custom Auth.

I've tried an alternative which worked for retrieving the initial access token, specifying an alternate Auth implementation:


class SignatureAuth(OAuth2ClientAuth):
   def auth_flow(
        self, request: httpx.Request
    ) -> typing.Generator[httpx.Request, httpx.Response, None]:
        nonce_request = prepare_nonce_request()
        nonce_response = yield nonce_request
        signed_request = # Extract the nonce, create a new request with the additional signing params
        yield from super().auth_flow(signed_request)
...
    withings: StarletteOAuth2App = oauth.create_client("withings")
    response = await withings.authorize_access_token(
        request,
        auth=SignatureAuth(),
    )

This approach doesn't work when I want to access a resource, using an expired access token.

    withings: StarletteOAuth2App = oauth.create_client("withings")
    response = await withings.post(url_to_resource, data=data, token=expired_token)

It doesn't pass any auth argument to fetch_token. It falls back to OAuth2ClientAuth.

Alternative 2: Don't sign requests

Additional context

Withings supports two ways to retrieve tokens. Here's the api doc.

It supports "using signature", which is what I've tried to do here. It also supports "using secret". In this case, Authlib works just fine with token_endpoint_auth_method=client_secret_post.

I just thought it would be better to not send the client secret over the network if possible. Note that, it appears that some of Withings apis (not all) require the signature approach.

Note: even though the Withings api has notions of nonce and signature, it doesn't appear to be oauth1. With client_secret_post, it works fine with Authlib oauth2 apis.