diff --git a/CHANGES.rst b/CHANGES.rst index 719480fa..db63f058 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Fixed ^^^^^ - Canaille executable did not support i18n. :issue:`227` +- Dynamic `kid` parameter for JWKs. :issue:`222` [0.0.61] - 2025-02-04 --------------------- diff --git a/canaille/oidc/oauth.py b/canaille/oidc/oauth.py index 7ba003ba..f5300bc4 100644 --- a/canaille/oidc/oauth.py +++ b/canaille/oidc/oauth.py @@ -2,7 +2,6 @@ from authlib.integrations.flask_oauth2 import AuthorizationServer from authlib.integrations.flask_oauth2 import ResourceProtector -from authlib.jose import JsonWebKey from authlib.oauth2.rfc6749.grants import ( AuthorizationCodeGrant as _AuthorizationCodeGrant, ) @@ -31,6 +30,7 @@ from flask import g from flask import request from flask import url_for +from joserfc.jwk import JWKRegistry from werkzeug.security import gen_salt from canaille.app import DOCUMENTATION_URL @@ -150,12 +150,14 @@ def get_jwt_config(grant=None): def get_jwks(): kty = current_app.config["CANAILLE_OIDC"]["JWT"]["KTY"] alg = current_app.config["CANAILLE_OIDC"]["JWT"]["ALG"] - jwk = JsonWebKey.import_key( - current_app.config["CANAILLE_OIDC"]["JWT"]["PUBLIC_KEY"], {"kty": kty} + jwk = JWKRegistry.import_key( + current_app.config["CANAILLE_OIDC"]["JWT"]["PUBLIC_KEY"], kty ) + jwk.ensure_kid() return { "keys": [ { + "kid": jwk.kid, "use": "sig", "alg": alg, **jwk, diff --git a/pyproject.toml b/pyproject.toml index ce073b93..20f62d0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ front = [ oidc = [ "authlib >= 1.3.0", + "joserfc>=1.0.2", ] scim = [ diff --git a/tests/oidc/test_jwks.py b/tests/oidc/test_jwks.py index 3518444c..7451ca39 100644 --- a/tests/oidc/test_jwks.py +++ b/tests/oidc/test_jwks.py @@ -1,3 +1,5 @@ +from unittest import mock + from authlib.jose import JsonWebKey @@ -9,6 +11,7 @@ def test_jwks(testclient, keypair): assert res.json == { "keys": [ { + "kid": mock.ANY, "use": "sig", "alg": "RS256", **jwk, diff --git a/uv.lock b/uv.lock index 27c058e4..a9713405 100644 --- a/uv.lock +++ b/uv.lock @@ -177,6 +177,7 @@ mysql = [ ] oidc = [ { name = "authlib" }, + { name = "joserfc" }, ] otp = [ { name = "otpauth" }, @@ -271,6 +272,7 @@ requires-dist = [ { name = "flask-themer", marker = "extra == 'front'", specifier = ">=2.0.0" }, { name = "flask-wtf", specifier = ">=1.2.1" }, { name = "hypercorn", marker = "extra == 'server'", specifier = ">=0.17.3" }, + { name = "joserfc", marker = "extra == 'oidc'", specifier = ">=1.0.2" }, { name = "otpauth", marker = "extra == 'otp'", specifier = ">=2.1.1" }, { name = "passlib", marker = "extra == 'mysql'", specifier = ">=1.7.4" }, { name = "passlib", marker = "extra == 'postgresql'", specifier = ">=1.7.4" }, @@ -1037,6 +1039,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, ] +[[package]] +name = "joserfc" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/73/06a394569ecdb6decdee8cd2a916f2391730f269225b7c2416a2a806e270/joserfc-1.0.2.tar.gz", hash = "sha256:f3c4efa35a62fc100097e4bec2584f5fd21b878eee3eb5324bc9b37d2969b2a4", size = 170521 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/01/adc0fd12a64413fced1b5c85fa7af8e12b32430ccc736b43e3d29c52f714/joserfc-1.0.2-py3-none-any.whl", hash = "sha256:8d3e0253b0b24aeaf049d7bf7a847040dd6462de24cefd0c14abfc8b353f9619", size = 60931 }, +] + [[package]] name = "legacy-cgi" version = "2.6.2"