Algorithm whitelist and alg=none rejection
The rule
Every JWT/JWS verifier in the system (authorization server, MCP SDK, merchant SDK, wallet portal,
storefront) MUST refuse alg=none (RFC 7515 §10.6) and enforce an explicit allow-list:
| Context | Allow-list | Reject |
|---|---|---|
| Tokens the authorization server issues (JWT-AT, ID Token, mandate SD-JWT VC, software_statement) | EdDSA (Ed25519) | everything else for new tokens |
| Agent / Merchant DPoP proofs and client_assertion | EdDSA, ES256 | RS256 (unless legacy use case), RSA1_5, none |
| RFC 9421 signatures (Offer, Catalog) | ed25519, ecdsa-p256-sha256 | hmac-sha256, rsa-pss-sha512 |
| Externally introduced JWTs (federation partners) | EdDSA, ES256, RS256 (≥2048) | none, RSA1_5, RS256 < 2048 |
Mandatory tests: test_jwt_alg_none_rejected, test_jwt_rs256_rejected_outside_legacy, test_dpop_proof_hmac_rejected.
Why alg=none rejection matters
A verifier that respects the JWT header's alg claim without an allow-list will accept a
forged token whose header declares alg=none and whose body is whatever the attacker wants. This is the canonical JWT pitfall
(RFC 7515 §10.6). Every OID4Pay verifier enforces an explicit allow-list before signature verification.
Implementation pattern (Node)
import { jwtVerify } from "jose";
const { payload } = await jwtVerify(token, jwks, {
algorithms: ["EdDSA"], // explicit allow-list
issuer: "https://as.oid4pay.com",
audience: "https://shop.alpacanica.com",
typ: "at+jwt",
});Implementation pattern (Python)
from joserfc import jwt
from joserfc.jwk import KeySet
claims = jwt.decode(
token,
KeySet.import_key_set(as_jwks),
algorithms=["EdDSA"], # explicit allow-list
)
claims.validate(now=time.time())Implementation pattern (Go)
import "github.com/oid4pay/oid4ac-go/jwtverify"
claims, err := jwtverify.Verify(token, jwks,
jwtverify.WithAlgorithms("EdDSA"),
jwtverify.WithIssuer("https://as.oid4pay.com"),
jwtverify.WithAudience("https://shop.alpacanica.com"),
jwtverify.WithTyp("at+jwt"),
)SOPS key-generator default
The OID4Pay infra script infra/scripts/generate_oid4ac_keys.sh defaults to EdDSA; it emits ES256 only on explicit --alg=ES256 flag. Generating an RSA key requires --alg=RS256 --i-know-what-i-am-doing.
Mandatory test cases
Every SDK and every service ships these three tests in CI:
test_jwt_alg_none_rejected: a JWT with headeralg=noneis rejected.test_jwt_rs256_rejected_outside_legacy: an RS256-signed JWT is rejected on tokens the authorization server issues.test_dpop_proof_hmac_rejected: an HS256 DPoP proof is rejected.