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¶
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 jwk
from authlib.oauth2.rfc7523 import JWTBearerGrant as _JWTBearerGrant
class JWTBearerGrant(_JWTBearerGrant):
def authenticate_user(self, client, claims):
# get user from claims info, usually it is claims['sub']
# for anonymous user, return None
return None
def authenticate_client(self, claims):
# get client from claims, usually it is claims['iss']
# since the assertion JWT is generated by this client
return get_client_by_iss(claims['iss'])
def resolve_public_key(self, headers, payload):
# get public key to decode the assertion JWT
jwk_set = get_client_public_keys(claims['iss'])
return jwk.loads(jwk_set, header.get('kid'))
# 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:
AssertionSession
.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 servers’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:
client_secret_jwt
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)¶ -
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)¶ 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.
-
authenticate_user
(client, claims)¶ Authenticate user with the given assertion claims. Developers MUST implement it in subclass, e.g.:
def authenticate_user(self, client, claims): user = User.get_by_sub(claims['sub']) if is_authorized_to_client(user, client): return user
- Parameters
client – OAuth Client instance
claims – assertion payload claims
- Returns
User instance
-
authenticate_client
(claims)¶ Authenticate client with the given assertion claims. Developers MUST implement it in subclass, e.g.:
def authenticate_client(self, claims): return Client.get_by_iss(claims['iss'])
- Parameters
claims – assertion payload claims
- Returns
Client instance
-
resolve_public_key
(headers, payload)¶ Find public key to verify assertion signature. Developers MUST implement it in subclass, e.g.:
def resolve_public_key(self, headers, payload): jwk_set = get_jwk_set_by_iss(payload['iss']) return filter_jwk_set(jwk_set, headers['kid'])
- Parameters
headers – JWT headers dict
payload – JWT payload dict
- Returns
A public key
-
-
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)¶ Authentication method for OAuth 2.0 Client. This authentication method is called
client_secret_jwt
, which is usingclient_id
andclient_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)¶ Authentication method for OAuth 2.0 Client. This authentication method is called
private_key_jwt
, which is usingclient_id
andprivate_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