Skip to content

Cache loading of JWK object from OIDC private key #1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Bart Merenda
Bas van Oostveen
Brian Helba
Carl Schwan
Daniel Golding
Daniel 'Vector' Kerr
Darrel O'Pry
Dave Burkholder
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security
-->

## [unreleased]

### Added
* #1273 Add caching of loading of OIDC private key.

## [2.3.0] 2023-05-31

### WARNING
Expand Down
3 changes: 2 additions & 1 deletion oauth2_provider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .generators import generate_client_id, generate_client_secret
from .scopes import get_scopes_backend
from .settings import oauth2_settings
from .utils import jwk_from_pem
from .validators import RedirectURIValidator, WildcardSet


Expand Down Expand Up @@ -234,7 +235,7 @@ def jwk_key(self):
if self.algorithm == AbstractApplication.RS256_ALGORITHM:
if not oauth2_settings.OIDC_RSA_PRIVATE_KEY:
raise ImproperlyConfigured("You must set OIDC_RSA_PRIVATE_KEY to use RSA algorithm")
return jwk.JWK.from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY.encode("utf8"))
return jwk_from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY)
elif self.algorithm == AbstractApplication.HS256_ALGORITHM:
return jwk.JWK(kty="oct", k=base64url_encode(self.client_secret))
raise ImproperlyConfigured("This application does not support signed tokens")
Expand Down
12 changes: 12 additions & 0 deletions oauth2_provider/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import functools

from jwcrypto import jwk


@functools.lru_cache()
def jwk_from_pem(pem_string):
"""
A cached version of jwcrypto.JWK.from_pem.
Converting from PEM is expensive for large keys such as those using RSA.
"""
return jwk.JWK.from_pem(pem_string.encode("utf-8"))
5 changes: 3 additions & 2 deletions oauth2_provider/views/oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView, View
from jwcrypto import jwk, jwt
from jwcrypto import jwt
from jwcrypto.common import JWException
from jwcrypto.jws import InvalidJWSObject
from jwcrypto.jwt import JWTExpired
Expand All @@ -30,6 +30,7 @@
get_refresh_token_model,
)
from ..settings import oauth2_settings
from ..utils import jwk_from_pem
from .mixins import OAuthLibMixin, OIDCLogoutOnlyMixin, OIDCOnlyMixin


Expand Down Expand Up @@ -114,7 +115,7 @@ def get(self, request, *args, **kwargs):
oauth2_settings.OIDC_RSA_PRIVATE_KEY,
*oauth2_settings.OIDC_RSA_PRIVATE_KEYS_INACTIVE,
]:
key = jwk.JWK.from_pem(pem.encode("utf8"))
key = jwk_from_pem(pem)
data = {"alg": "RS256", "use": "sig", "kid": key.thumbprint()}
data.update(json.loads(key.export_public()))
keys.append(data)
Expand Down
27 changes: 27 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from oauth2_provider import utils


def test_jwk_from_pem_caches_jwk():
a_tiny_rsa_key = """-----BEGIN RSA PRIVATE KEY-----
MGQCAQACEQCxqYaL6GtPooVMhVwcZrCfAgMBAAECECyNmdsuHvMqIEl9/Fex27kC
CQDlc0deuSVrtQIJAMY4MTw2eCeDAgkA5VzfMykQ5yECCQCgkF4Zl0nHPwIJALPv
+IAFUPv3
-----END RSA PRIVATE KEY-----"""

# For the same private key we expect the same object to be returned

jwk1 = utils.jwk_from_pem(a_tiny_rsa_key)
jwk2 = utils.jwk_from_pem(a_tiny_rsa_key)

assert jwk1 is jwk2

a_different_tiny_rsa_key = """-----BEGIN RSA PRIVATE KEY-----
MGMCAQACEQCvyNNNw4J201yzFVogcfgnAgMBAAECEE3oXe5bNlle+xU4EVHTUIEC
CQDpSvwIvDMSIQIJAMDk47DzG9FHAghtvg1TWpy3oQIJAL6NHlS+RBufAgkA6QLA
2GK4aDc=
-----END RSA PRIVATE KEY-----"""

# But for a different key, a different object
jwk3 = utils.jwk_from_pem(a_different_tiny_rsa_key)

assert jwk3 is not jwk1