Summary
pkg/authserver offers two ways to stand up the embedded auth server:
- The runner —
runner.NewEmbeddedAuthServer(ctx, *RunConfig) / NewEmbeddedAuthServerWithStorage(ctx, *RunConfig, storage) — takes the serializable RunConfig, does all the RunConfig → authserver.Config translation internally, but does not expose authserver.Config.UpstreamFilter (or other Config-only fields).
authserver.New(ctx, Config, storage) — takes a fully-built authserver.Config, which is the only way to set Config.UpstreamFilter.
An embedder that needs Config.UpstreamFilter (or any Config-only field) must therefore call authserver.New directly — but there is no exported way to derive a Config from a RunConfig. That translation is entirely unexported in pkg/authserver/runner: buildUpstreamConfigs → buildOIDCConfig/buildPureOAuth2Config, resolveSecret, convertUserInfoConfig, the ephemeral key/HMAC defaulting (createKeyProvider/loadHMACSecrets), upstream.RegisterModifiers(), and the DCR body-size cap that EmbeddedAuthServer.Handler() applies.
Problem
So each embedder that wants the Config path re-implements that translation by hand. That is:
- Error-prone. The translation quietly does several things that are easy to miss when copied: the DCR request-body cap (
bodylimit.Middleware(handlers.MaxDCRBodySize) on Handler()), threading InsecureAllowHTTP onto the top-level Config, calling upstream.RegisterModifiers(), client-secret-from-file/env resolution, and scope defaults. Missing any of these silently weakens or breaks auth (e.g. serving authserver.Server.Handler() directly loses the body cap on the open DCR endpoint).
- Drift-prone. A hand copy does not follow changes to the (unexported) source across releases.
- Duplicated. At least two separate downstream consumers have independently re-implemented this same translation (plus a per-user
UpstreamFilter on top). Same logic, multiple copies, each re-discovering the same gaps — a fix in one does not reach the others.
Recommended solution
Export the RunConfig → Config construction from pkg/authserver so embedders that need authserver.New reuse the same reviewed code instead of copying it. Any one of these closes the gap:
- A public constructor —
func (rc *RunConfig) BuildConfig() (Config, keys.KeyProvider, error) (or a free func BuildConfig(rc *RunConfig) (...)) — that runs exactly what the runner does today (key/HMAC defaulting, upstream mapping incl. DCR, RegisterModifiers, etc.). The runner constructors then become thin wrappers over it, so there is a single source of truth.
- Export the sub-helpers (
BuildUpstreamConfigs, …) if one coarse entry point isn't desirable.
- Export a handler-wrapping helper for the DCR body-size cap (e.g.
WrapWithBodyLimit(http.Handler) http.Handler), since authserver.Server.Handler() returns the bare router while EmbeddedAuthServer.Handler() applies the cap — an easy footgun for anyone serving authserver.Server directly.
Alternative considered: add a WithUpstreamFilter(...) functional option to the runner constructors so consumers can keep using the runner. Smaller, but it only solves the UpstreamFilter case, not the general "I need a Config-only field" problem — exporting the construction is the more complete fix.
Why it matters
The Config path is (rightly) the intended integration point for UpstreamFilter and other advanced fields. But if building a Config from a RunConfig isn't a first-class exported operation, every embedder re-derives it: the construction gets copy-pasted across codebases, fixes in one don't reach the others, and security-relevant details (the DCR body-size cap, InsecureAllowHTTP, modifier registration) get dropped in the copies.
References (as of v0.34.0)
pkg/authserver/runner/embeddedauthserver.go — the unexported translation (buildUpstreamConfigs, buildOIDCConfig, buildPureOAuth2Config, resolveSecret, convertUserInfoConfig, createKeyProvider, loadHMACSecrets)
pkg/authserver/config.go — Config.UpstreamFilter
pkg/authserver/server_impl.go — Handler() returns the bare router (no body cap)
pkg/authserver/server/handlers/handler.go — UpstreamFilter, MaxDCRBodySize
Summary
pkg/authserveroffers two ways to stand up the embedded auth server:runner.NewEmbeddedAuthServer(ctx, *RunConfig)/NewEmbeddedAuthServerWithStorage(ctx, *RunConfig, storage)— takes the serializableRunConfig, does all theRunConfig → authserver.Configtranslation internally, but does not exposeauthserver.Config.UpstreamFilter(or otherConfig-only fields).authserver.New(ctx, Config, storage)— takes a fully-builtauthserver.Config, which is the only way to setConfig.UpstreamFilter.An embedder that needs
Config.UpstreamFilter(or anyConfig-only field) must therefore callauthserver.Newdirectly — but there is no exported way to derive aConfigfrom aRunConfig. That translation is entirely unexported inpkg/authserver/runner:buildUpstreamConfigs→buildOIDCConfig/buildPureOAuth2Config,resolveSecret,convertUserInfoConfig, the ephemeral key/HMAC defaulting (createKeyProvider/loadHMACSecrets),upstream.RegisterModifiers(), and the DCR body-size cap thatEmbeddedAuthServer.Handler()applies.Problem
So each embedder that wants the
Configpath re-implements that translation by hand. That is:bodylimit.Middleware(handlers.MaxDCRBodySize)onHandler()), threadingInsecureAllowHTTPonto the top-levelConfig, callingupstream.RegisterModifiers(), client-secret-from-file/env resolution, and scope defaults. Missing any of these silently weakens or breaks auth (e.g. servingauthserver.Server.Handler()directly loses the body cap on the open DCR endpoint).UpstreamFilteron top). Same logic, multiple copies, each re-discovering the same gaps — a fix in one does not reach the others.Recommended solution
Export the
RunConfig → Configconstruction frompkg/authserverso embedders that needauthserver.Newreuse the same reviewed code instead of copying it. Any one of these closes the gap:func (rc *RunConfig) BuildConfig() (Config, keys.KeyProvider, error)(or a freefunc BuildConfig(rc *RunConfig) (...)) — that runs exactly what the runner does today (key/HMAC defaulting, upstream mapping incl. DCR,RegisterModifiers, etc.). The runner constructors then become thin wrappers over it, so there is a single source of truth.BuildUpstreamConfigs, …) if one coarse entry point isn't desirable.WrapWithBodyLimit(http.Handler) http.Handler), sinceauthserver.Server.Handler()returns the bare router whileEmbeddedAuthServer.Handler()applies the cap — an easy footgun for anyone servingauthserver.Serverdirectly.Alternative considered: add a
WithUpstreamFilter(...)functional option to the runner constructors so consumers can keep using the runner. Smaller, but it only solves theUpstreamFiltercase, not the general "I need aConfig-only field" problem — exporting the construction is the more complete fix.Why it matters
The
Configpath is (rightly) the intended integration point forUpstreamFilterand other advanced fields. But if building aConfigfrom aRunConfigisn't a first-class exported operation, every embedder re-derives it: the construction gets copy-pasted across codebases, fixes in one don't reach the others, and security-relevant details (the DCR body-size cap,InsecureAllowHTTP, modifier registration) get dropped in the copies.References (as of v0.34.0)
pkg/authserver/runner/embeddedauthserver.go— the unexported translation (buildUpstreamConfigs,buildOIDCConfig,buildPureOAuth2Config,resolveSecret,convertUserInfoConfig,createKeyProvider,loadHMACSecrets)pkg/authserver/config.go—Config.UpstreamFilterpkg/authserver/server_impl.go—Handler()returns the bare router (no body cap)pkg/authserver/server/handlers/handler.go—UpstreamFilter,MaxDCRBodySize