Skip to content

feat(auth): support OIDC UserInfo validation for opaque access tokens #2154

Description

@delgadof

Problem Statement

OpenShell gateway OIDC authentication currently assumes the OAuth 2.0 access_token is a JWT that can be verified locally with the issuer's JWKS. OAuth 2.0 also permits opaque access tokens, and some conforming OIDC providers return an opaque access_token alongside a JWT id_token.

The CLI can complete Authorization Code with PKCE and store that token bundle, but protected gateway RPCs reject the opaque bearer token as invalid token. Substituting the id_token is not appropriate because an ID token is evidence for the OIDC client, not a bearer credential for the gateway API.

OpenShell needs a provider-neutral, opt-in way to validate opaque OIDC access tokens without weakening the existing local JWT validation path.

Proposed Design

Add an OIDC access-token validation strategy to gateway configuration:

[openshell.gateway.oidc]
token_validation = "userinfo" # "jwt" remains the default
userinfo_cache_ttl_secs = 30

The behavior would be:

  • jwt: preserve the current JWKS, issuer, audience, expiry, and claims validation behavior.
  • userinfo: discover userinfo_endpoint from the issuer metadata and call it with the OAuth access token in the Authorization: Bearer header.
  • Accept a standard JSON UserInfo claims object or a signed JWT UserInfo response. Continue to validate signed JWT responses with the existing JWKS, issuer, audience, and expiry checks.
  • Require sub and map the resulting claims into OpenShell's existing provider-neutral Identity representation.
  • Do not follow redirects on UserInfo requests so a bearer token cannot be forwarded to another origin.
  • Map UserInfo 401 and 403 responses to unauthenticated. Map network failures and other upstream failures to unavailable.
  • Cache only successful identities for a short configurable TTL. Key the cache with a SHA-256 token digest; never store or log the plaintext bearer token.
  • Keep the mode explicit. Do not automatically fall back from JWT parsing to a network request.

Expose the strategy consistently through gateway TOML, CLI/environment settings, Helm values, startup diagnostics, and the gateway authentication documentation. The implementation should contain no provider-specific branches.

Alternatives Considered

Require every provider to issue JWT access tokens. This preserves local validation but excludes standards-compliant providers that intentionally issue reference or opaque access tokens.

Use the OIDC id_token as the gateway bearer token. This confuses authentication evidence with API authorization. The ID token's audience is the OAuth client, and it should not replace the access token.

Automatically call UserInfo when JWT parsing fails. This makes network behavior dependent on untrusted token syntax, hides configuration mistakes, and can add avoidable load. An explicit strategy is easier to operate and audit.

Add RFC 7662 token introspection in the same change. Introspection is a useful future strategy, but it requires confidential-client credentials and endpoint configuration that are not part of standard OIDC discovery. Keeping it separate makes this proposal smaller and avoids introducing secret distribution into gateway auth.

Standards Alignment

  • OAuth 2.0 access tokens may be self-contained or reference credentials, so a conforming provider does not have to issue JWT access tokens.
  • OpenID Connect UserInfo defines a protected resource that returns claims about the subject represented by an access token.
  • RFC 7662 token introspection remains a separate future validation strategy rather than part of this change.

Agent Investigation

  • The current gateway path is implemented in crates/openshell-server/src/auth/oidc.rs, where access tokens are parsed and validated as JWTs against discovered JWKS.
  • crates/openshell-bootstrap/src/oidc_token.rs stores the OAuth access token and optional refresh token used by the CLI.
  • crates/openshell-server/src/auth/identity.rs already provides the provider-neutral identity boundary needed by either validation strategy.
  • docs/reference/gateway-auth.mdx documents Authorization Code with PKCE and the existing gateway bearer-token flow.
  • A live compatibility test against a conforming OIDC provider confirmed the failing shape: PKCE login succeeds, the OAuth response contains an opaque access token and JWT ID token, and the discovered UserInfo endpoint returns signed identity claims. A provider-neutral prototype successfully authenticated protected gateway RPCs with that opaque token and preserved the existing JWT validation default.

Test Considerations

  • Preserve the existing JWT/JWKS validation tests as the default behavior.
  • Add unit tests for JSON and signed-JWT UserInfo responses, rejected tokens, malformed responses, positive caching, and redirect rejection.
  • Add configuration and Helm rendering tests for both validation strategies and authentication-only claim settings.
  • Verify a protected gateway RPC with an opaque access token against a conforming OIDC provider. Never log or assert plaintext token values.

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

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions