Skip to content

OpenID Connect support #915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Mar 17, 2021
Merged

Conversation

tevansuk
Copy link
Contributor

@tevansuk tevansuk commented Jan 12, 2021

Fixes #110

Description of the Change

DRAFT

Adds support for OIDC authorization code flow and hybrid flow. This is mostly not my work; I've just rebased #859 onto the latest master and added support for swappable ModelAdmins for the new models. It's quite a large change, so I've put it up for review prior to working on finishing off the actual PR (see checklist).

Thanks to Dave Burkholder, William Souza, Allisson Azevedo, fvlima and Shaun Stanworth for their initial work on rounds 1 and 2 of OIDC support 👍

Checklist

Significant changes that still need to be made:

  • PR only contains one change (considered splitting up PR)
  • unit-test added
  • CHANGELOG.md updated (only for user relevant changes)
  • author name in AUTHORS
  • don't break existing non OIDC setups ('OIDC_RSA_PRIVATE_KEY' is mandatory #873)
  • documentation on enabling/customising OIDC is missing
  • don't double produce id token - save it in the authorization endpoint and retrieve it in token endpoint
  • helper functions for checking when at_hash and c_hash are required
  • support id_token_hint (RequestValidator.validate_user_hint)
  • support authorization code nonce (RequestValidator.get_authorization_code_nonce)

thinkwelltwd and others added 2 commits January 7, 2021 17:26
* 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 <dave@thinkwelldesigns.com>
Co-authored-by: Wiliam Souza <wiliamsouza83@gmail.com>
Co-authored-by: Allisson Azevedo <allisson@gmail.com>
Co-authored-by: fvlima <frederico.vieira@gmail.com>
Co-authored-by: Shaun Stanworth <shaun.stanworth@googlemail.com>
@codecov
Copy link

codecov bot commented Jan 12, 2021

Codecov Report

Merging #915 (ec06198) into master (c0a9ac9) will increase coverage by 1.52%.
The diff coverage is 99.68%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #915      +/-   ##
==========================================
+ Coverage   95.07%   96.59%   +1.52%     
==========================================
  Files          30       31       +1     
  Lines        1420     1704     +284     
==========================================
+ Hits         1350     1646     +296     
+ Misses         70       58      -12     
Impacted Files Coverage Δ
oauth2_provider/views/application.py 96.77% <ø> (ø)
oauth2_provider/oauth2_backends.py 94.39% <92.85%> (-0.24%) ⬇️
oauth2_provider/admin.py 100.00% <100.00%> (ø)
oauth2_provider/forms.py 100.00% <100.00%> (ø)
oauth2_provider/models.py 98.66% <100.00%> (+0.39%) ⬆️
oauth2_provider/oauth2_validators.py 93.57% <100.00%> (+2.22%) ⬆️
oauth2_provider/settings.py 100.00% <100.00%> (+16.04%) ⬆️
oauth2_provider/urls.py 100.00% <100.00%> (ø)
oauth2_provider/views/__init__.py 100.00% <100.00%> (ø)
oauth2_provider/views/base.py 97.79% <100.00%> (-0.02%) ⬇️
... and 6 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c0a9ac9...ec06198. Read the comment docs.

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: django-oauth#873
@auvipy auvipy requested a review from n2ygk January 25, 2021 15:05
@tevansuk
Copy link
Contributor Author

There's still more to come :)

I'm working this week on adding more tests + documentation, and then on the TODO items on the checklist

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.
* 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.
Not entirely sure how my previous changes caused that, but convert them
to sets and compare the sets of 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.
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.
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!
* 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.
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.
@Natureshadow
Copy link
Contributor

This feature would be much appreciated. We are trying to get OpenID Connect provider support in AlekSIS and are currently struggling to integrate django-oidc-provider with django-oauth-toolkit. Having django-oauth-toolkit be able to do the whole job in time for our release would simplify things a lot!

@Natureshadow
Copy link
Contributor

Natureshadow commented Mar 5, 2021

We got it working in staging, with one issue:

