Skip to content

Commit ffba5cc

Browse files
atanunqakatsoulas
authored andcommitted
Add support for Elliptic Curve signing algorithm
* Add tests for ES256 * Mention ES256 in the documentation * Add comments in tests
1 parent 74693ba commit ffba5cc

File tree

4 files changed

+98
-6
lines changed

4 files changed

+98
-6
lines changed

HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pending
88
=======
99

1010
* Added PKCE support in the authorization code flow.
11+
* Added support for Elliptic Curve JWT signing algorithms
1112

1213

1314
3.0.0 (2022-11-14)

docs/installation.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ Depending on your OpenID Connect provider (OP) you might need to change the
5454
default signing algorithm from ``HS256`` to ``RS256`` by settings the
5555
``OIDC_RP_SIGN_ALGO`` value accordingly.
5656

57-
For ``RS256`` algorithm to work, you need to set either the OP signing key or
58-
the OP JWKS Endpoint.
57+
For ``RS256`` and ``ES256`` algorithms to work, you need to set either the
58+
OP signing key or the OP JWKS Endpoint.
5959

6060
The corresponding settings values are:
6161

mozilla_django_oidc/auth.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ def __init__(self, *args, **kwargs):
5555
self.OIDC_RP_SIGN_ALGO = self.get_settings("OIDC_RP_SIGN_ALGO", "HS256")
5656
self.OIDC_RP_IDP_SIGN_KEY = self.get_settings("OIDC_RP_IDP_SIGN_KEY", None)
5757

58-
if self.OIDC_RP_SIGN_ALGO.startswith("RS") and (
58+
if (
59+
self.OIDC_RP_SIGN_ALGO.startswith("RS")
60+
or self.OIDC_RP_SIGN_ALGO.startswith("ES")
61+
) and (
5962
self.OIDC_RP_IDP_SIGN_KEY is None and self.OIDC_OP_JWKS_ENDPOINT is None
6063
):
6164
msg = "{} alg requires OIDC_RP_IDP_SIGN_KEY or OIDC_OP_JWKS_ENDPOINT to be configured."
@@ -199,7 +202,9 @@ def verify_token(self, token, **kwargs):
199202
nonce = kwargs.get("nonce")
200203

201204
token = force_bytes(token)
202-
if self.OIDC_RP_SIGN_ALGO.startswith("RS"):
205+
if self.OIDC_RP_SIGN_ALGO.startswith("RS") or self.OIDC_RP_SIGN_ALGO.startswith(
206+
"ES"
207+
):
203208
if self.OIDC_RP_IDP_SIGN_KEY is not None:
204209
key = self.OIDC_RP_IDP_SIGN_KEY
205210
else:

tests/test_auth.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from unittest.mock import Mock, call, patch
33

44
from cryptography.hazmat.backends import default_backend
5-
from cryptography.hazmat.primitives import hashes, hmac
5+
from cryptography.hazmat.primitives import hashes, hmac, serialization
6+
from cryptography.hazmat.primitives.asymmetric import ec
67
from django.conf import settings
78
from django.contrib.auth import get_user_model
8-
from django.core.exceptions import SuspiciousOperation
9+
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
910
from django.test import RequestFactory, TestCase, override_settings
1011
from django.utils.encoding import force_bytes, smart_str
1112
from josepy.b64 import b64encode
13+
from josepy.jwa import ES256
1214

1315
from mozilla_django_oidc.auth import OIDCAuthenticationBackend, default_username_algo
1416

@@ -1203,3 +1205,87 @@ def dotted_username_algo_callback_with_claims(email, claims=None):
12031205
domain = claims["domain"]
12041206
username = f"{domain}/{email}"
12051207
return username
1208+
1209+
1210+
@override_settings(OIDC_OP_TOKEN_ENDPOINT="https://server.example.com/token")
1211+
@override_settings(OIDC_OP_USER_ENDPOINT="https://server.example.com/user")
1212+
@override_settings(OIDC_RP_CLIENT_ID="example_id")
1213+
@override_settings(OIDC_RP_CLIENT_SECRET="client_secret")
1214+
@override_settings(OIDC_RP_SIGN_ALGO="ES256")
1215+
class OIDCAuthenticationBackendES256WithJwksEndpointTestCase(TestCase):
1216+
"""Authentication tests with ALG ES256 and IpD JWKS Endpoint."""
1217+
1218+
def test_es256_alg_misconfiguration(self):
1219+
"""Test that ES algorithm requires a JWKS endpoint"""
1220+
1221+
with self.assertRaises(ImproperlyConfigured) as ctx:
1222+
OIDCAuthenticationBackend()
1223+
1224+
self.assertEqual(
1225+
ctx.exception.args[0],
1226+
"ES256 alg requires OIDC_RP_IDP_SIGN_KEY or OIDC_OP_JWKS_ENDPOINT to be configured.",
1227+
)
1228+
1229+
@patch("mozilla_django_oidc.auth.requests")
1230+
@override_settings(OIDC_OP_JWKS_ENDPOINT="https://server.example.com/jwks")
1231+
def test_es256_alg_verification(self, mock_requests):
1232+
"""Test that token can be verified with the ES algorithm"""
1233+
1234+
self.backend = OIDCAuthenticationBackend()
1235+
1236+
# Generate a private key to create a test token with
1237+
private_key = ec.generate_private_key(ec.SECP256R1, default_backend())
1238+
private_key_pem = private_key.private_bytes(
1239+
serialization.Encoding.PEM,
1240+
serialization.PrivateFormat.PKCS8,
1241+
serialization.NoEncryption(),
1242+
)
1243+
1244+
# Make the public key available through the JWKS response
1245+
public_numbers = private_key.public_key().public_numbers()
1246+
get_json_mock = Mock()
1247+
get_json_mock.json.return_value = {
1248+
"keys": [
1249+
{
1250+
"kid": "eckid",
1251+
"kty": "EC",
1252+
"alg": "ES256",
1253+
"use": "sig",
1254+
"x": smart_str(b64encode(public_numbers.x.to_bytes(32, "big"))),
1255+
"y": smart_str(b64encode(public_numbers.y.to_bytes(32, "big"))),
1256+
"crv": "P-256",
1257+
}
1258+
]
1259+
}
1260+
mock_requests.get.return_value = get_json_mock
1261+
1262+
header = force_bytes(
1263+
json.dumps(
1264+
{
1265+
"typ": "JWT",
1266+
"alg": "ES256",
1267+
"kid": "eckid",
1268+
},
1269+
)
1270+
)
1271+
data = {"name": "John Doe", "test": "test_es256_alg_verification"}
1272+
1273+
h = hmac.HMAC(private_key_pem, hashes.SHA256(), backend=default_backend())
1274+
msg = "{}.{}".format(
1275+
smart_str(b64encode(header)),
1276+
smart_str(b64encode(force_bytes(json.dumps(data)))),
1277+
)
1278+
h.update(force_bytes(msg))
1279+
1280+
signature = b64encode(ES256.sign(private_key, force_bytes(msg)))
1281+
token = "{}.{}".format(
1282+
msg,
1283+
smart_str(signature),
1284+
)
1285+
1286+
# Verify the token created with the private key by using the JWKS endpoint,
1287+
# where the public numbers are.
1288+
payload = self.backend.verify_token(token)
1289+
1290+
self.assertEqual(payload, data)
1291+
mock_requests.get.assert_called_once()

0 commit comments

Comments
 (0)