Skip to content

authserver: filter the upstream authorization chain via a callback-handler hook #5724

Description

@tgrunnagle

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

  1. The first upstream is unchanged. An authorization always begins at
    upstreams[0]. It is never filtered out.

  2. 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.

  3. 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.

  4. 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.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Fields

No fields configured for Story 🗺️.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions