Skip to content

Commit b56987e

Browse files
tevansukthinkwelltwdwiliamsouzaallissonfvlima
authored
OpenID Connect support (jazzband#915)
* Openid Connect Core support - Round 3 * Add OpenID connect hybrid grant type * Add OpenID connect algorithm type to Application model * Add OpenID connect id token model * Add nonce Authorization as required by OpenID connect Implicit Flow * Add body to create_authorization_response to pass nonce and future OpenID parameters to oauthlib.common.Request * Add OpenID connect ID token creation and validation methods and scopes * Add OpenID connect response types * Add OpenID connect authorization code flow test * Add OpenID connect implicit flow tests * Add validate_user_match method to OAuth2Validator * Add RSA_PRIVATE_KEY setting with blank value * Update tox * Add get_jwt_bearer_token to OAuth2Validator * Add validate_jwt_bearer_token to OAuth2Validator * Change OAuth2Validator.validate_id_token default return value to False to avoid validation security breach * Change to use .encode to avoid py2.7 tox test error * Add OpenID connect hybrid flow tests * Change to use .encode to avoid py2.7 tox test error * Add RSA_PRIVATE_KEY to the list of settings that cannot be empt * Add support for oidc connect discovery * Use double quotes for strings * Rename migrations to avoid name and order conflict * Remove commando to install OAuthLib from master and removed jwcrypto duplication * Remove python 2 compatible code * Change errors access_denied/unauthorized_client/consent_required/login_required to be 400 as changed in oauthlib/pull/623 * Change iss claim value to come from settings * Change to use openid connect code server class * Change test to include missing state * Add id_token relation to AbstractAccessToken * Add claims property to AbstractIDToken * Change OAuth2Validator._create_access_token to save id_token to access_token * Add userinfo endpoint * Update migrations and remove oauthlib duplication * Remove old generated migrations * Add new migrations * Fix tests * Add nonce to hybrid tests * Add missing new attributes to test migration * Rebase fixing conflicts and tests * Remove auto generate message * Fix flake8 issues * Fix test doc deps * Add project settings to be ignored in coverage * Tweak migrations to support non-overidden models * OIDC_USERINFO_ENDPOINT is not mandatory * refresh_token grant should be support for OpenID hybrid * Fix the user info view, and remove hard dependency on DRF * Use proper URL generation for OIDC endpoints * Support rich ID tokens and userinfo claims Extend the validator and override get_additional_claims based on your own user model. * Bug fix for at_hash generation See https://openid.net/specs/openid-connect-core-1_0.html#id_token-tokenExample to prove algorithm * OIDC_ISS_ENDPOINT is an optional setting * Support OIDC urls from issuer url if provided * Test for generated OIDC urls * Flake * Rebase on master and migrate url function to re_path * Handle invalid token format exceptions as invalid tokens * Merge migrations and sort imports isort for flake8 lint check Co-authored-by: Dave Burkholder <[email protected]> Co-authored-by: Wiliam Souza <[email protected]> Co-authored-by: Allisson Azevedo <[email protected]> Co-authored-by: fvlima <[email protected]> Co-authored-by: Shaun Stanworth <[email protected]> * Make IDToken admin class swappable * Make OIDC support optional Make OIDC support optional by not requiring OIDC_RSA_PRIVATE_KEY to be set in the settings, and using the standard oauthlib.oauth2.Server class when an OIDC private key is not configured. Add a test fixture wrapping oauth2_settings. This allows individual tests / test suites to override oauth2 settings and have them reset at the end of the test. This avoids configuration leaking from one test to another, and allows us to test multiple different configurations in one test run. When using the oauth2_settings fixture, allow configuration for the test case to be loaded from a pytest marker called oauth2_settings. Split out OIDC specific tests requiring specific OIDC configuration into separate TestCase. Adjust the OAuthLibMixin to fallback to using the server, validator and core classes specified in oauth2_settings when not hardcoded in to the class. These classes can still be specified as hard-coded attributes in sub-classes, but it's no longer required if you just want what is configured in oauth2_settings, so remove all attributes that are just pointing at the configuration anyway. Add a setting ALWAYS_RELOAD_OAUTHLIB_CORE, which causes OAuthLibMixin to reload the OAuthLibCore object on each request. This is only intended to be used during testing, to allow the views to recognise changes in configuration. Show missing coverage lines in the coverage report. Fixes: jazzband#873 * Add tests for OIDC userinfo view * Add test for creating the OIDC issuer url for JWTs * Add ID token generation and validation tests * Add tests for OAuth2ProviderSettings * Add tests for IDToken model methods * Remove unnecessary __future__ declarations * Enable OIDC only views only when using OIDC Add a mixin for OIDC only views that checks that OIDC is correctly configured before allowing the request to continue. If OIDC is not enabled, raise an ImproperlyConfigured exception if the site is in DEBUG mode, otherwise log a warning and return a 404 response. * Remove mistakenly committed comment * Add OIDC documentation * Add OIDC support change to CHANGELOG.md * Support nonce and claims in OIDC auth * Add nonce and claims fields to Grant model. * Complete support for nonce in the OIDC auth code flow, and work around an oauthlib bug in the OIDC hybrid flow that caused a nonce presented in the authentication endpoint to be omitted from the ID token. * Switch to using `RequestValidator.finalize_id_token` instead of `RequestValidator.get_id_token`. This allows us to use the oauthlib stock implementation of `get_id_token`, which creates `at_hash` and `c_hash` when appropriate. * Support `claims` in the authentication endpoint to specify what claims are desired in the ID token, as per section 5.5. Interpreting the claims parameter is up to implementers. * Add documentation on using scopes and claims as authentication endpoint parameters to influence what claims to add to the tokens. * Add tests for `nonce` and `claims` handling. * Fix py3.5 tests getting scopes in unexpected order Not entirely sure how my previous changes caused that, but convert them to sets and compare the sets of scopes. * Simplify and optimise get_authorization_code_scopes `code` is sufficient for retrieving an auth code's scopes, and we don't need to do two DB queries where one will suffice. * Serve JWKs using well-known URL JWKs are commonly served from `.well-known/jwks.json`. This isn't a standard, but it is in common usage so it makes sense to conform. * Allow POST for OIDC UserInfo endpoint * Refactor how OIDC issuer url is generated Split out how to generate the OIDC issuer (when not specified in settings) out to a util function. Add tests Dont use reverse_lazy when the next thing we do with the url is to format it in to a string - no time for it to be lazy! * Support HS256 for OIDC * Add full support for HS256 signed OIDC keys. * Add a new default signing algorithm, NO_ALGORITHM. * Add docs on why you shouldn't choose HS256, and how to do it anyway. * Add docs on how to enable an app for OIDC. * Remove OIDC_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED and generate it depending on whether you have added an RSA key. * Add validation for your application signing algorithm so its not possible to accidentally setup something insecure. * Add `Application.jwk_key` property to load the correct key for an application. * Peek at a supplied id_token to determine the application it belongs to so that we can load the correct key to verify the signature. * Add an OIDC_ENABLED setting, as we can now enable OIDC without adding OIDC_RSA_PRIVATE_KEY. * Update and add new tests. * fix: json.loads on python 3.5 requires a str * Remove changes to create_authorization_response When using OIDC, the additional parameters nonce and claims are correctly extracted by oauthlib, we don't need to manually parse them out and pass them as a phony body parameter. Similarly, the django request is passed down to the final wrapper layer that calls create_authorization_response in oauthlib, we can defer calling `request.get_raw_uri()` until that point and drop passing `uri` down two function calls. This reverts create_authorization_response back to basically how it was pre-OIDC changes. * Reduce changes compared to master There were a few other places where stylistic changes have been made that are not a part of the changes. Revert them to make the patch less different compared to master. * Use simpler import name for oauthlib OIDC server * Fix another reference to oauthlib.openid.Server * Add indirect six dependency from jwcrpyto jwcrypto has a direct dependency on six, but does not list it yet in a release. Previously, cryptography also depended on six, so this was unnoticed. * Django master now supports only python 3.8+ * Store jti instead of the ID token contents Add a jti parameter to each ID token generated. After verifying a received ID token JWT, extract the jti claim to verify that the ID token exists in our database and is not expired. We don't require the contents of the JWT to verify that an ID token hasn't expired or been revoked, just the jti claim, so don't bother to store or index the ID token contents. Because we only would look at this when presented with an ID token JWT, all the claim contents are available in this JWT. Add missing scope check when verifying an ID token, add tests to verify this. Add functions _load_id_token and _load_access_token to OAuth2Validator, analagous to _save_id_token and _create_access_token. These can be overridden in sub-classes to customise loading behaviour, if these models have been swapped. * Code review changes * Don't swallow ValueError in ClientProtectedResourceMixin * Use OAuthLibCore._get_escaped_full_path to encode URI before passing to oauthlib. * You cannot create an ID Token using client credentials flow, so remove dead code handling this eventuality from OAuth2Validator._save_id_token * Remove TODO comment about saving IDTokens, it isn't necessary. * Generate the correct issuer URL from oauthlib OAuthlib will use OAuth2Validator.get_oidc_issuer_endpoint to generate the issuer endpoint; we create a phony django request to use django's mechanisms for generating the fully qualified URL. However, when SSL is enableld, not enough information is passed through to determine that the protocol is HTTPS. Adjust OAuthLibCore.extract_headers() to inject a custom header when the django request is secure. Use this extra header when generating the issuer URL to determine whether to set the protocol to "https", and use a custom django.http.HttpRequest subclass to allow us to set this. Adjust OAuthLibCore.create_authorization_response() to pass through headers to oauthlib to allow this header to be received. * Update OIDC docs * Krl/oidc round three fixes (jazzband#1) Add kid to id_token header, conditional on the algorithm used * Fix linting from PR * Handle invalid tokens in userinfo endpoint * Update a comment Co-authored-by: Dave Burkholder <[email protected]> Co-authored-by: Wiliam Souza <[email protected]> Co-authored-by: Allisson Azevedo <[email protected]> Co-authored-by: fvlima <[email protected]> Co-authored-by: Shaun Stanworth <[email protected]> Co-authored-by: Alan Crosswell <[email protected]> Co-authored-by: Kristian Rune Larsen <[email protected]>
1 parent c0a9ac9 commit b56987e

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)