.. _specs/rfc7523:

RFC7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants
=================================================================================

.. meta::
    :description: API references on RFC7523 JWT Bearer Grant of Python
        implementation, guide on how to create a server that support
        JWT profile for OAuth 2.0 client authentication and authorization
        grants.

This section contains the generic Python implementation of :rfc:`7523`.

.. module:: authlib.oauth2.rfc7523


.. _jwt_grant_type:

Using JWTs as Authorization Grants
----------------------------------

.. versionchanged:: v1.0.0
    Please note that all not-implemented methods are changed.

JWT Profile for OAuth 2.0 Authorization Grants works in the same way with
:ref:`RFC6749 <specs/rfc6749>` built-in grants. Which means it can be
registered with :meth:`~authlib.oauth2.rfc6749.AuthorizationServer.register_grant`.

The client signs the JWT itself, and exchanges it for an access token:

.. mermaid::

    sequenceDiagram
        autonumber
        participant Client
        box rgba(80, 120, 220, 0.12) JWTBearerGrant
            participant AS as Authorization Server
        end
        participant RS as Resource Server

        Note over Client: Signs a JWT
        Client->>AS: POST /token (jwt-bearer)
        AS-->>Client: 200 OK + access token
        Client->>RS: GET /api (bearer token)
        RS-->>Client: 200 OK + resource

1. The client sends ``grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer``
   and the JWT as ``assertion``. In the JWT, ``iss`` is the client and ``sub``
   is the user. ``JWTBearerGrant`` checks the signature with the client key,
   then the claims, then ``has_granted_permission()``.
2. The authorization server returns an access token, without a refresh token.
3. The client calls the API of the Resource Server with the access token, in
   the ``Authorization: Bearer`` header.
4. The Resource Server returns the protected resource.

The ``/token`` and ``/api`` paths are examples.

The base class is :class:`JWTBearerGrant`, you need to implement the missing
methods in order to use it. Here is an example::

    from joserfc.jwk import KeySet
    from authlib.oauth2.rfc7523 import JWTBearerGrant as _JWTBearerGrant

    class JWTBearerGrant(_JWTBearerGrant):
        def get_audiences(self):
            # Per RFC 7523 Section 3, both the token endpoint URL and the
            # authorization server's issuer identifier are valid audience values.
            return ['https://example.com/oauth/token', 'https://example.com']

        def resolve_issuer_client(self, issuer):
            # if using client_id as issuer
            return Client.objects.get(client_id=issuer)

        def resolve_client_key(self, client, headers, payload):
            # if client has `jwks` column
            key_set = KeySet.import_key_set(client.jwks)
            return key_set

        def authenticate_user(self, subject):
            # when assertion contains `sub` value, if this `sub` is email
            return User.objects.get(email=subject)

        def has_granted_permission(self, client, user):
            # check if the client has access to user's resource.
            # for instance, we have a table `UserGrant`, which user can add client
            # to this table to record that client has granted permission
            grant = UserGrant.objects.get(client_id=client.client_id, user_id=user.id)
            if grant:
              return grant.enabled
            return False

    # register grant to authorization server
    authorization_server.register_grant(JWTBearerGrant)

``get_audiences()`` must return a non-empty list of audience identifiers for
this authorization server. Assertions are rejected with ``invalid_grant``
if the method is not overridden or returns an empty list, or if the JWT's
``aud`` does not contain one of the configured identifiers.

The assertion signature only proves the client identity. It does not prove
the user consent. ``has_granted_permission()`` decides if the client may act
for the user. It is not called when the assertion has no ``sub`` claim.

When creating a client, authorization server will generate several key pairs.
The server itself can only keep the public keys, which will be used to decode
assertion value.

For **client implementation**, check out:

1. :class:`~authlib.integrations.requests_client.AssertionSession`.
2. :class:`~authlib.integrations.httpx_client.AssertionSession`.
3. :class:`~authlib.integrations.httpx_client.AsyncAssertionSession`.

.. _jwt_client_authentication:

Using JWTs for Client Authentication
------------------------------------

In :ref:`specs/rfc6749`, Authlib provided three built-in client authentication
methods, which are ``none``, ``client_secret_post`` and ``client_secret_basic``.
With the power of Assertion Framework, we can add more client authentication
methods. In this section, Authlib provides two more options:
``client_secret_jwt`` and ``private_key_jwt``. RFC7523 itself doesn't define
any names, these two names are defined by OpenID Connect in ClientAuthentication_.

The JWT replaces the client credentials in a token request of any grant
type. For instance, with the authorization code grant:

.. mermaid::

    sequenceDiagram
        autonumber
        participant User as User's browser<br/>(User Agent)
        participant Client
        box rgba(80, 120, 220, 0.12) JWTBearerClientAssertion
            participant AS as Authorization Server
        end
        participant RS as Resource Server

        rect rgba(120, 120, 120, 0.08)
            Note over User,AS: Front channel (browser)
            Client-->>User: 302 Found → /authorize
            User->>AS: GET /authorize
            AS-->>User: 302 Found → redirect_uri?code=…
            User->>Client: GET redirect_uri?code=…
        end
        rect rgba(120, 120, 120, 0.08)
            Note over Client,RS: Back channel (server to server)
            Client->>AS: POST /token (authorization_code)
            AS-->>Client: 200 OK + access token
            Client->>RS: GET /api (bearer token)
            RS-->>Client: 200 OK + resource
        end

