You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a first-class outbound token framework for RAS services.
RAS services frequently need to call other systems. Sometimes the downstream system is a third-party API such as GitHub, Google, Slack, or a customer system. Sometimes it is another internal RAS service. Today each project tends to hand-roll token acquisition, grant/refresh-token storage, access-token caching, bearer-header attachment, host validation, and realistic fakes.
This issue should define the reusable outbound token/client framework with pluggable token sources. OAuth2/OIDC should be one token source for third-party APIs. RAS-internal JWT issuance should be another token source for internal service-to-service calls, backed by #13.
Developers using RAS frequently need to call external APIs, internal services, or customer systems.
Today each project tends to hand-roll:
where refresh tokens/grants come from and how they are stored
how access tokens are cached and refreshed
how internal service tokens are requested
how bearer tokens are attached to outbound requests
how refresh-token rotation is persisted
how to validate that tokens are only sent to the right downstream hosts
how to test integrations without hitting real providers
how to exercise the actual service code path in tests instead of replacing everything with mocks
Important terminology:
A refresh token is a stored grant. RAS should not magically know it; the application must provide a consent flow, seeded grant, or custom grant store. RAS should then use that grant to acquire and refresh access tokens.
Internal service-to-service tokens should not require Auth0/Entra service app setup. For internal services, Add RAS-native authorization control plane and internal service token issuer #13 should provide the RAS-owned service registry, authorization decision, and JWT issuer. This issue should consume that issuer through a token source.
Proposed Direction
Add a new integration-auth crate family, likely under crates/integration/:
ras-integration-core
TokenSource abstraction
token request/lease types
token manager
grant-store traits
access-token cache traits
in-memory stores for tests/dev
reqwest/header authorization helpers
redacted secret wrappers
capability-scoped integration clients so handlers cannot request arbitrary integrations/scopes/audiences
ras-integration-oauth2
generic OAuth2/OIDC token source
authorization-code + PKCE user grants
refresh-token flow
client-credentials service tokens for third-party providers that support it
strict state/callback validation for user consent flows
no Auth0/Entra app/client setup for internal services
ras-integration-test
in-process fake OAuth/token/API provider
fake TokenSource implementations
test helpers for consent, refresh, invalid scope, revoked grant, provider failure, internal token issuance failure, and downstream bearer verification
Keep this separate from existing inbound auth. Existing RAS sessions identify the logged-in application user. The authorization/control-plane work in #13 decides whether a user/service/application may obtain a downstream token. This issue handles how that token is acquired, cached, refreshed, and attached.
For browser traffic that fans out to multiple backend services, token narrowing should be handled by the optional auth gateway/proxy rather than by every backend. This issue should provide reusable token and attachment primitives that the gateway can use, but deploying a gateway must remain opt-in.
For multi-service systems, topology-generated artifacts from #15 can provide the route/audience/service graph policy that determines which tokens may be requested or attached.
Ordinary service code should prefer capability-scoped clients over constructing raw TokenRequest values:
let google = AuthorizedHttpClient::for_user(
reqwest::Client::new(),
token_manager.clone(),"google-calendar",
user.user_id.clone(),["calendar.readonly"],);
google.get(calendar_url).send().await?;
Internal service usage should look similar:
let invoice_api = AuthorizedHttpClient::for_service(
reqwest::Client::new(),
token_manager.clone(),"invoice-service",["invoice:write"],);
invoice_api.post(invoice_url).json(&request).send().await?;
For the internal case:
billing-service
-> asks token manager for token to call invoice-service
-> RasInternalTokenSource asks RAS issuer from #13
-> RAS checks local service registration/grants/audience
-> RAS issues signed JWT
-> AuthorizedHttpClient attaches the JWT
-> invoice-service validates via RAS JWKS
For a browser frontend behind the optional auth gateway/proxy:
browser sends RAS web session cookie/JWT
-> gateway validates RAS session locally
-> gateway maps route to target audience
-> gateway narrows permissions to that audience
-> gateway mints/caches short-lived service-specific token
-> backend receives only single-audience token
Backends should not need to parse permissions for unrelated audiences.
Internal service requests should distinguish principal mode instead of treating every call as the same subject:
service-as-service: the caller requests a token for its own service identity
user-delegated: the caller requests a downstream token on behalf of a RAS-authenticated user, constrained by both caller/service policy and the user's delegated permissions
application/service-account: the caller uses a non-human principal with explicit RAS grants
The v1 implementation may focus on service-as-service, but TokenSubject, cache keys, and RasInternalTokenSource should leave room for delegated calls without introducing a second token model later.
Grant injection paths for external OAuth providers:
custom GrantStore backed by Vault, KMS, SQL, Redis, etc.
Security And Threat Model Requirements
This layer must not become a broad token vending machine. The default ergonomic path should inject capability-scoped clients, not a raw global token manager. A handler should receive something like a preconfigured Google Calendar read client or invoice-service write client, not the ability to request any integration ID, scope, audience, or subject.
Primary assets:
refresh tokens/grants
OAuth client secrets
RAS internal service tokens
access tokens
user-to-provider account links
integration configuration, allowed hosts, allowed audiences, and allowed scopes
Primary trust boundaries:
browser OAuth callback into service
RAS session identity into integration grant lookup
OAuth CSRF/account-linking risk: consent state must bind to the RAS user/session, integration ID, provider/issuer, redirect URI, requested scopes/audience, and PKCE verifier.
Secret persistence risk: GrantStore is a security boundary, and refresh grants must not be accidentally logged, debug-printed, or serialized.
Bearer-token exfiltration risk: managed bearer tokens must only be attached to configured integration hosts/base URLs, with redirect behavior explicitly controlled.
Cross-tenant/cache-collision risk: cache and grant keys must include tenant/customer context where applicable, subject, provider/client identity, audience/resource, canonical scopes, and grant/config version.
Cache keys must also include token family/type and principal mode so external OAuth tokens, internal service tokens, delegated internal tokens, and gateway-derived tokens cannot collide.
Token-family confusion risk: external OAuth tokens, internal RAS JWTs, static/legacy tokens, and future token kinds must not share ambiguous cache keys or host/audience attachment rules.
Scope escalation risk: user token requests must be subset-checked against the stored grant scopes; broader scopes require a new consent flow.
Internal token leakage risk: internal RAS JWTs must never be attached to third-party hosts or unregistered internal service hosts.
Backend domain privacy risk: backend services should receive only their own audience-specific permissions, not the caller's full cross-service permission map, when the optional gateway mode is used.
Refresh-token rotation risk: rotated refresh tokens must be persisted transactionally enough that a successful token response does not silently lose the new grant.
Unsafe retry risk: automatic refresh-and-retry must not replay non-idempotent requests unless explicitly opted in.
A service can request an access token for a service identity using OAuth2 client credentials when configured for an external provider.
A service can request an access token for a RAS-authenticated user using a stored OAuth refresh grant.
A service can request an internal RAS-issued JWT for an internal downstream service through RasInternalTokenSource.
Internal token request types distinguish service-as-service, user-delegated, and application/service-account principals, even if v1 only fully implements service-as-service issuance.
Token primitives support an optional gateway mode that derives single-audience backend tokens from a RAS web session without requiring a RAS authority call per proxied request.
Missing user grants return a typed ConsentRequired error rather than falling back silently.
Ordinary handler code can use capability-scoped clients without being able to request arbitrary integration IDs, scopes, audiences, or subjects.
Integration configuration declares allowed scopes/audiences and allowed outbound hosts/base URLs; requests outside those bounds fail closed.
OAuth consent state is opaque, single-use, expiring, and bound to the initiating RAS user/session, integration, provider/issuer, redirect URI, scopes/audience, and PKCE verifier.
User token requests are subset-checked against stored grant scopes; broader scopes require a new consent flow.
Access tokens are cached by token source/family, integration, tenant/customer context where applicable, subject, provider/client identity, audience/resource, canonicalized scopes, and grant/config version.
Internal token cache keys include principal mode and target audience/service.
Internal RAS JWTs and external OAuth tokens cannot collide in cache keys and cannot be attached through the wrong host/audience policy.
Tokens refresh before expiry using configurable clock skew.
Refresh-token rotation is persisted back to the configured GrantStore; save failures are surfaced and covered by tests.
Concurrent refreshes for the same token key are de-duplicated.
Secrets are redacted in Debug, errors, and logs, and refresh-token types do not accidentally serialize through serde.
Integration-style service test where a RAS handler calls a fake downstream API through AuthorizedHttpClient; assert the fake API receives the expected bearer token.
Tests ensuring token and refresh-token values are never printed through debug/error paths.
Tests proving non-idempotent outbound requests are not automatically replayed after token refresh unless explicitly configured.
Tests proving a handler cannot request undeclared scopes or integrations through the default/capability-scoped API.
Tests proving external OAuth tokens and internal RAS JWTs use distinct cache keys and cannot be sent to each other's host classes.
Do not implement the full reverse proxy/gateway in this issue; that should be tracked separately.
Do not ship SQL/Redis/Vault persistence adapters in the first version.
Do not build provider-specific SDKs for Google/Auth0/Azure/GitHub in v1.
Do not implement non-OAuth signing schemes such as HMAC/API-key rotation in v1.
Notes
The key design goal is to let service code say “give me a token for this integration/user/scope” and keep token authorization, acquisition, caching, refresh, request authorization, and realistic fakes behind reusable RAS abstractions.
For internal services, the external IdP should not be involved. The internal path should be RAS-native:
Summary
Add a first-class outbound token framework for RAS services.
RAS services frequently need to call other systems. Sometimes the downstream system is a third-party API such as GitHub, Google, Slack, or a customer system. Sometimes it is another internal RAS service. Today each project tends to hand-roll token acquisition, grant/refresh-token storage, access-token caching, bearer-header attachment, host validation, and realistic fakes.
This issue should define the reusable outbound token/client framework with pluggable token sources. OAuth2/OIDC should be one token source for third-party APIs. RAS-internal JWT issuance should be another token source for internal service-to-service calls, backed by #13.
Related:
Problem
Developers using RAS frequently need to call external APIs, internal services, or customer systems.
Today each project tends to hand-roll:
Important terminology:
Proposed Direction
Add a new integration-auth crate family, likely under
crates/integration/:ras-integration-coreTokenSourceabstractionras-integration-oauth2ras-integration-rasRasInternalTokenSourceras-integration-testTokenSourceimplementationsKeep this separate from existing inbound auth. Existing RAS sessions identify the logged-in application user. The authorization/control-plane work in #13 decides whether a user/service/application may obtain a downstream token. This issue handles how that token is acquired, cached, refreshed, and attached.
For browser traffic that fans out to multiple backend services, token narrowing should be handled by the optional auth gateway/proxy rather than by every backend. This issue should provide reusable token and attachment primitives that the gateway can use, but deploying a gateway must remain opt-in.
For multi-service systems, topology-generated artifacts from #15 can provide the route/audience/service graph policy that determines which tokens may be requested or attached.
Core API Sketch
Generic token source:
Token request model:
Planned token sources:
Ordinary service code should prefer capability-scoped clients over constructing raw
TokenRequestvalues:Internal service usage should look similar:
For the internal case:
For a browser frontend behind the optional auth gateway/proxy:
Backends should not need to parse permissions for unrelated audiences.
Internal service requests should distinguish principal mode instead of treating every call as the same subject:
The v1 implementation may focus on service-as-service, but
TokenSubject, cache keys, andRasInternalTokenSourceshould leave room for delegated calls without introducing a second token model later.Grant injection paths for external OAuth providers:
Applications may acquire grants through:
GrantStorebacked by Vault, KMS, SQL, Redis, etc.Security And Threat Model Requirements
This layer must not become a broad token vending machine. The default ergonomic path should inject capability-scoped clients, not a raw global token manager. A handler should receive something like a preconfigured Google Calendar read client or invoice-service write client, not the ability to request any integration ID, scope, audience, or subject.
Primary assets:
Primary trust boundaries:
RasInternalTokenSourceThe implementation must address:
TokenRequestvalues from user-controlled input.RasInternalTokenSourcemust call Add RAS-native authorization control plane and internal service token issuer #13 for authorization and must not duplicate or bypass partial local policy checks.statemust bind to the RAS user/session, integration ID, provider/issuer, redirect URI, requested scopes/audience, and PKCE verifier.GrantStoreis a security boundary, and refresh grants must not be accidentally logged, debug-printed, or serialized.Acceptance Criteria
TokenSourceabstraction exists and can be used by the token manager.TokenSource.RasInternalTokenSourcebacked by Add RAS-native authorization control plane and internal service token issuer #13.RasInternalTokenSourcenever mints or returns a token without a successful authorization/issuance response from Add RAS-native authorization control plane and internal service token issuer #13.RasInternalTokenSource.ConsentRequirederror rather than falling back silently.GrantStore; save failures are surfaced and covered by tests.Debug, errors, and logs, and refresh-token types do not accidentally serialize through serde.set_bearer_tokenbehavior remains compatible.Test Plan
AuthorizedHttpClient; assert the fake API receives the expected bearer token.Non-Goals For v1
AuthProvider,SessionService, or existing identity crates.Notes
The key design goal is to let service code say “give me a token for this integration/user/scope” and keep token authorization, acquisition, caching, refresh, request authorization, and realistic fakes behind reusable RAS abstractions.
For internal services, the external IdP should not be involved. The internal path should be RAS-native: