File: grants.rst

package info (click to toggle)
python-authlib 1.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,016 kB
  • sloc: python: 26,998; makefile: 53; sh: 14
file content (265 lines) | stat: -rw-r--r-- 9,036 bytes parent folder | download | duplicates (2)
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
Register Grants
===============

.. meta::
    :description: Register Authorization Code Grant, Implicit Grant,
        Resource Owner Password Credentials Grant, Client Credentials Grant
        and Refresh Token Grant into Django OAuth 2.0 provider.

.. module:: authlib.oauth2.rfc6749.grants
    :noindex:

There are four grant types defined by RFC6749, you can also create your own
extended grant. Register the supported grant types to the authorization server.

.. _django_oauth2_code_grant:

Authorization Code Grant
------------------------

Authorization Code Grant is a very common grant type, it is supported by almost
every OAuth 2 providers. It uses an authorization code to exchange access
token. In this case, we need a place to store the authorization code. It can be
kept in a database or a cache like redis. Here is an example of database
**AuthorizationCode**::

    from django.db.models import ForeignKey, CASCADE
    from django.contrib.auth.models import User
    from authlib.oauth2.rfc6749 import AuthorizationCodeMixin

    def now_timestamp():
        return int(time.time())

    class AuthorizationCode(Model, AuthorizationCodeMixin):
        user = ForeignKey(User, on_delete=CASCADE)
        client_id = CharField(max_length=48, db_index=True)
        code = CharField(max_length=120, unique=True, null=False)
        redirect_uri = TextField(default='', null=True)
        response_type = TextField(default='')
        scope = TextField(default='', null=True)
        auth_time = IntegerField(null=False, default=now_timestamp)

        def is_expired(self):
            return self.auth_time + 300 < time.time()

        def get_redirect_uri(self):
            return self.redirect_uri

        def get_scope(self):
            return self.scope or ''

        def get_auth_time(self):
            return self.auth_time

Note here, you **MUST** implement the missing methods of
:class:`~authlib.oauth2.rfc6749.AuthorizationCodeMixin` API interface.

Later, you can use this ``AuthorizationCode`` database model to handle ``authorization_code``
grant type. Here is how::

    from authlib.oauth2.rfc6749 import grants
    from authlib.common.security import generate_token

    class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
        def save_authorization_code(self, code, request):
            client = request.client
            auth_code = AuthorizationCode(
                code=code,
                client_id=client.client_id,
                redirect_uri=request.redirect_uri,
                response_type=request.payload.response_type,
                scope=request.payload.scope,
                user=request.user,
            )
            auth_code.save()
            return auth_code

        def query_authorization_code(self, code, client):
            try:
                item = AuthorizationCode.objects.get(code=code, client_id=client.client_id)
            except AuthorizationCode.DoesNotExist:
                return None

            if not item.is_expired():
                return item

        def delete_authorization_code(self, authorization_code):
            authorization_code.delete()

        def authenticate_user(self, authorization_code):
            return authorization_code.user

    # register it to grant endpoint
    server.register_grant(AuthorizationCodeGrant)

.. note:: AuthorizationCodeGrant is the most complex grant.

Default allowed :ref:`client_auth_methods` are:

1. client_secret_basic
2. client_secret_post
3. none

You can change it in the subclass, e.g. remove the ``none`` authentication method::

    class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
        TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post']

.. note:: This is important when you want to support OpenID Connect.

Implicit Grant
--------------

The implicit grant type is usually used in a browser, when resource
owner granted the access, access token is issued in the redirect URI,
there is no missing implementation, which means it can be easily registered
with::

    from authlib.oauth2.rfc6749 import grants

    # register it to grant endpoint
    server.register_grant(grants.ImplicitGrant)

Implicit Grant is used by **public** client which has no **client_secret**.
Only allowed :ref:`client_auth_methods`: ``none``.

Resource Owner Password Credentials Grant
-----------------------------------------

