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