In settings.oidc_issuer, when the request is an oauthlib.common.Request, the scheme is not determined correctly (there simply is no way for the django_request object that is constructed to determine whether the site uses HTTPS or not). Thus, the issuer URL in the discovery document ends up to be correct (with https scheme), whereis the JWT will contain the issueer URL with http scheme.

We worked around that by setting Django's SECURE_PROXY_SSL_HEADER setting to something that forces Django's HttpRequest.is_secure() to return True, but that's not a solution (for instance, it will not work if used without a proxy).

Copy link
Contributor

@n2ygk n2ygk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really great. Just a few questions.

tevansuk added 2 commits March 9, 2021 16:08
* 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.
@tevansuk
Copy link
Contributor Author

tevansuk commented Mar 9, 2021

We got it working in staging, with one issue:

In settings.oidc_issuer, when the request is an oauthlib.common.Request, the scheme is not determined correctly (there simply is no way for the django_request object that is constructed to determine whether the site uses HTTPS or not). Thus, the issuer URL in the discovery document ends up to be correct (with https scheme), whereis the JWT will contain the issueer URL with http scheme.

We worked around that by setting Django's SECURE_PROXY_SSL_HEADER setting to something that forces Django's HttpRequest.is_secure() to return True, but that's not a solution (for instance, it will not work if used without a proxy).

Ah, that's a bit of a pain. You can also fix the issuer by setting it explicitly in settings, but it would be preferable to avoid adding a setting just to get SSL working correctly. We have some control over how oauthlib.Request objects get created, I'll see if I can smuggle through whether the request is SSL or not and use that to create the phony django request.

tevansuk and others added 9 commits March 9, 2021 23:28
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.
Add kid to id_token header, conditional on the algorithm used
@n2ygk
Copy link
Contributor

n2ygk commented Mar 17, 2021

@tevansuk Are you still making changes to this PR? I had begun reviewing it but will wait if you aren't ready.

@tevansuk
Copy link
Contributor Author

@tevansuk Are you still making changes to this PR? I had begun reviewing it but will wait if you aren't ready.

@n2ygk I'm not planning any more changes at this point. The last two changes were from bug reports I've received:

  • Missing kid in the ID token header
  • Userinfo endpoint raises an unhandled exception if the id token used is expired or invalid.

I'll triage new bug fixes into a new PR to avoid changing this PR any more 👍

@n2ygk
Copy link
Contributor

n2ygk commented Mar 17, 2021

I'll triage new bug fixes into a new PR to avoid changing this PR any more 👍

@tevansuk Sounds like a plan. I assume once we release this as a 1.5.0 release -- hopefully in the next day or two -- there may be a flurry of bug reports, etc. which we can quickly push out as small patch releases. This many commits in a single release is difficult to review.

Thanks.

Copy link
Contributor

@n2ygk n2ygk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not convinced about that redirect_uri change you've made. Please convince me otherwise!

@n2ygk n2ygk merged commit b56987e into django-oauth:master Mar 17, 2021
@Natureshadow
Copy link
Contributor

Great work, this is a really good addition!

Making a release very soon would be of much help to the AlekSIS project.

tswfi added a commit to TampereHacklab/mulysa that referenced this pull request Jun 4, 2021
tswfi added a commit to TampereHacklab/mulysa that referenced this pull request Sep 19, 2021
tswfi added a commit to TampereHacklab/mulysa that referenced this pull request Sep 21, 2021
* rebased

* use bleeding edge from django-oauth/django-oauth-toolkit#915

* update readme, example of extra data provided to keycloack with mulysaoauthvalidator

* Update dependencies

* use the released version, make flake8 happy

* blacken settings.py while we are here

* and black urls.py

* and more black

* pipenv lock

* add mxid_full and mxid_local_part to returned data

* Revert "add mxid_full and mxid_local_part to returned data"

This reverts commit 4a656ff.
@dopry
Copy link
Member

dopry commented Oct 1, 2024

@tevansuk hey I'm a new DOT maintainer. I noticed pytest got added to the project in this PR. I'm trying to get some historic context regarding the mix of test fixture libraries in the project. Do you remember why you chose to add pytest over just working with django.test and decorators like @override_settings?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

OpenID Connect
10 participants