1. The client redirects the browser to the authorization endpoint.
2. The browser sends ``response_type=code``. The user logs in and consents.
3. The authorization server redirects the browser to the client with a code.
4. The client receives the code.
5. The client sends ``grant_type=authorization_code``, the ``code``,
   ``client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer``
   and a JWT as ``client_assertion``. In the JWT, ``iss`` and ``sub`` are the
   ``client_id``. ``JWTBearerClientAssertion`` checks the signature with the
   client secret or public key.
6. The authorization server returns the token response of the grant.
7. The client calls the API of the Resource Server with the access token, in
   the ``Authorization: Bearer`` header.
8. The Resource Server returns the protected resource.

The ``/authorize``, ``/token`` and ``/api`` paths are examples.

The :class:`~authlib.oauth2.rfc6749.AuthorizationServer` has provided a method
:meth:`~authlib.oauth2.rfc6749.AuthorizationServer.register_client_auth_method`
to add more client authentication methods.

In Authlib, ``client_secret_jwt`` and ``private_key_jwt`` share the same API,
using :class:`JWTBearerClientAssertion` to create a new client authentication::

    class JWTClientAuth(JWTBearerClientAssertion):
        def get_audiences(self):
            # Per RFC 7523 Section 3, both the token endpoint URL and the
            # authorization server's issuer identifier are valid audience values.
            return ['https://example.com/oauth/token', 'https://example.com']

        def validate_jti(self, claims, jti):
            # validate_jti is required by OpenID Connect
            # but it is optional by RFC7523
            # use cache to validate jti value
            key = 'jti:{}-{}'.format(claims['sub'], jti)
            if cache.get(key):
                return False
            cache.set(key, 1, timeout=3600)
            return True

        def resolve_client_public_key(self, client, headers):
            if headers['alg'] == 'HS256':
                return client.client_secret
            if headers['alg'] == 'RS256':
                return client.public_key
            # you may support other ``alg`` value

    authorization_server.register_client_auth_method(
        JWTClientAuth.CLIENT_AUTH_METHOD,
        JWTClientAuth()
    )

The ``get_audiences`` method returns the list of valid ``aud`` values
accepted in client assertion JWTs. Per RFC 7523 Section 3,
both the token endpoint URL and the authorization server's issuer
identifier are valid audience values.

Now we have added this client auth method to authorization server, but no
grant types support this authentication method, you need to add it to the
supported grant types too, e.g. we want to support this in authorization
code grant::

    from authlib.oauth2.rfc6749 import grants

    class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
        TOKEN_ENDPOINT_AUTH_METHODS = [
            'client_secret_basic',
            JWTClientAuth.CLIENT_AUTH_METHOD,
        ]
        # ...

You may noticed that the value of ``CLIENT_AUTH_METHOD`` is
``client_assertion_jwt``. It is not ``client_secret_jwt`` or
``private_key_jwt``, because they have the same logic. In the above
implementation::

    def resolve_client_public_key(self, client, headers):
        alg = headers['alg']

If this ``alg`` is a MAC SHA like ``HS256``, it is called ``client_secret_jwt``,
because the key used to sign a JWT is the client's ``client_secret`` value. If
this ``alg`` is ``RS256`` or something else, it is called ``private_key_jwt``,
because client will use its private key to sign the JWT. You can set a limitation
in the implementation of ``resolve_client_public_key`` to accept only ``HS256``
alg, in this case, you can also alter ``CLIENT_AUTH_METHOD = 'client_secret_jwt'``.

.. _ClientAuthentication: http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

.. _jwt_oauth2session:

Using JWTs Client Assertion in OAuth2Session
--------------------------------------------

Authlib RFC7523 provides two more client authentication methods for :ref:`OAuth 2 Session <oauth_2_session>`:

1. ``client_secret_jwt``
2. ``private_key_jwt``

Here is an example of how to register ``client_secret_jwt`` for ``OAuth2Session``::

    from authlib.oauth2.rfc7523 import ClientSecretJWT
    from authlib.integrations.requests_client import OAuth2Session

    session = OAuth2Session(
        'your-client-id', 'your-client-secret',
        token_endpoint_auth_method='client_secret_jwt'
    )
    token_endpoint = 'https://example.com/oauth/token'
    session.register_client_auth_method(ClientSecretJWT(token_endpoint))
    session.fetch_token(token_endpoint)

How about ``private_key_jwt``? It is the same as ``client_secret_jwt``::

    from authlib.oauth2.rfc7523 import PrivateKeyJWT

    with open('your-private-key.pem', 'rb') as f:
        private_key = f.read()

    session = OAuth2Session(
        'your-client-id', private_key,
        token_endpoint_auth_method='private_key_jwt'  # NOTICE HERE
    )
    token_endpoint = 'https://example.com/oauth/token'
    session.register_client_auth_method(PrivateKeyJWT(token_endpoint))
    session.fetch_token(token_endpoint)

API Reference
-------------

.. autoclass:: JWTBearerGrant
    :member-order: bysource
    :members:

.. autoclass:: JWTBearerClientAssertion
    :member-order: bysource
    :members:

.. autoclass:: ClientSecretJWT

.. autoclass:: PrivateKeyJWT
