Skip to content

Commit fb8380d

Browse files
authored
Merge branch 'master' into remove_google_group
2 parents d45f41e + b56987e commit fb8380d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3860
-257
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ indent_size = 4
88
insert_final_newline = true
99
trim_trailing_whitespace = true
1010

11-
[{Makefile,tox.ini,setup.cfg}]
11+
[{Makefile,setup.cfg}]
1212
indent_style = tab
1313

1414
[*.{yml,yaml}]

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pip-log.txt
2626

2727
# Unit test / coverage reports
2828
.cache
29+
.pytest_cache
2930
.coverage
3031
.tox
3132
.pytest_cache/

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
## [unreleased]
1818

19+
### Added
20+
* #915 Add optional OpenID Connect support.
21+
1922
## [1.4.1]
2023

2124
### Changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Index
4040
views/details
4141
models
4242
advanced_topics
43+
oidc
4344
signals
4445
settings
4546
resource_server

docs/oidc.rst

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
OpenID Connect
2+
++++++++++++++
3+
4+
OpenID Connect support
5+
======================
6+
7+
``django-oauth-toolkit`` supports OpenID Connect (OIDC), which standardizes
8+
authentication flows and provides a plug and play integration with other
9+
systems. OIDC is built on top of OAuth 2.0 to provide:
10+
11+
* Generating ID tokens as part of the login process. These are JWT that
12+
describe the user, and can be used to authenticate them to your application.
13+
* Metadata based auto-configuration for providers
14+
* A user info endpoint, which applications can query to get more information
15+
about a user.
16+
17+
Enabling OIDC doesn't affect your existing OAuth 2.0 flows, these will
18+
continue to work alongside OIDC.
19+
20+
We support:
21+
22+
* OpenID Connect Authorization Code Flow
23+
* OpenID Connect Implicit Flow
24+
* OpenID Connect Hybrid Flow
25+
26+
27+
Configuration
28+
=============
29+
30+
OIDC is not enabled by default because it requires additional configuration
31+
that must be provided. ``django-oauth-toolkit`` supports two different
32+
algorithms for signing JWT tokens, ``RS256``, which uses asymmetric RSA keys (a
33+
public key and a private key), and ``HS256``, which uses a symmetric key.
34+
35+
It is preferrable to use ``RS256``, because this produces a token that can be
36+
verified by anyone using the public key (which is made available and
37+
discoverable by OIDC service auto-discovery, included with
38+
``django-oauth-toolkit``). ``HS256`` on the other hand uses the
39+
``client_secret`` in order to verify keys. This is simpler to implement, but
40+
makes it harder to safely verify tokens.
41+
42+
Using ``HS256`` also means that you cannot use the Implicit or Hybrid flows,
43+
or verify the tokens in public clients, because you cannot disclose the
44+
``client_secret`` to a public client. If you are using a public client, you
45+
must use ``RS256``.
46+
47+
48+
Creating RSA private key
49+
~~~~~~~~~~~~~~~~~~~~~~~~
50+
51+
To use ``RS256`` requires an RSA private key, which is used for signing JWT. You
52+
can generate this using the `openssl`_ tool::
53+
54+
openssl genrsa -out oidc.key 4096
55+
56+
This will generate a 4096-bit RSA key, which will be sufficient for our needs.
57+
58+
.. _openssl: https://www.openssl.org
59+
60+
.. warning::
61+
The contents of this key *must* be kept a secret. Don't put it in your
62+
settings and commit it to version control!
63+
64+
If the key is ever accidentally disclosed, an attacker could use it to
65+
forge JWT tokens that verify as issued by your OAuth provider, which is
66+
very bad!
67+
68+
If it is ever disclosed, you should immediately replace the key.
69+
70+
Safe ways to handle it would be:
71+
72+
* Store it in a secure system like `Hashicorp Vault`_, and inject it in to
73+
your environment when running your server.
74+
* Store it in a secure file on your server, and use your initialization
75+
scripts to inject it in to your environment.
76+
77+
.. _Hashicorp Vault: https://www.hashicorp.com/products/vault
78+
79+
Now we need to add this key to our settings and allow the ``openid`` scope to
80+
be used. Assuming we have set an environment variable called
81+
``OIDC_RSA_PRIVATE_KEY``, we can make changes to our ``settings.py``::
82+
83+
import os.environ
84+
85+
OAUTH2_PROVIDER = {
86+
"OIDC_ENABLED": True,
87+
"OIDC_RSA_PRIVATE_KEY": os.environ.get("OIDC_RSA_PRIVATE_KEY"),
88+
"SCOPES": {
89+
"openid": "OpenID Connect scope",
90+
# ... any other scopes that you use
91+
},
92+
# ... any other settings you want
93+
}
94+
95+
If you are adding OIDC support to an existing OAuth 2.0 provider site, and you
96+
are currently using a custom class for ``OAUTH2_SERVER_CLASS``, you must
97+
change this class to derive from ``oauthlib.openid.Server`` instead of
98+
``oauthlib.oauth2.Server``.
99+
100+
With ``RSA`` key-pairs, the public key can be generated from the private key,
101+
so there is no need to add a setting for the public key.
102+
103+
Using ``HS256`` keys
104+
~~~~~~~~~~~~~~~~~~~~
105+
106+
If you would prefer to use just ``HS256`` keys, you don't need to create any
107+
additional keys, ``django-oauth-toolkit`` will just use the application's
108+
``client_secret`` to sign the JWT token.
109+
110+
In this case, you just need to enable OIDC and add ``openid`` to your list of
111+
scopes in your ``settings.py``::
112+
113+
OAUTH2_PROVIDER = {
114+
"OIDC_ENABLED": True,
115+
"SCOPES": {
116+
"openid": "OpenID Connect scope",
117+
# ... any other scopes that you use
118+
},
119+
# ... any other settings you want
120+
}
121+
122+
.. info::
123+
If you want to enable ``RS256`` at a later date, you can do so - just add
124+
the private key as described above.
125+
126+
Setting up OIDC enabled clients
127+
===============================
128+
129+
Setting up an OIDC client in ``django-oauth-toolkit`` is simple - in fact, all
130+
existing OAuth 2.0 Authorization Code Flow and Implicit Flow applications that
131+
are already configured can be easily updated to use OIDC by setting the
132+
appropriate algorithm for them to use.
133+
134+
You can also switch existing apps to use OIDC Hybrid Flow by changing their
135+
Authorization Grant Type and selecting a signing algorithm to use.
136+
137+
You can read about the pros and cons of the different flows in `this excellent
138+
article`_ from Robert Broeckelmann.
139+
140+
.. _this excellent article: https://medium.com/@robert.broeckelmann/when-to-use-which-oauth2-grants-and-oidc-flows-ec6a5c00d864
141+
142+
OIDC Authorization Code Flow
143+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
144+
145+
To create an OIDC Authorization Code Flow client, create an ``Application``
146+
with the grant type ``Authorization code`` and select your desired signing
147+
algorithm.
148+
149+
When making an authorization request, be sure to include ``openid`` as a
150+
scope. When the code is exchanged for the access token, the response will
151+
also contain an ID token JWT.
152+
153+
If the ``openid`` scope is not requested, authorization requests will be
154+
treated as standard OAuth 2.0 Authorization Code Grant requests.
155+
156+
With ``PKCE`` enabled, even public clients can use this flow, and it is the most
157+
secure and recommended flow.
158+
159+
OIDC Implicit Flow
160+
~~~~~~~~~~~~~~~~~~
161+
162+
OIDC Implicit Flow is very similar to OAuth 2.0 Implicit Grant, except that
163+
the client can request a ``response_type`` of ``id_token`` or ``id_token
164+
token``. Requesting just ``token`` is also possible, but it would make it not
165+
an OIDC flow and would fall back to being the same as OAuth 2.0 Implicit
166+
Grant.
167+
168+
To setup an OIDC Implicit Flow client, simply create an ``Application`` with
169+
the a grant type of ``Implicit`` and select your desired signing algorithm,
170+
and configure the client to request the ``openid`` scope and an OIDC
171+
``response_type`` (``id_token`` or ``id_token token``).
172+
173+
174+
OIDC Hybrid Flow
175+
~~~~~~~~~~~~~~~~
176+
177+
OIDC Hybrid Flow is a mixture of the previous two flows. It allows the ID
178+
token and an access token to be returned to the frontend, whilst also
179+
allowing the backend to retrieve the ID token and an access token (not
180+
necessarily the same access token) on the backend.
181+
182+
To setup an OIDC Hybrid Flow application, create an ``Application`` with a
183+
grant type of ``OpenID connect hybrid`` and select your desired signing
184+
algorithm.
185+
186+
187+
Customizing the OIDC responses
188+
==============================
189+
190+
This basic configuration will give you a basic working OIDC setup, but your
191+
ID tokens will have very few claims in them, and the ``UserInfo`` service will
192+
just return the same claims as the ID token.
193+
194+
To configure all of these things we need to customize the
195+
``OAUTH2_VALIDATOR_CLASS`` in ``django-oauth-toolkit``. Create a new file in
196+
our project, eg ``my_project/oauth_validator.py``::
197+
198+
from oauth2_provider.oauth2_validators import OAuth2Validator
199+
200+
201+
class CustomOAuth2Validator(OAuth2Validator):
202+
pass
203+
204+
205+
and then configure our site to use this in our ``settings.py``::
206+
207+
OAUTH2_PROVIDER = {
208+
"OAUTH2_VALIDATOR_CLASS": "my_project.oauth_validators.CustomOAuth2Validator",
209+
# ... other settings
210+
}
211+
212+
Now we can customize the tokens and the responses that are produced by adding
213+
methods to our custom validator.
214+
215+
216+
Adding claims to the ID token
217+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
218+
219+
By default the ID token will just have a ``sub`` claim (in addition to the
220+
required claims, eg ``iss``, ``aud``, ``exp``, ``iat``, ``auth_time`` etc),
221+
and the ``sub`` claim will use the primary key of the user as the value.
222+
You'll probably want to customize this and add additional claims or change
223+
what is sent for the ``sub`` claim. To do so, you will need to add a method to
224+
our custom validator::
225+
226+
class CustomOAuth2Validator(OAuth2Validator):
227+
228+
def get_additional_claims(self, request):
229+
return {
230+
"sub": request.user.email,
231+
"first_name": request.user.first_name,
232+
"last_name": request.user.last_name,
233+
}
234+
235+
.. note::
236+
This ``request`` object is not a ``django.http.Request`` object, but an
237+
``oauthlib.common.Request`` object. This has a number of attributes that
238+
you can use to decide what claims to put in to the ID token:
239+
240+
* ``request.scopes`` - a list of the scopes requested by the client when
241+
making an authorization request.
242+
* ``request.claims`` - a dictionary of the requested claims, using the
243+
`OIDC claims requesting system`_. These must be requested by the client
244+
when making an authorization request.
245+
* ``request.user`` - the django user object.
246+
247+
.. _OIDC claims requesting system: https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
248+
249+
What claims you decide to put in to the token is up to you to determine based
250+
upon what the scopes and / or claims means to your provider.
251+
252+
253+
Adding information to the ``UserInfo`` service
254+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
255+
256+
The ``UserInfo`` service is supplied as part of the OIDC service, and is used
257+
to retrieve more information about the user than was supplied in the ID token
258+
when the user logged in to the OIDC client application. It is optional to use
259+
the service. The service is accessed by making a request to the
260+
``UserInfo`` endpoint, eg ``/o/userinfo/`` and supplying the access token
261+
retrieved at login as a ``Bearer`` token.
262+
263+
Again, to modify the content delivered, we need to add a function to our
264+
custom validator. The default implementation adds the claims from the ID
265+
token, so you will probably want to re-use that::
266+
267+
class CustomOAuth2Validator(OAuth2Validator):
268+
269+
def get_userinfo_claims(self, request):
270+
claims = super().get_userinfo_claims()
271+
claims["color_scheme"] = get_color_scheme(request.user)
272+
return claims
273+
274+
275+
OIDC Views
276+
==========
277+
278+
Enabling OIDC support adds three views to ``django-oauth-toolkit``. When OIDC
279+
is not enabled, these views will log that OIDC support is not enabled, and
280+
return a ``404`` response, or if ``DEBUG`` is enabled, raise an
281+
``ImproperlyConfigured`` exception.
282+
283+
In the docs below, it assumes that you have mounted the
284+
``django-oauth-toolkit`` at ``/o/``. If you have mounted it elsewhere, adjust
285+
the URLs accordingly.
286+
287+
288+
ConnectDiscoveryInfoView
289+
~~~~~~~~~~~~~~~~~~~~~~~~
290+
291+
Available at ``/o/.well-known/openid-configuration/``, this view provides auto
292+
discovery information to OIDC clients, telling them the JWT issuer to use, the
293+
location of the JWKs to verify JWTs with, the token and userinfo endpoints to
294+
query, and other details.
295+
296+
297+
JwksInfoView
298+
~~~~~~~~~~~~
299+
300+
Available at ``/o/.well-known/jwks.json``, this view provides details of the key used to sign
301+
the JWTs generated for ID tokens, so that clients are able to verify them.
302+
303+
304+
UserInfoView
305+
~~~~~~~~~~~~
306+
307+
Available at ``/o/userinfo/``, this view provides extra user details. You can
308+
customize the details included in the response as described above.

0 commit comments

Comments
 (0)