Add support for Elliptic Curve signing algorithm
* Add tests for ES256 * Mention ES256 in the documentation * Add comments in tests
This commit is contained in:
Родитель
74693ba090
Коммит
ffba5cc58c
|
@ -8,6 +8,7 @@ pending
|
|||
=======
|
||||
|
||||
* Added PKCE support in the authorization code flow.
|
||||
* Added support for Elliptic Curve JWT signing algorithms
|
||||
|
||||
|
||||
3.0.0 (2022-11-14)
|
||||
|
|
|
@ -54,8 +54,8 @@ Depending on your OpenID Connect provider (OP) you might need to change the
|
|||
default signing algorithm from ``HS256`` to ``RS256`` by settings the
|
||||
``OIDC_RP_SIGN_ALGO`` value accordingly.
|
||||
|
||||
For ``RS256`` algorithm to work, you need to set either the OP signing key or
|
||||
the OP JWKS Endpoint.
|
||||
For ``RS256`` and ``ES256`` algorithms to work, you need to set either the
|
||||
OP signing key or the OP JWKS Endpoint.
|
||||
|
||||
The corresponding settings values are:
|
||||
|
||||
|
|
|
@ -55,7 +55,10 @@ class OIDCAuthenticationBackend(ModelBackend):
|
|||
self.OIDC_RP_SIGN_ALGO = self.get_settings("OIDC_RP_SIGN_ALGO", "HS256")
|
||||
self.OIDC_RP_IDP_SIGN_KEY = self.get_settings("OIDC_RP_IDP_SIGN_KEY", None)
|
||||
|
||||
if self.OIDC_RP_SIGN_ALGO.startswith("RS") and (
|
||||
if (
|
||||
self.OIDC_RP_SIGN_ALGO.startswith("RS")
|
||||
or self.OIDC_RP_SIGN_ALGO.startswith("ES")
|
||||
) and (
|
||||
self.OIDC_RP_IDP_SIGN_KEY is None and self.OIDC_OP_JWKS_ENDPOINT is None
|
||||
):
|
||||
msg = "{} alg requires OIDC_RP_IDP_SIGN_KEY or OIDC_OP_JWKS_ENDPOINT to be configured."
|
||||
|
@ -199,7 +202,9 @@ class OIDCAuthenticationBackend(ModelBackend):
|
|||
nonce = kwargs.get("nonce")
|
||||
|
||||
token = force_bytes(token)
|
||||
if self.OIDC_RP_SIGN_ALGO.startswith("RS"):
|
||||
if self.OIDC_RP_SIGN_ALGO.startswith("RS") or self.OIDC_RP_SIGN_ALGO.startswith(
|
||||
"ES"
|
||||
):
|
||||
if self.OIDC_RP_IDP_SIGN_KEY is not None:
|
||||
key = self.OIDC_RP_IDP_SIGN_KEY
|
||||
else:
|
||||
|
|
|
@ -2,13 +2,15 @@ import json
|
|||
from unittest.mock import Mock, call, patch
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, hmac
|
||||
from cryptography.hazmat.primitives import hashes, hmac, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.utils.encoding import force_bytes, smart_str
|
||||
from josepy.b64 import b64encode
|
||||
from josepy.jwa import ES256
|
||||
|
||||
from mozilla_django_oidc.auth import OIDCAuthenticationBackend, default_username_algo
|
||||
|
||||
|
@ -1203,3 +1205,87 @@ def dotted_username_algo_callback_with_claims(email, claims=None):
|
|||
domain = claims["domain"]
|
||||
username = f"{domain}/{email}"
|
||||
return username
|
||||
|
||||
|
||||
@override_settings(OIDC_OP_TOKEN_ENDPOINT="https://server.example.com/token")
|
||||
@override_settings(OIDC_OP_USER_ENDPOINT="https://server.example.com/user")
|
||||
@override_settings(OIDC_RP_CLIENT_ID="example_id")
|
||||
@override_settings(OIDC_RP_CLIENT_SECRET="client_secret")
|
||||
@override_settings(OIDC_RP_SIGN_ALGO="ES256")
|
||||
class OIDCAuthenticationBackendES256WithJwksEndpointTestCase(TestCase):
|
||||
"""Authentication tests with ALG ES256 and IpD JWKS Endpoint."""
|
||||
|
||||
def test_es256_alg_misconfiguration(self):
|
||||
"""Test that ES algorithm requires a JWKS endpoint"""
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as ctx:
|
||||
OIDCAuthenticationBackend()
|
||||
|
||||
self.assertEqual(
|
||||
ctx.exception.args[0],
|
||||
"ES256 alg requires OIDC_RP_IDP_SIGN_KEY or OIDC_OP_JWKS_ENDPOINT to be configured.",
|
||||
)
|
||||
|
||||
@patch("mozilla_django_oidc.auth.requests")
|
||||
@override_settings(OIDC_OP_JWKS_ENDPOINT="https://server.example.com/jwks")
|
||||
def test_es256_alg_verification(self, mock_requests):
|
||||
"""Test that token can be verified with the ES algorithm"""
|
||||
|
||||
self.backend = OIDCAuthenticationBackend()
|
||||
|
||||
# Generate a private key to create a test token with
|
||||
private_key = ec.generate_private_key(ec.SECP256R1, default_backend())
|
||||
private_key_pem = private_key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption(),
|
||||
)
|
||||
|
||||
# Make the public key available through the JWKS response
|
||||
public_numbers = private_key.public_key().public_numbers()
|
||||
get_json_mock = Mock()
|
||||
get_json_mock.json.return_value = {
|
||||
"keys": [
|
||||
{
|
||||
"kid": "eckid",
|
||||
"kty": "EC",
|
||||
"alg": "ES256",
|
||||
"use": "sig",
|
||||
"x": smart_str(b64encode(public_numbers.x.to_bytes(32, "big"))),
|
||||
"y": smart_str(b64encode(public_numbers.y.to_bytes(32, "big"))),
|
||||
"crv": "P-256",
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_requests.get.return_value = get_json_mock
|
||||
|
||||
header = force_bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"typ": "JWT",
|
||||
"alg": "ES256",
|
||||
"kid": "eckid",
|
||||
},
|
||||
)
|
||||
)
|
||||
data = {"name": "John Doe", "test": "test_es256_alg_verification"}
|
||||
|
||||
h = hmac.HMAC(private_key_pem, hashes.SHA256(), backend=default_backend())
|
||||
msg = "{}.{}".format(
|
||||
smart_str(b64encode(header)),
|
||||
smart_str(b64encode(force_bytes(json.dumps(data)))),
|
||||
)
|
||||
h.update(force_bytes(msg))
|
||||
|
||||
signature = b64encode(ES256.sign(private_key, force_bytes(msg)))
|
||||
token = "{}.{}".format(
|
||||
msg,
|
||||
smart_str(signature),
|
||||
)
|
||||
|
||||
# Verify the token created with the private key by using the JWKS endpoint,
|
||||
# where the public numbers are.
|
||||
payload = self.backend.verify_token(token)
|
||||
|
||||
self.assertEqual(payload, data)
|
||||
mock_requests.get.assert_called_once()
|
||||
|
|
Загрузка…
Ссылка в новой задаче