Resource owner uses their username and password to exchange an access token,
this grant type should be used only when the client is trustworthy, implement
it with a subclass of :class:`ResourceOwnerPasswordCredentialsGrant`::

    from authlib.oauth2.rfc6749 import grants
    from django.contrib.auth.models import User

    class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
        def authenticate_user(self, username, password):
            try:
                user = User.objects.get(username=username)
                if user.check_password(password):
                    return user
            except User.DoesNotExist:
                return None

    # register it to grant endpoint
    server.register_grant(PasswordGrant)

Default allowed :ref:`client_auth_methods`: ``client_secret_basic``.
You can add more in the subclass::

    class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
        TOKEN_ENDPOINT_AUTH_METHODS = [
            'client_secret_basic', 'client_secret_post'
        ]

Client Credentials Grant
------------------------

Client credentials grant type can access public resources and the
client's creator's resources. It can be easily registered with::

    from authlib.oauth2.rfc6749 import grants

    # register it to grant endpoint
    server.register_grant(grants.ClientCredentialsGrant)

Default allowed :ref:`client_auth_methods`: ``client_secret_basic``.
You can add more in the subclass::

    class ClientCredentialsGrant(grants.ClientCredentialsGrant):
        TOKEN_ENDPOINT_AUTH_METHODS = [
            'client_secret_basic', 'client_secret_post'
        ]

Refresh Token Grant
-------------------

Many OAuth 2 providers haven't implemented refresh token endpoint. Authlib
provides it as a grant type, implement it with a subclass of
:class:`RefreshTokenGrant`::

    from authlib.oauth2.rfc6749 import grants

    class RefreshTokenGrant(grants.RefreshTokenGrant):
        def authenticate_refresh_token(self, refresh_token):
            try:
                item = OAuth2Token.objects.get(refresh_token=refresh_token)
                if item.is_refresh_token_active():
                    return item
            except OAuth2Token.DoesNotExist:
                return None

        def authenticate_user(self, credential):
            return credential.user

        def revoke_old_credential(self, credential):
            credential.revoked = True
            credential.save()

    # register it to grant endpoint
    server.register_grant(RefreshTokenGrant)

Default allowed :ref:`client_auth_methods`: ``client_secret_basic``.
You can add more in the subclass::

    class RefreshTokenGrant(grants.RefreshTokenGrant):
        TOKEN_ENDPOINT_AUTH_METHODS = [
            'client_secret_basic', 'client_secret_post'
        ]

By default, RefreshTokenGrant will not issue a ``refresh_token`` in the token
response. Developers can change this behavior with::

    class RefreshTokenGrant(grants.RefreshTokenGrant):
        INCLUDE_NEW_REFRESH_TOKEN = True

Custom Grant Types
------------------

It is also possible to create your own grant types. In Authlib, a **Grant**
supports two endpoints:

1. Authorization Endpoint: which can handle requests with ``response_type``.
2. Token Endpoint: which is the endpoint to issue tokens.

Creating a custom grant type with **BaseGrant**::

    from authlib.oauth2.rfc6749.grants import (
        BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin
    )

    class MyCustomGrant(BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin):
        GRANT_TYPE = 'custom-grant-type-name'

        def validate_authorization_request(self):
            # only needed if using AuthorizationEndpointMixin

        def create_authorization_response(self, grant_user):
            # only needed if using AuthorizationEndpointMixin

        def validate_token_request(self):
            # only needed if using TokenEndpointMixin

        def create_token_response(self):
            # only needed if using TokenEndpointMixin

For a better understanding, you can read the source code of the built-in
grant types. And there are extended grant types defined by other specs:

1. :ref:`jwt_grant_type`


Grant Extensions
----------------

Grant can accept extensions. Developers can pass extensions when registering
grant::

    server.register_grant(AuthorizationCodeGrant, [extension])

For instance, there is ``CodeChallenge`` extension in Authlib::

    server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)])

Learn more about ``CodeChallenge`` at :ref:`specs/rfc7636`.