|
2 | 2 | from unittest.mock import Mock, call, patch
|
3 | 3 |
|
4 | 4 | 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 |
6 | 7 | from django.conf import settings
|
7 | 8 | from django.contrib.auth import get_user_model
|
8 |
| -from django.core.exceptions import SuspiciousOperation |
| 9 | +from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation |
9 | 10 | from django.test import RequestFactory, TestCase, override_settings
|
10 | 11 | from django.utils.encoding import force_bytes, smart_str
|
11 | 12 | from josepy.b64 import b64encode
|
| 13 | +from josepy.jwa import ES256 |
12 | 14 |
|
13 | 15 | from mozilla_django_oidc.auth import OIDCAuthenticationBackend, default_username_algo
|
14 | 16 |
|
@@ -1203,3 +1205,87 @@ def dotted_username_algo_callback_with_claims(email, claims=None):
|
1203 | 1205 | domain = claims["domain"]
|
1204 | 1206 | username = f"{domain}/{email}"
|
1205 | 1207 | 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