|
| 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