Repository navigation
feat(auth): OIDC JWKS support for EC and EdDSA signing keys #2196
Description
Problem Statement
The gateway OIDC authenticator only ingests RSA keys from the issuer JWKS endpoint. ECDSA (kty: "EC", ES256/ES384) and EdDSA (kty: "OKP", Ed25519) keys are silently dropped during JWKS refresh, so tokens signed with those keys cannot authenticate against protected gRPC methods.
Today this manifests as invalid token: unknown signing key when the JWT kid refers to a non-RSA key that was never cached. The skip path in refresh_keys() has no warning log, so operators running at default log levels get no signal that keys were discarded.
This blocks common OIDC deployments that sign with EC keys (Auth0/Okta tenants configured for ES256/ES384, Dex, Zitadel, and other self-hosted providers). Keycloak defaults to RS256 and is unaffected. Microsoft Entra ID JWKS remains RSA-only as of mid-2026, so Entra deployments are not impacted today.
OIDC support landed in #935 targeting Keycloak/RS256; subsequent auth work has not broadened JWKS key-type handling.
Proposed Design
Scope: crates/openshell-server/src/auth/oidc.rs only. No new dependencies — workspace jsonwebtoken 9.3.1 already exposes DecodingKey::from_ec_components and from_ed_components.
Extend JWK parsing
Add optional x, y, and crv fields to JwkKey (alongside existing RSA n/e).
Cache decoding key and algorithm per kid
Change the in-memory cache from HashMap<String, DecodingKey> to HashMap<String, (DecodingKey, Algorithm)>. In refresh_keys(), branch on kty:
kty |
Construction | Pinned algorithm |
|---|---|---|
RSA |
from_rsa_components(n, e) |
RS256 (RS384/RS512 can follow via JWK alg later) |
EC |
from_ec_components(x, y) |
P-256 → ES256, P-384 → ES384 |
OKP |
from_ed_components(x) |
EdDSA |
| other | skip | warn! with kty and kid |
Pin algorithm server-side during validation
Look up the stored (DecodingKey, Algorithm) for the JWT kid and call Validation::new(stored_algorithm). Pin validation.algorithms to the stored value (same pattern as sandbox_jwt.rs) so header alg mismatches are rejected before decode.
Do not select the verification algorithm from the JWT header alone. That enables algorithm-confusion attacks (RFC 8725 §3.2). The algorithm must be derived from trusted JWKS metadata (kty/crv), not attacker-controlled header data.
Tests
Unit tests with fixture JWKS covering RSA, EC (P-256 and P-384), and OKP (Ed25519):
- Each key type loads into the cache with the correct pinned algorithm.
- Tokens signed with each key type validate successfully.
- Unsupported
ktyvalues emitwarn!and are excluded from the cache. - Algorithm-confusion rejection (e.g., header claims
HS256while thekidmaps to an EC key pinned toES256).
Definition of Done
- EC (
ES256,ES384) and EdDSA (Ed25519) JWKS keys work for OIDC Bearer authentication. - Unsupported
ktyvalues log atwarn!withktyandkid. - Algorithm is pinned per key from JWKS metadata, never from the JWT header alone.
- Unit tests cover all three key families and algorithm-confusion rejection.
-
mise run pre-commitandmise run testpass.
Alternatives Considered
Accept algorithm from JWT header. Rejected — classic algorithm-confusion vulnerability. Pin one algorithm per cached key from JWKS kty/crv.
Warn-only first PR (log skipped keys, defer parsing). Useful incremental de-risk for operator visibility, but does not restore authentication on its own.
EC only, defer EdDSA. Closes the Auth0/Okta ES256 gap but leaves OKP unsupported. EdDSA is already proven in sandbox_jwt.rs and fits the same cache pattern.
Trust JWK alg over crv derivation. RFC 7517 marks alg optional; many issuers omit it. Deriving from kty/crv is more reliable.
Bug report instead of feature. The silent skip is poor diagnostics, but the root gap is incomplete JWKS support scoped to Keycloak/RS256 at introduction. A feature request with a concrete design is the better vehicle for multi-algorithm verification.
Agent Investigation
Explored crates/openshell-server/src/auth/oidc.rs and related auth code to confirm feasibility.
Key findings:
JwkKeydeserializes only RSA material (n,e); nox,y, orcrvfor EC/OKP keys.refresh_keys()skips any key wherekty != "RSA"with a barecontinue— no log line.validate_token()hardcodesValidation::new(Algorithm::RS256)regardless of key type.- On
kidmiss after refresh, clients seeinvalid token: unknown signing key; only adebug!is emitted. sandbox_jwt.rsalready validates EdDSA with pinnedvalidation.algorithms— a pattern to mirror.- Existing
oidc.rstests cover role/scope extraction only; no JWKS key-loading or multi-algorithm validation tests. - No upstream issue found for this gap.
Estimated scope: ~60 lines production change in oidc.rs plus ~80 lines of unit tests. No Helm, config, or new crate dependencies.
Checklist
- I've reviewed existing issues and the architecture docs
- This is a design proposal, not a "please build this" request
Activity
lunarwhite commentedon Jul 9, 2026
I would like to work on this once/if it's accepted
drew commentedon Jul 10, 2026
varshaprasad96 commentedon Jul 15, 2026
Hey @lunarwhite, just checking in, if you'd like to take up this issue, or I can create a PR for this feature. Thanks!
lunarwhite commentedon Jul 15, 2026
@varshaprasad96 Thanks for your interest! Yes I'm working on it. I'll post it here once PR is ready. You could review it then if you'd like. :)
github-actions commentedon Aug 2, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
lunarwhite commentedon Aug 3, 2026
PR is up: #2593 and ready for review.