Summary
The embedded OAuth authorization server walks every configured upstream
provider on every authorization. Add an optional filter hook, consulted
in the callback handler after the first upstream resolves, that narrows
the remaining chain to a subset of the configured upstreams. With no
hook configured, behaviour is unchanged.
Current behaviour
Config.Upstreams is an ordered list. NewHandler stores it as
h.upstreams and it is fixed for the life of the server.
/oauth/authorize always redirects to upstreams[0] and stores a
pending authorization. The session id is minted here.
- On callback,
continueChainOrComplete calls
nextMissingUpstream(sessionID). That method iterates all of
h.upstreams, skips any leg that already holds a live or refreshable
token, and redirects to the first leg still missing one. When no leg is
missing, it issues the authorization code.
- The only construction option today is
WithUpstreamRefresher.
Limitation
The chain is fixed at config time, so every authorization walks the full
configured list. A consumer has no way to say that only a subset of the
configured upstreams is relevant for a given authorization. Two costs
follow:
- The flow prompts for upstreams the authorization does not need.
- The server collects and stores upstream tokens that are never used,
widening the stored-token surface for no benefit.
Proposal
-
The first upstream is unchanged. An authorization always begins at
upstreams[0]. It is never filtered out.
-
A new optional filter hook narrows the rest. Add a construction
option, alongside WithUpstreamRefresher, that injects an upstream
filter. After upstreams[0] resolves in the callback, the handler
consults the filter with the request context and the configured
upstream names. The filter returns the subset to keep. The chain then
walks only upstreams[0] plus that subset.
-
The existing skip still applies. Within the kept set, a leg that
already holds a live or refreshable token is still skipped, so it is
not re-prompted.
-
Backward compatible. With no filter configured, the handler walks
all configured upstreams exactly as today.
Suggested shape
The signature is the maintainers' call; this is one concrete form. The
filter reads whatever it needs from the request context, so the auth
server stays agnostic to the filtering criterion.
// UpstreamFilter narrows the authorization chain to a subset of the
// configured upstreams. It is consulted once in the callback handler,
// after the first upstream resolves. The first upstream (upstreams[0]) is
// always required and is not passed here.
type UpstreamFilter interface {
FilterUpstreams(ctx context.Context, configured []string) ([]string, error)
}
func WithUpstreamFilter(f UpstreamFilter) Option
Implementation notes
- Compute the kept set once, right after the first leg resolves, and
carry it in the chain state (PendingAuthorization). A chain of N legs
should not re-run the filter N times.
- The cross-leg identity-consistency check in
continueChainOrComplete
should run against the effective kept set, not the raw config.
- A filter error should fail the authorization cleanly with a server
error. It must not silently fall back to walking every upstream.
Non-goals
SingleLeg authorizations are unaffected. They already bypass chain
continuation and target one upstream.
- No change to the upstream-token storage key contracts.
Summary
The embedded OAuth authorization server walks every configured upstream
provider on every authorization. Add an optional filter hook, consulted
in the callback handler after the first upstream resolves, that narrows
the remaining chain to a subset of the configured upstreams. With no
hook configured, behaviour is unchanged.
Current behaviour
Config.Upstreamsis an ordered list.NewHandlerstores it ash.upstreamsand it is fixed for the life of the server./oauth/authorizealways redirects toupstreams[0]and stores apending authorization. The session id is minted here.
continueChainOrCompletecallsnextMissingUpstream(sessionID). That method iterates all ofh.upstreams, skips any leg that already holds a live or refreshabletoken, and redirects to the first leg still missing one. When no leg is
missing, it issues the authorization code.
WithUpstreamRefresher.Limitation
The chain is fixed at config time, so every authorization walks the full
configured list. A consumer has no way to say that only a subset of the
configured upstreams is relevant for a given authorization. Two costs
follow:
widening the stored-token surface for no benefit.
Proposal
The first upstream is unchanged. An authorization always begins at
upstreams[0]. It is never filtered out.A new optional filter hook narrows the rest. Add a construction
option, alongside
WithUpstreamRefresher, that injects an upstreamfilter. After
upstreams[0]resolves in the callback, the handlerconsults the filter with the request context and the configured
upstream names. The filter returns the subset to keep. The chain then
walks only
upstreams[0]plus that subset.The existing skip still applies. Within the kept set, a leg that
already holds a live or refreshable token is still skipped, so it is
not re-prompted.
Backward compatible. With no filter configured, the handler walks
all configured upstreams exactly as today.
Suggested shape
The signature is the maintainers' call; this is one concrete form. The
filter reads whatever it needs from the request context, so the auth
server stays agnostic to the filtering criterion.
Implementation notes
carry it in the chain state (
PendingAuthorization). A chain of N legsshould not re-run the filter N times.
continueChainOrCompleteshould run against the effective kept set, not the raw config.
error. It must not silently fall back to walking every upstream.
Non-goals
SingleLegauthorizations are unaffected. They already bypass chaincontinuation and target one upstream.