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
Wire upstream token enrichment into the auth middleware and simplify the upstreamswap middleware by removing its storage dependency. After this change, the TokenValidator eagerly loads all upstream access tokens into Identity.UpstreamTokens at JWT validation time, so upstreamswap only needs to read identity.UpstreamTokens[cfg.ProviderName] directly — no storage call, no context extraction, no ServiceGetter pattern.
Context
RFC-0052 introduces a sequential authorization chain that accumulates tokens from multiple upstream IDPs per session. The current upstreamswap middleware retrieves the correct upstream token by calling InProcessService.GetValidTokens(ctx, tsid) at request time, which couples the middleware to the UpstreamTokenStorage dependency via a ServiceGetter function. This phase decouples that by moving the storage read into the auth middleware layer — a single GetAllUpstreamTokens bulk read happens once per request during JWT validation and the results are stored in the Identity struct. All downstream middleware (including upstreamswap) then read from the pre-enriched Identity with no additional storage round-trips.
This is Phase 4 of the RFC-0052 implementation. It depends on Phase 1 (#4136), which introduced the GetAllUpstreamTokens bulk-read method on UpstreamTokenStorage. It may be developed in parallel with TASK-002 and TASK-003 after TASK-001 merges.
Identity struct in pkg/auth/identity.go has a new UpstreamTokens map[string]string field (providerName → access token) with a doc comment stating it is populated by auth middleware after JWT validation, contains expired tokens as-is, and is empty for non-embedded-AS requests
Identity.MarshalJSON() redacts UpstreamTokens values (replaces each token value with "REDACTED") while keeping provider name keys visible; provider names are safe to log but token values are not
Identity.String() does not leak upstream token values (the existing format Identity{Subject:%q} is sufficient; no additional changes required unless token values would otherwise appear)
TokenValidator struct in pkg/auth/token.go has a new optional field upstreamStorage storage.UpstreamTokenStorage (nil = no enrichment; used for non-embedded-AS deployments where no storage exists)
Between claimsToIdentity (~line 1087) and WithIdentity (~line 1096) in TokenValidator.Middleware, when v.upstreamStorage is non-nil and the identity has a tsid claim, GetAllUpstreamTokens(r.Context(), tsid) is called and the result is used to populate identity.UpstreamTokens
When GetAllUpstreamTokens returns an error, the middleware logs at WARN level and continues with an empty (or nil) UpstreamTokens map — the error is non-fatal and the request proceeds
When the tsid claim is absent or v.upstreamStorage is nil, identity.UpstreamTokens remains nil/empty and the request proceeds without error
GetAuthenticationMiddleware in pkg/auth/utils.go accepts an optional storage.UpstreamTokenStorage parameter and threads it through to NewTokenValidator
CreateMiddleware in pkg/auth/middleware.go extracts the storage dependency from the runner and passes it to GetAuthenticationMiddleware
upstreamswap.Config in pkg/auth/upstreamswap/middleware.go has a new ProviderName string field with a comment stating it is derived from Upstreams[0].Name at construction time and defaults to "default"
ServiceGetter type and all references to serviceGetter/storageGetter are removed from pkg/auth/upstreamswap/middleware.go; createMiddlewareFunc no longer accepts a ServiceGetter parameter
upstreamswap middleware reads identity.UpstreamTokens[cfg.ProviderName] directly from the pre-enriched Identity; when the token is absent or empty it returns HTTP 401 with a WWW-Authenticate: Bearer error="invalid_token" header
addUpstreamSwapMiddleware in pkg/runner/middleware.go derives ProviderName from config.EmbeddedAuthServerConfig.Upstreams[0].Name, falling back to "default" when the name is empty; the derived ProviderName is set on upstreamswap.Config before serialization
GetUpstreamTokenService() is removed from the MiddlewareRunner interface in pkg/transport/types/transport.go if no other middleware callers remain after this refactor; the mock at pkg/transport/types/mocks/mock_transport.go is regenerated
pkg/auth/middleware_test.go is updated: tests verify GetAllUpstreamTokens is called when tsid claim is present, expired tokens are included in UpstreamTokens as-is, and UpstreamTokens is nil/empty when no tsid claim is present
pkg/auth/upstreamswap/middleware_test.go is updated: tests verify ProviderName config field, middleware reads from identity.UpstreamTokens directly, middleware returns 401 when the named provider token is absent, and StorageGetter is no longer part of the test setup
pkg/runner/middleware_test.go is updated: tests verify ProviderName is derived from Upstreams[0].Name, fallback to "default" when name is empty, and StorageGetter is no longer injected into middleware params
task license-check passes (all new/modified .go files have SPDX headers)
task lint-fix passes with no remaining lint errors
task test passes (unit tests)
Technical Approach
Recommended Implementation
Start with the data model in pkg/auth/identity.go — add the UpstreamTokens field and update MarshalJSON to redact token values. This is a self-contained change with no dependencies.
Next, add the upstreamStorage field to TokenValidator and insert the enrichment call in TokenValidator.Middleware. The insertion point is between the existing claimsToIdentity call and the WithIdentity call — this two-line gap (lines ~1087–1096) is where the new block belongs. Use an optional field pattern (nil = no enrichment) to preserve existing behavior for all non-embedded-AS deployments.
Then thread UpstreamTokenStorage from the runner through to TokenValidator by updating GetAuthenticationMiddleware in pkg/auth/utils.go (add an optional parameter or a functional option) and CreateMiddleware in pkg/auth/middleware.go (the runner already provides IDPTokenStorage() indirectly via embeddedAuthServer).
The threading path requires the runner to expose IDPTokenStorage() to the auth.CreateMiddleware factory. Currently the runner exposes GetUpstreamTokenService() on the MiddlewareRunner interface. After this phase, assess whether GetUpstreamTokenService() has any remaining callers — if upstreamswap no longer calls it, remove it from the interface and regenerate the mock.
Finally, simplify pkg/auth/upstreamswap/middleware.go: remove ServiceGetter, add ProviderName to Config, and rewrite createMiddlewareFunc to read identity.UpstreamTokens[cfg.ProviderName] instead of calling the service. Update addUpstreamSwapMiddleware in pkg/runner/middleware.go to derive ProviderName from config.EmbeddedAuthServerConfig.Upstreams[0].Name with fallback to "default".
Patterns & Frameworks
Optional field pattern for storage dependency: Add upstreamStorage storage.UpstreamTokenStorage as an optional field on TokenValidator (nil = feature disabled). This is consistent with Go's convention of zero-value-safe types. Existing deployments without an embedded auth server pass nil and observe no behavior change.
Enrichment is non-fatal: Per architecture doc core principle Implement secret store #4: if GetAllUpstreamTokens fails, log at WARN with slog.WarnContext and continue with empty UpstreamTokens. Do not fail the entire request — the downstream backend will produce its own auth error.
No token values in logs: Per core principle Implement secret injection #5: identity.UpstreamTokens values (access tokens) must be redacted in MarshalJSON. Provider name keys are safe to log. The String() method currently returns only Identity{Subject:%q} — this format is already safe.
slog structured logging: Use slog.WarnContext(r.Context(), "failed to load upstream tokens", "err", err) for the non-fatal error log. Do not use slog.Warn (use the context-aware variant).
Immutable assignment in middleware: Use an immediately-invoked anonymous function for any conditional value derivation (e.g., when computing the fallback provider name), per ToolHive Go coding style.
go.uber.org/mock (mockgen): If GetUpstreamTokenService() is removed from MiddlewareRunner, regenerate pkg/transport/types/mocks/mock_transport.go using go generate ./pkg/transport/types/... or the directive at the top of transport.go.
SPDX license headers: All new or modified .go files must have // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. and // SPDX-License-Identifier: Apache-2.0. Run task license-fix to add them automatically.
Table-driven tests with require.NoError: Follow the existing patterns in middleware_test.go and upstreamswap/middleware_test.go. Use require.NoError(t, err) rather than t.Fatal.
Code Pointers
pkg/auth/identity.go — Add UpstreamTokens map[string]string field to Identity struct and update MarshalJSON to redact values. The SafeIdentity shadow struct pattern already handles redaction of Token; add a parallel map with redacted values for UpstreamTokens.
pkg/auth/token.go lines 354–383 — TokenValidator struct definition. Add upstreamStorage storage.UpstreamTokenStorage as the last field. Lines 1086–1097 — the insertion point for the enrichment block between claimsToIdentity and WithIdentity.
pkg/auth/token.go lines 557–655 — NewTokenValidator constructor. Add a WithUpstreamStorage(stor storage.UpstreamTokenStorage) TokenValidatorOption functional option (or equivalent) to wire the storage dependency without breaking the existing call signature.
pkg/auth/utils.go line 64–76 — GetAuthenticationMiddleware. Thread UpstreamTokenStorage through here to NewTokenValidator. The function currently creates NewTokenValidator(ctx, *oidcConfig) — add an optional parameter or a functional option for the storage.
pkg/auth/middleware.go lines 47–74 — CreateMiddleware factory. The runner exposes GetUpstreamTokenService() on types.MiddlewareRunner today; after this change a new method (or repurposed method) is needed to expose IDPTokenStorage(). Evaluate whether to add a GetUpstreamTokenStorage() storage.UpstreamTokenStorage method to MiddlewareRunner or reuse the existing interface extension point.
pkg/auth/upstreamswap/middleware.go — Complete rewrite of createMiddlewareFunc. Remove ServiceGetter type (line 47) and the serviceGetter parameter. Add ProviderName to Config struct (line 32–38). In the middleware handler, replace the four-step tsid → serviceGetter → GetValidTokens → inject flow with a two-step UpstreamTokens[cfg.ProviderName] → inject read.
pkg/runner/middleware.go lines 252–282 — addUpstreamSwapMiddleware. Derive ProviderName from config.EmbeddedAuthServerConfig.Upstreams[0].Name, defaulting to "default" when the name is empty. Set it on the upstreamswap.Config before creating the middleware config.
pkg/transport/types/transport.go lines 65–87 — MiddlewareRunner interface. After removing the ServiceGetter dependency from upstreamswap, assess whether GetUpstreamTokenService() (line 86) has any remaining callers. If none remain, remove it and add a GetUpstreamTokenStorage() storage.UpstreamTokenStorage method if needed by the auth middleware enrichment path.
pkg/transport/types/mocks/mock_transport.go — Regenerated file; do not edit by hand. Run go generate ./pkg/transport/types/... after any interface changes.
pkg/auth/upstreamswap/middleware_test.go — Reference for the existing test patterns. After removing ServiceGetter, simplify requestWithIdentity to include UpstreamTokens in the Identity directly rather than using mock storage. Remove serviceGetterFromMocks and nilServiceGetter helpers that are no longer needed.
pkg/runner/middleware_test.go lines 22–41 — createMinimalAuthServerConfig() helper with Upstreams[0].Name = "test-upstream". New test cases for ProviderName derivation should verify that the serialized params contain ProviderName: "test-upstream".
Component Interfaces
// pkg/auth/identity.go — updated Identity structtypeIdentitystruct {
SubjectstringNamestringEmailstringGroups []stringClaimsmap[string]anyTokenstringTokenTypestringMetadatamap[string]string// UpstreamTokens holds upstream access tokens keyed by provider name.// Populated by auth middleware after JWT validation when the embedded auth// server is in use (tsid claim present + upstreamStorage configured).// Expired tokens are included as-is; callers must not attempt refresh.// Empty for non-embedded-AS requests.UpstreamTokensmap[string]string// providerName → access token
}
// MarshalJSON must redact UpstreamTokens values:// SafeIdentity.UpstreamTokens: build map[string]string with same keys but "REDACTED" values
// pkg/auth/upstreamswap/middleware.go — updated Config and createMiddlewareFunctypeConfigstruct {
HeaderStrategystring`json:"header_strategy,omitempty" yaml:"header_strategy,omitempty"`CustomHeaderNamestring`json:"custom_header_name,omitempty" yaml:"custom_header_name,omitempty"`// ProviderName is the upstream provider whose token to inject.// Derived from Upstreams[0].Name at construction time; defaults to "default".ProviderNamestring`json:"provider_name" yaml:"provider_name"`
}
// ServiceGetter type and storageGetter field are removed entirely.// createMiddlewareFunc: simplified handler reads from identity.UpstreamTokensfunccreateMiddlewareFunc(cfg*Config) types.MiddlewareFunction {
// ... injectToken determined at startup time (unchanged) ...returnfunc(next http.Handler) http.Handler {
returnhttp.HandlerFunc(func(w http.ResponseWriter, r*http.Request) {
identity, ok:=auth.IdentityFromContext(r.Context())
if!ok {
next.ServeHTTP(w, r)
return
}
// No tsid check needed — if UpstreamTokens is empty, behave same as absenttoken, exists:=identity.UpstreamTokens[cfg.ProviderName]
if!exists||token=="" {
writeUpstreamAuthRequired(w)
return
}
injectToken(r, token)
next.ServeHTTP(w, r)
})
}
}
GetAllUpstreamTokens is called when tsid claim is present and upstreamStorage is non-nil; tokens are populated in identity.UpstreamTokens keyed by provider name
Expired tokens are included in UpstreamTokens as-is (no filtering by expiry at enrichment time)
UpstreamTokens is nil/empty when the tsid claim is absent from the JWT
UpstreamTokens is nil/empty when upstreamStorage is nil (non-embedded-AS deployment)
When GetAllUpstreamTokens returns an error, the middleware logs WARN and proceeds with empty UpstreamTokens (does not return HTTP error)
MarshalJSON on Identity with populated UpstreamTokens redacts token values to "REDACTED" while preserving provider name keys
Unit Tests — pkg/auth/upstreamswap/middleware_test.go
Middleware reads identity.UpstreamTokens[cfg.ProviderName] and injects the token when present
Middleware returns HTTP 401 with WWW-Authenticate: Bearer error="invalid_token" when identity.UpstreamTokens[cfg.ProviderName] is absent
Middleware returns HTTP 401 when the token value is an empty string
ProviderName config field is respected — using "provider-a" reads UpstreamTokens["provider-a"], not UpstreamTokens["provider-b"]
Middleware proceeds without swap when identity has no UpstreamTokens map at all (nil map) — returns 401 since token is required for an embedded-AS-configured runner
CreateMiddleware factory no longer calls GetUpstreamTokenService() on the runner mock
TestCreateMiddleware test cases do not expect mockRunner.EXPECT().GetUpstreamTokenService() to be called
Unit Tests — pkg/runner/middleware_test.go
addUpstreamSwapMiddleware with Upstreams[0].Name = "test-upstream" produces upstreamswap.Config{ProviderName: "test-upstream"}
addUpstreamSwapMiddleware with Upstreams[0].Name = "" produces upstreamswap.Config{ProviderName: "default"}
addUpstreamSwapMiddleware with empty Upstreams slice produces upstreamswap.Config{ProviderName: "default"}
Serialized upstreamswap.MiddlewareParams.Config.ProviderName matches the derived name in all existing TestAddUpstreamSwapMiddleware cases
Edge Cases
Identity.MarshalJSON() with an empty UpstreamTokens map (map[string]string{}) serializes to an empty object or omitted field, not "REDACTED"
upstreamswap middleware with ProviderName = "" behaves deterministically (either returns 401 because UpstreamTokens[""] is absent, or validateConfig rejects an empty ProviderName — document the chosen behavior)
TokenValidator with upstreamStorage = nil and a JWT containing a tsid claim does not panic and does not call any storage method
Description
Wire upstream token enrichment into the auth middleware and simplify the
upstreamswapmiddleware by removing its storage dependency. After this change, theTokenValidatoreagerly loads all upstream access tokens intoIdentity.UpstreamTokensat JWT validation time, soupstreamswaponly needs to readidentity.UpstreamTokens[cfg.ProviderName]directly — no storage call, no context extraction, noServiceGetterpattern.Context
RFC-0052 introduces a sequential authorization chain that accumulates tokens from multiple upstream IDPs per session. The current
upstreamswapmiddleware retrieves the correct upstream token by callingInProcessService.GetValidTokens(ctx, tsid)at request time, which couples the middleware to theUpstreamTokenStoragedependency via aServiceGetterfunction. This phase decouples that by moving the storage read into the auth middleware layer — a singleGetAllUpstreamTokensbulk read happens once per request during JWT validation and the results are stored in theIdentitystruct. All downstream middleware (includingupstreamswap) then read from the pre-enrichedIdentitywith no additional storage round-trips.This is Phase 4 of the RFC-0052 implementation. It depends on Phase 1 (#4136), which introduced the
GetAllUpstreamTokensbulk-read method onUpstreamTokenStorage. It may be developed in parallel with TASK-002 and TASK-003 after TASK-001 merges.Dependencies: #4136
Blocks: None (terminal phase)
Acceptance Criteria
Identitystruct inpkg/auth/identity.gohas a newUpstreamTokens map[string]stringfield (providerName → access token) with a doc comment stating it is populated by auth middleware after JWT validation, contains expired tokens as-is, and is empty for non-embedded-AS requestsIdentity.MarshalJSON()redactsUpstreamTokensvalues (replaces each token value with"REDACTED") while keeping provider name keys visible; provider names are safe to log but token values are notIdentity.String()does not leak upstream token values (the existing formatIdentity{Subject:%q}is sufficient; no additional changes required unless token values would otherwise appear)TokenValidatorstruct inpkg/auth/token.gohas a new optional fieldupstreamStorage storage.UpstreamTokenStorage(nil = no enrichment; used for non-embedded-AS deployments where no storage exists)claimsToIdentity(~line 1087) andWithIdentity(~line 1096) inTokenValidator.Middleware, whenv.upstreamStorageis non-nil and the identity has atsidclaim,GetAllUpstreamTokens(r.Context(), tsid)is called and the result is used to populateidentity.UpstreamTokensGetAllUpstreamTokensreturns an error, the middleware logs at WARN level and continues with an empty (or nil)UpstreamTokensmap — the error is non-fatal and the request proceedstsidclaim is absent orv.upstreamStorageis nil,identity.UpstreamTokensremains nil/empty and the request proceeds without errorGetAuthenticationMiddlewareinpkg/auth/utils.goaccepts an optionalstorage.UpstreamTokenStorageparameter and threads it through toNewTokenValidatorCreateMiddlewareinpkg/auth/middleware.goextracts the storage dependency from the runner and passes it toGetAuthenticationMiddlewareupstreamswap.Configinpkg/auth/upstreamswap/middleware.gohas a newProviderName stringfield with a comment stating it is derived fromUpstreams[0].Nameat construction time and defaults to"default"ServiceGettertype and all references toserviceGetter/storageGetterare removed frompkg/auth/upstreamswap/middleware.go;createMiddlewareFuncno longer accepts aServiceGetterparameterupstreamswapmiddleware readsidentity.UpstreamTokens[cfg.ProviderName]directly from the pre-enrichedIdentity; when the token is absent or empty it returns HTTP 401 with aWWW-Authenticate: Bearer error="invalid_token"headeraddUpstreamSwapMiddlewareinpkg/runner/middleware.goderivesProviderNamefromconfig.EmbeddedAuthServerConfig.Upstreams[0].Name, falling back to"default"when the name is empty; the derivedProviderNameis set onupstreamswap.Configbefore serializationGetUpstreamTokenService()is removed from theMiddlewareRunnerinterface inpkg/transport/types/transport.goif no other middleware callers remain after this refactor; the mock atpkg/transport/types/mocks/mock_transport.gois regeneratedpkg/auth/middleware_test.gois updated: tests verifyGetAllUpstreamTokensis called whentsidclaim is present, expired tokens are included inUpstreamTokensas-is, andUpstreamTokensis nil/empty when notsidclaim is presentpkg/auth/upstreamswap/middleware_test.gois updated: tests verifyProviderNameconfig field, middleware reads fromidentity.UpstreamTokensdirectly, middleware returns 401 when the named provider token is absent, andStorageGetteris no longer part of the test setuppkg/runner/middleware_test.gois updated: tests verifyProviderNameis derived fromUpstreams[0].Name, fallback to"default"when name is empty, andStorageGetteris no longer injected into middleware paramstask license-checkpasses (all new/modified.gofiles have SPDX headers)task lint-fixpasses with no remaining lint errorstask testpasses (unit tests)Technical Approach
Recommended Implementation
Start with the data model in
pkg/auth/identity.go— add theUpstreamTokensfield and updateMarshalJSONto redact token values. This is a self-contained change with no dependencies.Next, add the
upstreamStoragefield toTokenValidatorand insert the enrichment call inTokenValidator.Middleware. The insertion point is between the existingclaimsToIdentitycall and theWithIdentitycall — this two-line gap (lines ~1087–1096) is where the new block belongs. Use an optional field pattern (nil = no enrichment) to preserve existing behavior for all non-embedded-AS deployments.Then thread
UpstreamTokenStoragefrom the runner through toTokenValidatorby updatingGetAuthenticationMiddlewareinpkg/auth/utils.go(add an optional parameter or a functional option) andCreateMiddlewareinpkg/auth/middleware.go(the runner already providesIDPTokenStorage()indirectly viaembeddedAuthServer).The threading path requires the runner to expose
IDPTokenStorage()to theauth.CreateMiddlewarefactory. Currently the runner exposesGetUpstreamTokenService()on theMiddlewareRunnerinterface. After this phase, assess whetherGetUpstreamTokenService()has any remaining callers — ifupstreamswapno longer calls it, remove it from the interface and regenerate the mock.Finally, simplify
pkg/auth/upstreamswap/middleware.go: removeServiceGetter, addProviderNametoConfig, and rewritecreateMiddlewareFuncto readidentity.UpstreamTokens[cfg.ProviderName]instead of calling the service. UpdateaddUpstreamSwapMiddlewareinpkg/runner/middleware.goto deriveProviderNamefromconfig.EmbeddedAuthServerConfig.Upstreams[0].Namewith fallback to"default".Patterns & Frameworks
upstreamStorage storage.UpstreamTokenStorageas an optional field onTokenValidator(nil = feature disabled). This is consistent with Go's convention of zero-value-safe types. Existing deployments without an embedded auth server pass nil and observe no behavior change.GetAllUpstreamTokensfails, log at WARN withslog.WarnContextand continue with emptyUpstreamTokens. Do not fail the entire request — the downstream backend will produce its own auth error.identity.UpstreamTokensvalues (access tokens) must be redacted inMarshalJSON. Provider name keys are safe to log. TheString()method currently returns onlyIdentity{Subject:%q}— this format is already safe.slogstructured logging: Useslog.WarnContext(r.Context(), "failed to load upstream tokens", "err", err)for the non-fatal error log. Do not useslog.Warn(use the context-aware variant).go.uber.org/mock(mockgen): IfGetUpstreamTokenService()is removed fromMiddlewareRunner, regeneratepkg/transport/types/mocks/mock_transport.gousinggo generate ./pkg/transport/types/...or the directive at the top oftransport.go..gofiles must have// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.and// SPDX-License-Identifier: Apache-2.0. Runtask license-fixto add them automatically.require.NoError: Follow the existing patterns inmiddleware_test.goandupstreamswap/middleware_test.go. Userequire.NoError(t, err)rather thant.Fatal.Code Pointers
pkg/auth/identity.go— AddUpstreamTokens map[string]stringfield toIdentitystruct and updateMarshalJSONto redact values. TheSafeIdentityshadow struct pattern already handles redaction ofToken; add a parallel map with redacted values forUpstreamTokens.pkg/auth/token.golines 354–383 —TokenValidatorstruct definition. AddupstreamStorage storage.UpstreamTokenStorageas the last field. Lines 1086–1097 — the insertion point for the enrichment block betweenclaimsToIdentityandWithIdentity.pkg/auth/token.golines 557–655 —NewTokenValidatorconstructor. Add aWithUpstreamStorage(stor storage.UpstreamTokenStorage) TokenValidatorOptionfunctional option (or equivalent) to wire the storage dependency without breaking the existing call signature.pkg/auth/utils.goline 64–76 —GetAuthenticationMiddleware. ThreadUpstreamTokenStoragethrough here toNewTokenValidator. The function currently createsNewTokenValidator(ctx, *oidcConfig)— add an optional parameter or a functional option for the storage.pkg/auth/middleware.golines 47–74 —CreateMiddlewarefactory. The runner exposesGetUpstreamTokenService()ontypes.MiddlewareRunnertoday; after this change a new method (or repurposed method) is needed to exposeIDPTokenStorage(). Evaluate whether to add aGetUpstreamTokenStorage() storage.UpstreamTokenStoragemethod toMiddlewareRunneror reuse the existing interface extension point.pkg/auth/upstreamswap/middleware.go— Complete rewrite ofcreateMiddlewareFunc. RemoveServiceGettertype (line 47) and theserviceGetterparameter. AddProviderNametoConfigstruct (line 32–38). In the middleware handler, replace the four-steptsid → serviceGetter → GetValidTokens → injectflow with a two-stepUpstreamTokens[cfg.ProviderName] → injectread.pkg/runner/middleware.golines 252–282 —addUpstreamSwapMiddleware. DeriveProviderNamefromconfig.EmbeddedAuthServerConfig.Upstreams[0].Name, defaulting to"default"when the name is empty. Set it on theupstreamswap.Configbefore creating the middleware config.pkg/transport/types/transport.golines 65–87 —MiddlewareRunnerinterface. After removing theServiceGetterdependency fromupstreamswap, assess whetherGetUpstreamTokenService()(line 86) has any remaining callers. If none remain, remove it and add aGetUpstreamTokenStorage() storage.UpstreamTokenStoragemethod if needed by the auth middleware enrichment path.pkg/transport/types/mocks/mock_transport.go— Regenerated file; do not edit by hand. Rungo generate ./pkg/transport/types/...after any interface changes.pkg/auth/upstreamswap/middleware_test.go— Reference for the existing test patterns. After removingServiceGetter, simplifyrequestWithIdentityto includeUpstreamTokensin theIdentitydirectly rather than using mock storage. RemoveserviceGetterFromMocksandnilServiceGetterhelpers that are no longer needed.pkg/runner/middleware_test.golines 22–41 —createMinimalAuthServerConfig()helper withUpstreams[0].Name = "test-upstream". New test cases forProviderNamederivation should verify that the serialized params containProviderName: "test-upstream".Component Interfaces
Testing Strategy
Unit Tests —
pkg/auth/middleware_test.goGetAllUpstreamTokensis called whentsidclaim is present andupstreamStorageis non-nil; tokens are populated inidentity.UpstreamTokenskeyed by provider nameUpstreamTokensas-is (no filtering by expiry at enrichment time)UpstreamTokensis nil/empty when thetsidclaim is absent from the JWTUpstreamTokensis nil/empty whenupstreamStorageis nil (non-embedded-AS deployment)GetAllUpstreamTokensreturns an error, the middleware logs WARN and proceeds with emptyUpstreamTokens(does not return HTTP error)MarshalJSONonIdentitywith populatedUpstreamTokensredacts token values to"REDACTED"while preserving provider name keysUnit Tests —
pkg/auth/upstreamswap/middleware_test.goidentity.UpstreamTokens[cfg.ProviderName]and injects the token when presentWWW-Authenticate: Bearer error="invalid_token"whenidentity.UpstreamTokens[cfg.ProviderName]is absentProviderNameconfig field is respected — using"provider-a"readsUpstreamTokens["provider-a"], notUpstreamTokens["provider-b"]identityhas noUpstreamTokensmap at all (nil map) — returns 401 since token is required for an embedded-AS-configured runnerCreateMiddlewarefactory no longer callsGetUpstreamTokenService()on the runner mockTestCreateMiddlewaretest cases do not expectmockRunner.EXPECT().GetUpstreamTokenService()to be calledUnit Tests —
pkg/runner/middleware_test.goaddUpstreamSwapMiddlewarewithUpstreams[0].Name = "test-upstream"producesupstreamswap.Config{ProviderName: "test-upstream"}addUpstreamSwapMiddlewarewithUpstreams[0].Name = ""producesupstreamswap.Config{ProviderName: "default"}addUpstreamSwapMiddlewarewith emptyUpstreamsslice producesupstreamswap.Config{ProviderName: "default"}upstreamswap.MiddlewareParams.Config.ProviderNamematches the derived name in all existingTestAddUpstreamSwapMiddlewarecasesEdge Cases
Identity.MarshalJSON()with an emptyUpstreamTokensmap (map[string]string{}) serializes to an empty object or omitted field, not"REDACTED"upstreamswapmiddleware withProviderName = ""behaves deterministically (either returns 401 becauseUpstreamTokens[""]is absent, orvalidateConfigrejects an emptyProviderName— document the chosen behavior)TokenValidatorwithupstreamStorage = niland a JWT containing atsidclaim does not panic and does not call any storage methodOut of Scope
nextMissingUpstream, multi-upstream authorize/callback): covered in TASK-002len > 1guard): covered in TASK-003UpstreamTokenStorageinterface, backends): covered in TASK-001 (Implement multi-provider upstream token storage layer (RFC-0052 Phase 1) #4136)MCPServer/MCPRemoteProxyK8s workload typesReferences
docs/proposals/THV-0052-multi-upstream-idp-authserver.mddocs/arch/11-auth-server-storage.md