Skip to content

Commit e647d51

Browse files
dopryn2ygkpre-commit-ci[bot]
authored
feat: Update PKCE_REQUIRED to true by default (#1129)
* feat: default PKCE_REQUIRED to True BREAKING CHANGE: set to False to maintain legacy behavior Co-authored-by: Alan Crosswell <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c79eae2 commit e647d51

File tree

10 files changed

+41
-16
lines changed

10 files changed

+41
-16
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,4 @@ Eduardo Oliveira
7373
Andrea Greco
7474
Dominik George
7575
David Hill
76+
Darrel O'Pry

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
on using Celery to automate clearing expired tokens.
2626

2727
### Changed
28+
* #1129 (**Breaking**) Changed default value of PKCE_REQUIRED to True. This is a **breaking change**. Clients without
29+
PKCE enabled will fail to authenticate. This breaks with [section 5 of RFC7636](https://datatracker.ietf.org/doc/html/rfc7636)
30+
in favor of the [OAuth2 Security Best Practices for Authorization Code Grants](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1).
31+
If you want to retain the pre-2.x behavior, set `PKCE_REQUIRED = False ` in your settings.py
32+
2833
* #1093 (**Breaking**) Changed to implement [hashed](https://docs.djangoproject.com/en/stable/topics/auth/passwords/)
2934
client_secret values. This is a **breaking change** that will migrate all your existing
3035
cleartext `application.client_secret` values to be hashed with Django's default password hashing algorithm

docs/settings.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,21 @@ will be used.
253253

254254
PKCE_REQUIRED
255255
~~~~~~~~~~~~~
256-
Default: ``False``
256+
Default: ``True``
257+
258+
Can be either a bool or a callable that takes a client id and returns a bool.
259+
260+
Whether or not `Proof Key for Code Exchange <https://oauth.net/2/pkce/>`_ is required.
261+
262+
According to `OAuth 2.0 Security Best Current Practice <https://oauth.net/2/oauth-best-practice/>`_ related to the
263+
`Authorization Code Grant <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1.>`_
264+
265+
- Public clients MUST use PKCE `RFC7636 <https://datatracker.ietf.org/doc/html/rfc7636>`_
266+
- For confidential clients, the use of PKCE `RFC7636 <https://datatracker.ietf.org/doc/html/rfc7636>`_ is RECOMMENDED.
267+
268+
269+
257270

258-
Whether or not PKCE is required. Can be either a bool or a callable that takes a client id and returns a bool.
259271

260272

261273
OIDC_RSA_PRIVATE_KEY

oauth2_provider/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"RESOURCE_SERVER_INTROSPECTION_CREDENTIALS": None,
9898
"RESOURCE_SERVER_TOKEN_CACHING_SECONDS": 36000,
9999
# Whether or not PKCE is required
100-
"PKCE_REQUIRED": False,
100+
"PKCE_REQUIRED": True,
101101
# Whether to re-create OAuthlibCore on every request.
102102
# Should only be required in testing.
103103
"ALWAYS_RELOAD_OAUTHLIB_CORE": False,

tests/presets.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"openid": "OpenID connect",
2020
},
2121
"DEFAULT_SCOPES": ["read", "write"],
22+
"PKCE_REQUIRED": False,
2223
}
2324
OIDC_SETTINGS_RO = deepcopy(OIDC_SETTINGS_RW)
2425
OIDC_SETTINGS_RO["DEFAULT_SCOPES"] = ["read"]

tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,4 @@
159159

160160
CLEAR_EXPIRED_TOKENS_BATCH_SIZE = 1
161161
CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL = 0
162+
PKCE_REQUIRED = False

tests/test_authorization_code.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def setUp(self):
4848
self.dev_user = UserModel.objects.create_user("dev_user", "[email protected]", "123456")
4949

5050
self.oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["http", "custom-scheme"]
51+
self.oauth2_settings.PKCE_REQUIRED = False
5152

