Skip to content

feat(auth): OIDC JWKS support for EC and EdDSA signing keys #2196

Description

@lunarwhite

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-256ES256, P-384ES384
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

  • EC (ES256, ES384) and EdDSA (Ed25519) JWKS keys work for OIDC Bearer authentication.
  • Unsupported kty values log at warn! with kty and kid.
  • 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-commit and mise run test pass.

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

  • I've reviewed existing issues and the architecture docs
  • This is a design proposal, not a "please build this" request

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:gatewayGateway server and control-plane worktopic:compatibilityCompatibility-related work

    Fields

    No fields configured for Enhancement.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions