Skip to content

Commit 178f315

Browse files
authored
Merge branch 'master' into fix/introspection
2 parents 241bb01 + 621574c commit 178f315

File tree

12 files changed

+117
-11
lines changed

12 files changed

+117
-11
lines changed

AUTHORS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ David Smith
2626
Diego Garcia
2727
Dulmandakh Sukhbaatar
2828
Dylan Giesler
29+
Dylan Tack
2930
Emanuele Palazzetti
3031
Federico Dolce
3132
Frederico Vieira
@@ -40,6 +41,7 @@ Jonathan Steffan
4041
Jozef Knaperek
4142
Jun Zhou
4243
Kristian Rune Larsen
44+
Michael Howitz
4345
Paul Dekkers
4446
Paul Oswald
4547
Pavel Tvrdík

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
### Added
2323
* #712, #636, #808. Calls to `django.contrib.auth.authenticate()` now pass a `request`
2424
to provide compatibility with backends that need one.
25+
* #950 Add support for RSA key rotation.
2526

2627
### Fixed
2728
* #524 Restrict usage of timezone aware expire dates to Django projects with USE_TZ set to True.

docs/getting_started.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ Export the credential as an environment variable
358358
359359
export CREDENTIAL=YXhYU1NCVnV2T3lHVnpoNFB1cnZLYXE1TUhYTW03RnRySGdETWk0dToxZnV2NVdWZlI3QTVCbEYwbzE1NUg3czViTGdYbHdXTGhpM1k3cGRKOWFKdUNkbDBYVjVDeGdkMHRyaTduU3pDODBxeXJvdmg4cUZYRkhnRkFBYzBsZFBObjVaWUxhbnhTbTFTSTFyeGxScldVUDU5MXdwSERHYTNwU3BCNmRDWg==
360360
361-
To start the Client Credential flow you call ``/token/`` endpoint direct::
361+
To start the Client Credential flow you call ``/token/`` endpoint directly::
362362

363363
curl -X POST -H "Authorization: Basic ${CREDENTIAL}" -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" "http://127.0.0.1:8000/o/token/" -d "grant_type=client_credentials"
364364

docs/oidc.rst

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@ change this class to derive from ``oauthlib.openid.Server`` instead of
100100
With ``RSA`` key-pairs, the public key can be generated from the private key,
101101
so there is no need to add a setting for the public key.
102102

103+
104+
Rotating the RSA private key
105+
~~~~~~~~~~~~~~~~~~~~~~~~
106+
Extra keys can be published in the jwks_uri with the ``OIDC_RSA_PRIVATE_KEYS_INACTIVE``
107+
setting. For example:::
108+
109+
OAUTH2_PROVIDER = {
110+
"OIDC_RSA_PRIVATE_KEY": os.environ.get("OIDC_RSA_PRIVATE_KEY"),
111+
"OIDC_RSA_PRIVATE_KEYS_INACTIVE": [
112+
os.environ.get("OIDC_RSA_PRIVATE_KEY_2"),
113+
os.environ.get("OIDC_RSA_PRIVATE_KEY_3")
114+
]
115+
# ... other settings
116+
}
117+
118+
To rotate, follow these steps:
119+
120+
#. Generate a new key, and add it to the inactive set. Then deploy the app.
121+
#. Swap the active and inactive keys, then re-deploy.
122+
#. After some reasonable amount of time, remove the inactive key. At a minimum,
123+
you should wait ``ID_TOKEN_EXPIRE_SECONDS`` to ensure the key isn't removed
124+
before valid tokens expire.
125+
126+
103127
Using ``HS256`` keys
104128
~~~~~~~~~~~~~~~~~~~~
105129

@@ -297,7 +321,7 @@ query, and other details.
297321
JwksInfoView
298322
~~~~~~~~~~~~
299323

300-
Available at ``/o/.well-known/jwks.json``, this view provides details of the key used to sign
324+
Available at ``/o/.well-known/jwks.json``, this view provides details of the keys used to sign
301325
the JWTs generated for ID tokens, so that clients are able to verify them.
302326

303327

docs/settings.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,25 @@ Default: ``""``
264264

265265
The RSA private key used to sign OIDC ID tokens. If not set, OIDC is disabled.
266266