5253
self.application = Application.objects.create(
5354
name="Test Application",
@@ -73,6 +74,7 @@ class TestRegressionIssue315(BaseTest):
7374
"""
7475

7576
def test_request_is_not_overwritten(self):
77+
self.oauth2_settings.PKCE_REQUIRED = False
7678
self.client.login(username="test_user", password="123456")
7779
response = self.client.get(
7880
reverse("oauth2_provider:authorize"),
@@ -94,6 +96,7 @@ def test_skip_authorization_completely(self):
9496
"""
9597
If application.skip_authorization = True, should skip the authorization page.
9698
"""
99+
self.oauth2_settings.PKCE_REQUIRED = False
97100
self.client.login(username="test_user", password="123456")
98101
self.application.skip_authorization = True
99102
self.application.save()
@@ -132,6 +135,7 @@ def test_pre_auth_valid_client(self):
132135
"""
133136
Test response for a valid client_id with response_type: code
134137
"""
138+
self.oauth2_settings.PKCE_REQUIRED = False
135139
self.client.login(username="test_user", password="123456")
136140

137141
query_data = {
@@ -644,7 +648,6 @@ def get_pkce_auth(self, code_challenge, code_challenge_method):
644648
"""
645649
Helper method to retrieve a valid authorization code using pkce
646650
"""
647-
self.oauth2_settings.PKCE_REQUIRED = True
648651
authcode_data = {
649652
"client_id": self.application.client_id,
650653
"state": "random_state_string",
@@ -1115,7 +1118,6 @@ def test_public_pkce_S256_authorize_get(self):
11151118
self.application.client_type = Application.CLIENT_PUBLIC
11161119
self.application.save()
11171120
code_verifier, code_challenge = self.generate_pkce_codes("S256")
1118-
self.oauth2_settings.PKCE_REQUIRED = True
11191121

11201122
query_data = {
11211123
"client_id": self.application.client_id,
@@ -1143,7 +1145,6 @@ def test_public_pkce_plain_authorize_get(self):
11431145
self.application.client_type = Application.CLIENT_PUBLIC
11441146
self.application.save()
11451147
code_verifier, code_challenge = self.generate_pkce_codes("plain")
1146-
self.oauth2_settings.PKCE_REQUIRED = True
11471148

11481149
query_data = {
11491150
"client_id": self.application.client_id,
@@ -1171,7 +1172,6 @@ def test_public_pkce_S256(self):
11711172
self.application.save()
11721173
code_verifier, code_challenge = self.generate_pkce_codes("S256")
11731174
authorization_code = self.get_pkce_auth(code_challenge, "S256")
1174-
self.oauth2_settings.PKCE_REQUIRED = True
11751175

11761176
token_request_data = {
11771177
"grant_type": "authorization_code",
@@ -1200,7 +1200,6 @@ def test_public_pkce_plain(self):
12001200
self.application.save()
12011201
code_verifier, code_challenge = self.generate_pkce_codes("plain")
12021202
authorization_code = self.get_pkce_auth(code_challenge, "plain")
1203-
self.oauth2_settings.PKCE_REQUIRED = True
12041203

12051204
token_request_data = {
12061205
"grant_type": "authorization_code",
@@ -1228,7 +1227,6 @@ def test_public_pkce_invalid_algorithm(self):
12281227
self.application.client_type = Application.CLIENT_PUBLIC
12291228
self.application.save()
12301229
code_verifier, code_challenge = self.generate_pkce_codes("invalid")
1231-
self.oauth2_settings.PKCE_REQUIRED = True
12321230

12331231
query_data = {
12341232
"client_id": self.application.client_id,
@@ -1250,13 +1248,13 @@ def test_public_pkce_missing_code_challenge(self):
12501248
Request an access token using client_type: public
12511249
and PKCE enabled but with the code_challenge missing
12521250
"""
1251+
self.oauth2_settings.PKCE_REQUIRED = True
12531252
self.client.login(username="test_user", password="123456")
12541253

12551254
self.application.client_type = Application.CLIENT_PUBLIC
12561255
self.application.skip_authorization = True
12571256
self.application.save()
12581257
code_verifier, code_challenge = self.generate_pkce_codes("S256")
1259-
self.oauth2_settings.PKCE_REQUIRED = True
12601258

12611259
query_data = {
12621260
"client_id": self.application.client_id,
@@ -1282,7 +1280,6 @@ def test_public_pkce_missing_code_challenge_method(self):
12821280
self.application.client_type = Application.CLIENT_PUBLIC
12831281
self.application.save()
12841282
code_verifier, code_challenge = self.generate_pkce_codes("S256")
1285-
self.oauth2_settings.PKCE_REQUIRED = True
12861283

12871284
query_data = {
12881285
"client_id": self.application.client_id,
@@ -1308,7 +1305,6 @@ def test_public_pkce_S256_invalid_code_verifier(self):
13081305
self.application.save()
13091306
code_verifier, code_challenge = self.generate_pkce_codes("S256")
13101307
authorization_code = self.get_pkce_auth(code_challenge, "S256")
1311-
self.oauth2_settings.PKCE_REQUIRED = True
13121308

13131309
token_request_data = {
13141310
"grant_type": "authorization_code",
@@ -1332,7 +1328,6 @@ def test_public_pkce_plain_invalid_code_verifier(self):
13321328
self.application.save()
13331329
code_verifier, code_challenge = self.generate_pkce_codes("plain")
13341330
authorization_code = self.get_pkce_auth(code_challenge, "plain")
1335-
self.oauth2_settings.PKCE_REQUIRED = True
13361331

13371332
token_request_data = {
13381333
"grant_type": "authorization_code",
@@ -1356,7 +1351,6 @@ def test_public_pkce_S256_missing_code_verifier(self):
13561351
self.application.save()
13571352
code_verifier, code_challenge = self.generate_pkce_codes("S256")
13581353
authorization_code = self.get_pkce_auth(code_challenge, "S256")
1359-
self.oauth2_settings.PKCE_REQUIRED = True
13601354

13611355
token_request_data = {
13621356
"grant_type": "authorization_code",
@@ -1379,7 +1373,6 @@ def test_public_pkce_plain_missing_code_verifier(self):
13791373
self.application.save()
13801374
code_verifier, code_challenge = self.generate_pkce_codes("plain")
13811375
authorization_code = self.get_pkce_auth(code_challenge, "plain")
1382-
self.oauth2_settings.PKCE_REQUIRED = True
13831376

13841377
token_request_data = {
13851378
"grant_type": "authorization_code",

tests/test_hybrid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def setUp(self):
5252
self.factory = RequestFactory()
5353
self.hy_test_user = UserModel.objects.create_user("hy_test_user", "[email protected]", "123456")
5454
self.hy_dev_user = UserModel.objects.create_user("hy_dev_user", "[email protected]", "123456")
55-
55+
self.oauth2_settings.PKCE_REQUIRED = False
5656
self.oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["http", "custom-scheme"]
5757

5858
self.application = Application(

tests/test_scopes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def test_scopes_saved_in_grant(self):
8383
"""
8484
Test scopes are properly saved in grant
8585
"""
86+
self.oauth2_settings.PKCE_REQUIRED = False
8687
self.client.login(username="test_user", password="123456")
8788

8889
# retrieve a valid authorization code
@@ -105,6 +106,7 @@ def test_scopes_save_in_access_token(self):
105106
"""
106107
Test scopes are properly saved in access token
107108
"""
109+
self.oauth2_settings.PKCE_REQUIRED = False
108110
self.client.login(username="test_user", password="123456")
109111

110112
# retrieve a valid authorization code
@@ -141,6 +143,7 @@ def test_scopes_protection_valid(self):
141143
"""
142144
Test access to a scope protected resource with correct scopes provided
143145
"""
146+
self.oauth2_settings.PKCE_REQUIRED = False
144147
self.client.login(username="test_user", password="123456")
145148

146149
# retrieve a valid authorization code
@@ -183,6 +186,7 @@ def test_scopes_protection_fail(self):
183186
"""
184187
Test access to a scope protected resource with wrong scopes provided
185188
"""
189+
self.oauth2_settings.PKCE_REQUIRED = False
186190
self.client.login(username="test_user", password="123456")
187191

188192
# retrieve a valid authorization code
@@ -225,6 +229,7 @@ def test_multi_scope_fail(self):
225229
"""
226230
Test access to a multi-scope protected resource with wrong scopes provided
227231
"""
232+
self.oauth2_settings.PKCE_REQUIRED = False
228233
self.client.login(username="test_user", password="123456")
229234

230235
# retrieve a valid authorization code
@@ -267,6 +272,7 @@ def test_multi_scope_valid(self):
267272
"""
268273
Test access to a multi-scope protected resource with correct scopes provided
269274
"""
275+
self.oauth2_settings.PKCE_REQUIRED = False
270276
self.client.login(username="test_user", password="123456")
271277

272278
# retrieve a valid authorization code
@@ -308,6 +314,7 @@ def test_multi_scope_valid(self):
308314

309315
class TestReadWriteScope(BaseTest):
310316
def get_access_token(self, scopes):
317+
self.oauth2_settings.PKCE_REQUIRED = False
311318
self.client.login(username="test_user", password="123456")
312319

313320
# retrieve a valid authorization code

tests/test_settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,8 @@ def test_generating_iss_endpoint_type_error(oauth2_settings):
167167
with pytest.raises(TypeError) as exc:
168168
oauth2_settings.oidc_issuer(None)
169169
assert str(exc.value) == "request must be a django or oauthlib request: got None"
170+
171+
172+
def test_pkce_required_is_default():
173+
settings = OAuth2ProviderSettings()
174+
assert settings.PKCE_REQUIRED is True

0 commit comments

Comments
 (0)