1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
.. _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 RFC7523_.
.. _RFC7523: https://tools.ietf.org/html/rfc7523
.. 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 base class is :class:`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)
return key_set.find_by_kid(headers['kid'])
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)
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 :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 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'``.
.. _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`:
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
|