Skip to content

Commit

Permalink
Merge pull request #513 from akatsoulas/ec-support
Browse files Browse the repository at this point in the history
Add support for Elliptic Curve signing algorithm
  • Loading branch information
akatsoulas authored Dec 27, 2023
2 parents 74693ba + ffba5cc commit f75ff62
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 6 deletions.
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
9 changes: 7 additions & 2 deletions mozilla_django_oidc/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ def __init__(self, *args, **kwargs):
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."
Expand Down Expand Up @@ -199,7 +202,9 @@ def verify_token(self, token, **kwargs):
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:
Expand Down
90 changes: 88 additions & 2 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
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

Expand Down Expand Up @@ -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()

0 comments on commit f75ff62

Please sign in to comment.