267+
OIDC_RSA_PRIVATE_KEYS_INACTIVE
268+
~~~~~~~~~~~~~~~~~~~~
269+
Default: ``[]``
270+
271+
An array of *inactive* RSA private keys. These keys are not used to sign tokens,
272+
but are published in the jwks_uri location.
273+
274+
This is useful for providing a smooth transition during key rotation.
275+
``OIDC_RSA_PRIVATE_KEY`` can be replaced, and recently decommissioned keys
276+
should be retained in this inactive list.
277+
278+
OIDC_JWKS_MAX_AGE_SECONDS
279+
~~~~~~~~~~~~~~~~~~~~~~
280+
Default: ``3600``
281+
282+
The max-age value for the Cache-Control header on jwks_uri.
283+
284+
This enables the verifier to safely cache the JWK Set and not have to re-download
285+
the document for every token.
267286

268287
OIDC_USERINFO_ENDPOINT
269288
~~~~~~~~~~~~~~~~~~~~~~

docs/tutorial/tutorial_01.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Include the Django OAuth Toolkit urls in your `urls.py`, choosing the urlspace y
3333

3434
.. code-block:: python
3535
36+
from django.urls import path, include
37+
3638
urlpatterns = [
3739
path("admin", admin.site.urls),
3840
path("o/", include('oauth2_provider.urls', namespace='oauth2_provider')),

docs/tutorial/tutorial_03.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,21 @@ which takes care of token verification. In your settings.py:
1515

1616
.. code-block:: python
1717
18-
AUTHENTICATION_BACKENDS = (
18+
AUTHENTICATION_BACKENDS = [
1919
'oauth2_provider.backends.OAuth2Backend',
2020
# Uncomment following if you want to access the admin
21-
#'django.contrib.auth.backends.ModelBackend'
21+
#'django.contrib.auth.backends.ModelBackend',
2222
'...',
23-
)
23+
]
2424
25-
MIDDLEWARE = (
25+
MIDDLEWARE = [
2626
'...',
2727
# If you use SessionAuthenticationMiddleware, be sure it appears before OAuth2TokenMiddleware.
2828
# SessionAuthenticationMiddleware is NOT required for using django-oauth-toolkit.
2929
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
3030
'oauth2_provider.middleware.OAuth2TokenMiddleware',
3131
'...',
32-
)
32+
]
3333
3434
You will likely use the `django.contrib.auth.backends.ModelBackend` along with the OAuth2 backend
3535
(or you might not be able to log in into the admin), only pay attention to the order in which

oauth2_provider/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
"OIDC_ISS_ENDPOINT": "",
7373
"OIDC_USERINFO_ENDPOINT": "",
7474
"OIDC_RSA_PRIVATE_KEY": "",
75+
"OIDC_RSA_PRIVATE_KEYS_INACTIVE": [],
76+
"OIDC_JWKS_MAX_AGE_SECONDS": 3600,
7577
"OIDC_RESPONSE_TYPES_SUPPORTED": [
7678
"code",
7779
"token",

oauth2_provider/views/oidc.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,23 @@ class JwksInfoView(OIDCOnlyMixin, View):
7171
def get(self, request, *args, **kwargs):
7272
keys = []
7373
if oauth2_settings.OIDC_RSA_PRIVATE_KEY:
74-
key = jwk.JWK.from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY.encode("utf8"))
75-
data = {"alg": "RS256", "use": "sig", "kid": key.thumbprint()}
76-
data.update(json.loads(key.export_public()))
77-
keys.append(data)
74+
for pem in [
75+
oauth2_settings.OIDC_RSA_PRIVATE_KEY,
76+
*oauth2_settings.OIDC_RSA_PRIVATE_KEYS_INACTIVE,
77+
]:
78+
79+
key = jwk.JWK.from_pem(pem.encode("utf8"))
80+
data = {"alg": "RS256", "use": "sig", "kid": key.thumbprint()}
81+
data.update(json.loads(key.export_public()))
82+
keys.append(data)
7883
response = JsonResponse({"keys": keys})
7984
response["Access-Control-Allow-Origin"] = "*"
85+
response["Cache-Control"] = (
86+
"Cache-Control: public, "
87+
+ f"max-age={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}, "
88+
+ f"stale-while-revalidate={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}, "
89+
+ f"stale-if-error={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}"
90+
)
8091
return response
8192

8293

tests/presets.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"OIDC_ISS_ENDPOINT": "http://localhost/o",
1313
"OIDC_USERINFO_ENDPOINT": "http://localhost/o/userinfo/",
1414
"OIDC_RSA_PRIVATE_KEY": settings.OIDC_RSA_PRIVATE_KEY,
15+
"OIDC_RSA_PRIVATE_KEYS_INACTIVE": settings.OIDC_RSA_PRIVATE_KEYS_INACTIVE,
1516
"SCOPES": {
1617
"read": "Reading scope",
1718
"write": "Writing scope",

0 commit comments

Comments
 (0)