Skip to content

feat: support user-subject dynamic token grants for sandbox agents #1987

Description

@grs

Problem Statement

OpenShell can dynamically obtain provider tokens for sandbox traffic, but the existing client_credentials flow represents the sandbox/supervisor client rather than the end user. That works for service-to-
service access, but it does not preserve user delegation semantics for APIs that need to authorize based on both:

  • the human user who initiated or owns the sandbox activity
  • the sandbox agent identity that is acting on that user's behalf

For OIDC/OAuth providers that support token exchange, the desired final access token should keep the user as the token subject while identifying the sandbox agent as the authorized party (azp) or equivalent
client identity. This lets downstream services apply user-level authorization, auditing, and policy decisions without losing the fact that the request came from an OpenShell sandbox agent.

The value is strongest for enterprise APIs where access should be delegated, attributable, and scoped: the target service can answer "which user is this for?" and "which sandbox/agent is acting?" from a single
presented token.

Proposed Design

Extend dynamic provider token grants so a provider profile can request OAuth 2.0 token exchange (urn:ietf:params:oauth:grant-type:token-exchange) as an alternative to the existing client_credentials grant.

At a high level, the flow should be:

  1. A user authenticates to the OpenShell gateway using OIDC.
  2. The CLI can create or update a provider credential from the current OIDC token, for example via provider create/update --from-oidc-token.
  3. A provider profile declares a dynamic credential with token_grant.grant_type: token_exchange.
  4. The profile also declares the stored user token as the subject_token for the exchange.
  5. When sandbox traffic matches a profile endpoint requiring the dynamic credential, the supervisor requests an intermediate subject token from the gateway.
  6. The supervisor presents its SPIFFE JWT-SVID to the gateway with that request.
  7. The gateway validates the supervisor SVID and uses the SVID subject as the requested audience for the intermediate token, preventing the supervisor from forging arbitrary audiences.
  8. The gateway performs an intermediate token exchange using its own SPIFFE JWT-SVID as the client assertion and the stored user token as the subject token.
  9. The gateway returns the intermediate token to the supervisor.
  10. The supervisor performs the final token exchange at the same configured token endpoint, using:
    • its own SPIFFE JWT-SVID as the client assertion
    • the intermediate token as the subject token
    • the configured final audience/scope for the target API
  11. The supervisor injects the resulting access token into the outbound HTTP request according to the profile credential placement rules.

The intended final token semantics are:

  • sub or equivalent subject claim identifies the user
  • azp or equivalent authorized-party/client claim identifies the sandbox/supervisor SPIFFE identity
  • the token audience and scopes are suitable for the target API
  • the target API receives only the final exchanged token, not the original user token or intermediate token

Security constraints:

  • The gateway must not allow the supervisor to choose an arbitrary intermediate-token audience.
  • The intermediate audience should be derived from the validated supervisor SVID subject.
  • The gateway should validate the supervisor SVID against the SPIFFE OIDC/JWKS discovery material.
  • The gateway should use its own SPIFFE JWT-SVID as the client assertion for the intermediate exchange.
  • Stored user subject tokens should be treated as provider credentials and should expire according to their token expiry.
  • Updating a provider from the current OIDC token should reuse the existing provider update path where possible.

Profile shape:

credentials:
  - name: subject_token
    required: true

  - name: access_token
    auth_style: bearer
    token_grant:
      grant_type: token_exchange
      token_endpoint: https://idp.example.com/realms/example/protocol/openid-connect/token
      audience: target-api
      jwt_svid_audience: https://idp.example.com/realms/example
      client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer
      requested_token_type: urn:ietf:params:oauth:token-type:access_token
      subject_token:
        source: provider_credential
        credential: subject_token
        subject_token_type: urn:ietf:params:oauth:token-type:access_token

CLI behavior:

- provider create --from-oidc-token should create a provider credential containing the current user access token when the profile declares a token-exchange subject token.
- provider update --from-oidc-token should refresh or replace the stored subject-token credential.
- If a profile has exactly one token-exchange subject credential, the CLI can infer the destination credential.
- If multiple subject-token credentials are declared, the user should specify the destination credential explicitly.


### Alternatives Considered

### Continue using client_credentials

This is simpler and already supported, but the resulting token represents the sandbox/supervisor client rather than the user. It is not sufficient for user-level delegated authorization or audit trails.

### Give the sandbox the user's raw OIDC token

This would preserve the user subject, but it exposes the user's token directly to sandbox code and does not bind use of the token to the sandbox agent identity. It also makes it harder to scope the final
audience and lifetime for the target API.

### Let the supervisor request any intermediate audience

This would be flexible, but unsafe. The gateway should derive the intermediate audience from the verified supervisor SVID subject so the supervisor cannot mint an intermediate token for another client
identity.

### Put all audience configuration in the profile

The final target audience belongs in the profile, but the intermediate audience may only be known after sandbox creation because it depends on the supervisor SPIFFE identity. The gateway should derive that
intermediate audience at request time.


### Agent Investigation

The current dynamic token grant path is profile-driven and performed by the supervisor so it can use the sandbox/supervisor SPIFFE identity for client assertions. That makes token exchange a natural extension
of the existing token grant behavior.

The implementation requires work across:

- provider profile schema and validation for grant_type: token_exchange
- provider create/update CLI support for storing a user OIDC token as a provider credential
- gateway RPC support for supervisor-requested intermediate subject-token exchange
- SPIFFE JWT-SVID parsing and validation in shared/server code
- supervisor token grant logic to perform the two-stage exchange
- cache keying so exchanged tokens are scoped by provider, endpoint, audience, grant type, subject-token credential, and provider revision
- docs and examples showing the user-subject / sandbox-agent-authorized-party flow


### Checklist

- [x] I've reviewed existing issues and the architecture docs
- [x] 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
    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