From a4e6ca5c5d5520261f0646c934559cb7914eaca6 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 26 Apr 2026 15:10:19 +0300 Subject: [PATCH] auth: fix OIDCVerifier injection wiring + OIDCDevStub root attachment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaced by the first end-to-end smoke of `auth.oidc.enabled=true`: - `OIDCVerifier.Cfg` was typed `config.OIDCConfig`, but only `AuthConfig` is registered as a `config-section:""` (config.go:31-37). Steward's type-keyed injection threw `failed to find dependency` at `mgr.Inject`, panicking the daemon. Latent since 80b1c09 — never reached because no prior verified path enabled OIDC. Retyped to `config.AuthConfig`, Init now reads `Cfg.OIDC.{Issuer,Audience,UsernameClaim}`. Sibling- consistent with `Authenticator` and `OIDCDevStub`. - `OIDCDevStub` had no dependents and no `steward.Root()` modifier, so steward logged `ERR empty dependents asset without root option` and skipped it in lifecycle bookkeeping (Destroy never called on shutdown). Added `steward.Root()` to its registration; shutdown log now shows `destroying component component=auth.OIDCDevStub`. --- cmd/lethe/main.go | 5 ++++- internal/server/auth/devstub_test.go | 12 +++++++----- internal/server/auth/middleware_test.go | 12 +++++++----- internal/server/auth/oidc.go | 16 +++++++++------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/cmd/lethe/main.go b/cmd/lethe/main.go index c9f198dc12faf484d3b39f6452f6ffbbea73c41b..7c0823d0313139a7783fb052050df4710e5be6d7 100644 --- a/cmd/lethe/main.go +++ b/cmd/lethe/main.go @@ -134,7 +134,10 @@ func run() int { if cfg.Auth.OIDC.Enabled && cfg.Auth.OIDC.DevStub.Enabled { devStubSvc := &authpkg.OIDCDevStub{} registered = append(registered, devStubSvc) - mgr.AddComponent(ctx, steward.MustServiceAsset(devStubSvc)) + // Root: OIDCDevStub has no in-process dependents (it's a side listener), + // so without explicit Root attachment steward treats it as orphan and + // logs an ERR/WRN about graph quality. + mgr.AddComponent(ctx, steward.MustServiceAsset(devStubSvc, steward.Root())) } if cfg.Auth.OIDC.Enabled { oidcSvc := &authpkg.OIDCVerifier{} diff --git a/internal/server/auth/devstub_test.go b/internal/server/auth/devstub_test.go index b1749eb9179ef73fd3246ed6d3308c4658e17d64..bb48c381eeedd1bd9b6d2cc4a67ce69b3c406294 100644 --- a/internal/server/auth/devstub_test.go +++ b/internal/server/auth/devstub_test.go @@ -83,11 +83,13 @@ func TestOIDCDevStub_InitStartsListener_TokenVerifies(t *testing.T) { // Prove the listener answers OIDC discovery by constructing a real verifier. verifier := &auth.OIDCVerifier{ - Cfg: config.OIDCConfig{ - Enabled: true, - Issuer: issuer, - Audience: "lethe", - UsernameClaim: "preferred_username", + Cfg: config.AuthConfig{ + OIDC: config.OIDCConfig{ + Enabled: true, + Issuer: issuer, + Audience: "lethe", + UsernameClaim: "preferred_username", + }, }, } if err := verifier.Init(ctx); err != nil { diff --git a/internal/server/auth/middleware_test.go b/internal/server/auth/middleware_test.go index c0f52194d0767f405da617a7578651b392500919..27ab4bfd00f647658f21f61b045caa082044e6cb 100644 --- a/internal/server/auth/middleware_test.go +++ b/internal/server/auth/middleware_test.go @@ -98,11 +98,13 @@ func signToken(t *testing.T, stub *oidcstub.Stub, claims map[string]any) string func newOIDCVerifier(t *testing.T, stub *oidcstub.Stub, aud string) *auth.OIDCVerifier { t.Helper() v := &auth.OIDCVerifier{ - Cfg: config.OIDCConfig{ - Enabled: true, - Issuer: stub.Issuer(), - Audience: aud, - UsernameClaim: "preferred_username", + Cfg: config.AuthConfig{ + OIDC: config.OIDCConfig{ + Enabled: true, + Issuer: stub.Issuer(), + Audience: aud, + UsernameClaim: "preferred_username", + }, }, } if err := v.Init(context.Background()); err != nil { diff --git a/internal/server/auth/oidc.go b/internal/server/auth/oidc.go index 81855ee659fbfaf71f331da016c67fe0d198bc0a..d96ae9df41ec6fcb3af47e59612975d216e4ed62 100644 --- a/internal/server/auth/oidc.go +++ b/internal/server/auth/oidc.go @@ -28,25 +28,27 @@ const subClaim = "sub" // OIDCVerifier is the steward-managed bearer-token verifier. It is a // zero-value type: construct with `&OIDCVerifier{Cfg: ...}` and call Init -// before Verify. +// before Verify. Cfg holds the full AuthConfig so steward injection by type +// matches the registered `auth` config section (sibling-consistent with +// Authenticator and OIDCDevStub); the verifier reads `Cfg.OIDC.*` fields. type OIDCVerifier struct { - Cfg config.OIDCConfig `config:""` + Cfg config.AuthConfig `config:""` verifier *oidc.IDTokenVerifier usernameClaim string } -// Init performs OIDC discovery against Cfg.Issuer and constructs the JWKS- -// backed verifier. Failures are wrapped with codes that distinguish the +// Init performs OIDC discovery against Cfg.OIDC.Issuer and constructs the +// JWKS-backed verifier. Failures are wrapped with codes that distinguish the // discovery handshake (likely a network/issuer problem) from any other // initialization fault. func (v *OIDCVerifier) Init(ctx context.Context) error { - provider, err := oidc.NewProvider(ctx, v.Cfg.Issuer) + provider, err := oidc.NewProvider(ctx, v.Cfg.OIDC.Issuer) if err != nil { return culpa.WithCode(culpa.Wrap(err, "oidc discovery"), "OIDC_DISCOVERY") } - v.verifier = provider.Verifier(&oidc.Config{ClientID: v.Cfg.Audience}) - v.usernameClaim = v.Cfg.UsernameClaim + v.verifier = provider.Verifier(&oidc.Config{ClientID: v.Cfg.OIDC.Audience}) + v.usernameClaim = v.Cfg.OIDC.UsernameClaim if v.usernameClaim == "" { // Defense in depth: the validator already requires a non-empty value // when OIDC is enabled, but a zero-value here would silently fall