feat: Auto-propagate kid and alg from JWK to JWT header in RFC 7523 assertion signing#889
Conversation
ca4f8aa to
79d0d80
Compare
| from typing import Any | ||
|
|
||
| from joserfc import jwt | ||
| from joserfc._rfc7517.models import BaseKey |
There was a problem hiding this comment.
Better not import from a private module. You can use
from joserfc.jwk import OctKey, RSAKey, ECKey, OKPKey
if isinstance(key,(OctKey, RSAKey, ECKey, OKPKey)):| if isinstance(key, (OctKey, RSAKey, ECKey, OKPKey)): | ||
| parameter_value = key.get(parameter_name) | ||
| if parameter_value: | ||
| header[parameter_name] = parameter_value |
There was a problem hiding this comment.
I think you should use header.setdefault, if the given header contains alg field, you should not use the alg value from the key.
There was a problem hiding this comment.
if the given header contains alg field, you should not use the alg value from the key.
- client_secret_jwt_sign(...) uses
HS256as the default algorithm. private_key_jwt_sign(...) usesRS256as the default algorithm. These defaults overridekey.alg. As a result, the following usage is no longer supported. We have to manually provide the alg parameter to the function.
token = private_key_jwt_sign(
private_key=ed25519_key,
client_id="my-client",
token_endpoint="https://auth.example.com/token",
)
Likewise, the following pattern is no longer valid:
with OAuth2Session(
client_id,
client_secret,
token_endpoint_auth_method=ClientSecretJWT(token_endpoint),
token_endpoint=token_endpoint,
grant_type="client_credentials",
) as session:
Instead, the algorithm and headers must be specified explicitly:
with OAuth2Session(
client_id,
client_secret,
token_endpoint_auth_method=ClientSecretJWT(
token_endpoint,
alg=client_secret["alg"],
headers={"kid": client_secret.get("kid")},
),
token_endpoint=token_endpoint,
grant_type="client_credentials",
) as session:
- There are three possible sources for the algorithm (alg):
header.alg- The function parameter
alg key.alg
If either header.alg or the function parameter alg does not match key.alg, joserfc raises an exception and the function call fails.
In practice, the only way for users to avoid this failure is to ensure that header.alg matches key.alg. Therefore, key.alg effectively acts as a constraint on all other algorithm values and has the final authority over algorithm selection.
As a result, key.alg has the highest priority because it can reject alg values provided from any other source.
What kind of change does this PR introduce?
This is a feature implementation.
For the OAuth2 client credentials flow, when using
private_key_jwtorclient_secret_jwtauthentication with aJWKthat already containskidandalg, the caller still has to extract those values from the key and pass them separately, even though the key itself is already passed to the function. This is cumbersome, especially in projects with many OAuth2 clients, where every call site has to repeat the same boilerplate. E.g.,Before:
Solution: Automatically copy
kidandalgfrom theJWKinto theJWTheader via a new helperset_jwt_header_parameter_from_key(...). The key's value is an enforced constraint (joserfc raises ifheader alg != key alg), so it takes priority over any explicit value.Priority order:
key.alg / key.kid— highest priority (enforced by joserfc)alg/kidparameter — used when key has noalg/kidHS256forclient_secret_jwt_sign,RS256forprivate_key_jwt_signAfter:
Fully backward compatible:
alg/kid→ behavior unchanged, explicit params or defaults applyalg/kid→ they were already required to match (joserfc enforces this), so auto-propagation just removes the redundant boilerplateChecklist
prek.pragma: no cover