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
kty values emit warn! and are excluded from the cache.
- Algorithm-confusion rejection (e.g., header claims
HS256 while the kid maps to an EC key pinned to ES256).
Definition of Done
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:
JwkKey deserializes only RSA material (n, e); no x, y, or crv for EC/OKP keys.
refresh_keys() skips any key where kty != "RSA" with a bare continue — no log line.
validate_token() hardcodes Validation::new(Algorithm::RS256) regardless of key type.
- On
kid miss after refresh, clients see invalid token: unknown signing key; only a debug! is emitted.
sandbox_jwt.rs already validates EdDSA with pinned validation.algorithms — a pattern to mirror.
- Existing
oidc.rs tests 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
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 keywhen the JWTkidrefers to a non-RSA key that was never cached. The skip path inrefresh_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.rsonly. No new dependencies — workspacejsonwebtoken9.3.1 already exposesDecodingKey::from_ec_componentsandfrom_ed_components.Extend JWK parsing
Add optional
x,y, andcrvfields toJwkKey(alongside existing RSAn/e).Cache decoding key and algorithm per
kidChange the in-memory cache from
HashMap<String, DecodingKey>toHashMap<String, (DecodingKey, Algorithm)>. Inrefresh_keys(), branch onkty:ktyRSAfrom_rsa_components(n, e)RS256(RS384/RS512 can follow via JWKalglater)ECfrom_ec_components(x, y)P-256→ES256,P-384→ES384OKPfrom_ed_components(x)EdDSAwarn!withktyandkidPin algorithm server-side during validation
Look up the stored
(DecodingKey, Algorithm)for the JWTkidand callValidation::new(stored_algorithm). Pinvalidation.algorithmsto the stored value (same pattern assandbox_jwt.rs) so headeralgmismatches 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):
ktyvalues emitwarn!and are excluded from the cache.HS256while thekidmaps to an EC key pinned toES256).Definition of Done
ES256,ES384) and EdDSA (Ed25519) JWKS keys work for OIDC Bearer authentication.ktyvalues log atwarn!withktyandkid.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.rsand fits the same cache pattern.Trust JWK
algovercrvderivation. RFC 7517 marksalgoptional; many issuers omit it. Deriving fromkty/crvis 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.rsand 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.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.oidc.rstests cover role/scope extraction only; no JWKS key-loading or multi-algorithm validation tests.Estimated scope: ~60 lines production change in
oidc.rsplus ~80 lines of unit tests. No Helm, config, or new crate dependencies.Checklist