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

This section contains the generic Python implementation of RFC7523.

Using JWTs as Authorization Grants

Changed in version 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 RFC6749 built-in grants. Which means it can be registered with register_grant().

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

from authlib.jose import JsonWebKey
from authlib.oauth2.rfc7523 import JWTBearerGrant as _JWTBearerGrant

class JWTBearerGrant(_JWTBearerGrant):
    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 = JsonWebKey.import_key_set(client.jwks)

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

    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)

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. AssertionSession.

  2. AssertionSession.

  3. AsyncAssertionSession.

Using JWTs for Client Authentication

In RFC6749: The OAuth 2.0 Authorization Framework, 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 AuthorizationServer has provided a method register_client_auth_method() to add more client authentication methods.

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

class JWTClientAuth(JWTBearerClientAssertion):
    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('https://example.com/oauth/token')
)

The value https://example.com/oauth/token is your authorization server’s token endpoint, which is used as aud value in JWT.

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'.

Using JWTs Client Assertion in OAuth2Session

Authlib RFC7523 provides two more client authentication methods for 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

class authlib.oauth2.rfc7523.JWTBearerGrant(request, server)
CLAIMS_OPTIONS = {'aud': {'essential': True}, 'exp': {'essential': True}, 'iss': {'essential': True}}

Options for verifying JWT payload claims. Developers MAY overwrite this constant to create a more strict options.

process_assertion_claims(assertion)

Extract JWT payload claims from request “assertion”, per Section 3.1.

Parameters

assertion – assertion string value in the request

Returns

JWTClaims

Raise

InvalidGrantError

validate_token_request()

The client makes a request to the token endpoint by sending the following parameters using the “application/x-www-form-urlencoded” format per Section 2.1:

grant_type

REQUIRED. Value MUST be set to “urn:ietf:params:oauth:grant-type:jwt-bearer”.

assertion

REQUIRED. Value MUST contain a single JWT.

scope

OPTIONAL.

The following example demonstrates an access token request with a JWT as an authorization grant:

POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]
create_token_response()

If valid and authorized, the authorization server issues an access token.

resolve_issuer_client(issuer)

Fetch client via “iss” in assertion claims. Developers MUST implement this method in subclass, e.g.:

def resolve_issuer_client(self, issuer):
    return Client.query_by_iss(issuer)
Parameters

issuer – “iss” value in assertion

Returns

Client instance

resolve_client_key(client, headers, payload)

Resolve client key to decode assertion data. Developers MUST implement this method in subclass. For instance, there is a “jwks” column on client table, e.g.:

def resolve_client_key(self, client, headers, payload):
    # from authlib.jose import JsonWebKey

    key_set = JsonWebKey.import_key_set(client.jwks)
    return key_set.find_by_kid(headers['kid'])
Parameters
  • client – instance of OAuth client model

  • headers – headers part of the JWT

  • payload – payload part of the JWT

Returns

authlib.jose.Key instance

authenticate_user(subject)

Authenticate user with the given assertion claims. Developers MUST implement it in subclass, e.g.:

def authenticate_user(self, subject):
    return User.get_by_sub(subject)
Parameters

subject – “sub” value in claims

Returns

User instance

has_granted_permission(client, user)

Check if the client has permission to access the given user’s resource. Developers MUST implement it in subclass, e.g.:

def has_granted_permission(self, client, user):
    permission = ClientUserGrant.query(client=client, user=user)
    return permission.granted
Parameters
  • client – instance of OAuth client model

  • user – instance of User model

Returns

bool

class authlib.oauth2.rfc7523.JWTBearerClientAssertion(token_url, validate_jti=True)

Implementation of Using JWTs for Client Authentication, which is defined by RFC7523.

CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'

Value of client_assertion_type of JWTs

CLIENT_AUTH_METHOD = 'client_assertion_jwt'

Name of the client authentication method

create_claims_options()

Create a claims_options for verify JWT payload claims. Developers MAY overwrite this method to create a more strict options.

process_assertion_claims(assertion, resolve_key)

Extract JWT payload claims from request “assertion”, per Section 3.1.

Parameters
  • assertion – assertion string value in the request

  • resolve_key – function to resolve the sign key

Returns

JWTClaims

Raise

InvalidClientError

validate_jti(claims, jti)

Validate if the given jti value is used before. Developers MUST implement this method:

def validate_jti(self, claims, jti):
    key = 'jti:{}-{}'.format(claims['sub'], jti)
    if redis.get(key):
        return False
    redis.set(key, 1, ex=3600)
    return True
resolve_client_public_key(client, headers)

Resolve the client public key for verifying the JWT signature. A client may have many public keys, in this case, we can retrieve it via kid value in headers. Developers MUST implement this method:

def resolve_client_public_key(self, client, headers):
    return client.public_key
class authlib.oauth2.rfc7523.ClientSecretJWT(token_endpoint=None, claims=None, alg=None)

Authentication method for OAuth 2.0 Client. This authentication method is called client_secret_jwt, which is using client_id and client_secret constructed with JWT to identify a client.

Here is an example of use client_secret_jwt with Requests Session:

from authlib.integrations.requests_client import OAuth2Session

token_endpoint = 'https://example.com/oauth/token'
session = OAuth2Session(
    'your-client-id', 'your-client-secret',
    token_endpoint_auth_method='client_secret_jwt'
)
session.register_client_auth_method(ClientSecretJWT(token_endpoint))
session.fetch_token(token_endpoint)
Parameters
  • token_endpoint – A string URL of the token endpoint

  • claims – Extra JWT claims

class authlib.oauth2.rfc7523.PrivateKeyJWT(token_endpoint=None, claims=None, alg=None)

Authentication method for OAuth 2.0 Client. This authentication method is called private_key_jwt, which is using client_id and private_key constructed with JWT to identify a client.

Here is an example of use private_key_jwt with Requests Session:

from authlib.integrations.requests_client import OAuth2Session

token_endpoint = 'https://example.com/oauth/token'
session = OAuth2Session(
    'your-client-id', 'your-client-private-key',
    token_endpoint_auth_method='private_key_jwt'
)
session.register_client_auth_method(PrivateKeyJWT(token_endpoint))
session.fetch_token(token_endpoint)
Parameters
  • token_endpoint – A string URL of the token endpoint

  • claims – Extra JWT claims