From 717e25b528bcecf9364e792de85b8b467a137ef4 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 26 Apr 2026 14:57:24 +0300 Subject: [PATCH] config: add auth.oidc.dev_stub block (disabled by default) --- config.example.yaml | 5 ++++ internal/config/config.go | 19 ++++++++++--- internal/config/config_test.go | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index d1a71f0312d00735c7b3ea3d5bdfcd9fc0adf023..118efe15a5783f5e170bfa6059f12da890953527 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -17,6 +17,11 @@ auth: issuer: "https://auth.example.com" audience: "lethe" username_claim: "preferred_username" + # dev_stub: local-dev only — never enable in production + # dev_stub: + # enabled: false + # bind: "127.0.0.1:8181" + # token_ttl: "24h" logging: level: "info" diff --git a/internal/config/config.go b/internal/config/config.go index b154cd6048f3d696df93c7b25baeb095d338931c..6b602c9f0ca95d422fea796455e787f02fc77685 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,10 +67,20 @@ type ForwardAuthConfig struct { // OIDCConfig validates bearer tokens against an OIDC issuer. type OIDCConfig struct { - Enabled bool `mapstructure:"enabled"` - Issuer string `mapstructure:"issuer" validate:"required_if=Enabled true,omitempty,url"` - Audience string `mapstructure:"audience" validate:"required_if=Enabled true"` - UsernameClaim string `mapstructure:"username_claim"` + Enabled bool `mapstructure:"enabled"` + Issuer string `mapstructure:"issuer" validate:"required_if=Enabled true,omitempty,url"` + Audience string `mapstructure:"audience" validate:"required_if=Enabled true"` + UsernameClaim string `mapstructure:"username_claim"` + DevStub OIDCDevStubConfig `mapstructure:"dev_stub"` +} + +// OIDCDevStubConfig controls the local-dev OIDC stub server. It must never +// be enabled in production (see AS1). When disabled (the default), nothing +// starts and no listener opens. +type OIDCDevStubConfig struct { + Enabled bool `mapstructure:"enabled"` + Bind string `mapstructure:"bind" validate:"required_if=Enabled true,omitempty,loopback_bind"` + TokenTTL time.Duration `mapstructure:"token_ttl" validate:"omitempty,gt=0"` } // LoggingConfig selects log level and formatter. @@ -159,6 +169,7 @@ func registerDefaults(v *viper.Viper) { v.SetDefault("database.busy_timeout", 5*time.Second) v.SetDefault("auth.forward_auth.user_header", "Remote-User") v.SetDefault("auth.oidc.username_claim", "preferred_username") + v.SetDefault("auth.oidc.dev_stub.token_ttl", 24*time.Hour) v.SetDefault("ingest.max_body_bytes", int64(16*1024*1024)) v.SetDefault("ingest.max_turn_content_bytes", int64(4*1024*1024)) v.SetDefault("ingest.chunk_size", 500) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ea790e6e5adda35475f52abc680df0671b395361..cfc58af70f84afb14040993499c0b7546ec918c6 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -49,6 +49,29 @@ logging: ingest: {} ` +const validOIDCDevStubYAML = ` +server: + bind: "127.0.0.1:8080" +database: + path: "./lethe.db" +auth: + allowed_users: ["alice"] + forward_auth: + enabled: false + oidc: + enabled: true + issuer: "https://auth.example.com" + audience: "lethe" + dev_stub: + enabled: true + bind: "127.0.0.1:8181" + token_ttl: "1h" +logging: + level: "info" + format: "tint" +ingest: {} +` + const validBothEnabledYAML = ` server: bind: "127.0.0.1:8080" @@ -113,6 +136,30 @@ func TestLoad_ValidBothEnabled(t *testing.T) { } } +func TestLoad_ValidOIDCDevStub(t *testing.T) { + path := writeYAML(t, validOIDCDevStubYAML) + cfg, err := config.Load(path) + if err != nil { + t.Fatalf("Load: %v", err) + } + if !cfg.Auth.OIDC.DevStub.Enabled { + t.Error("DevStub.Enabled = false, want true") + } + if cfg.Auth.OIDC.DevStub.Bind != "127.0.0.1:8181" { + t.Errorf("DevStub.Bind = %q, want 127.0.0.1:8181", cfg.Auth.OIDC.DevStub.Bind) + } + if cfg.Auth.OIDC.DevStub.TokenTTL != time.Hour { + t.Errorf("DevStub.TokenTTL = %s, want 1h", cfg.Auth.OIDC.DevStub.TokenTTL) + } +} + +func TestLoad_DevStubBindNonLoopback_Rejects(t *testing.T) { + body := strings.Replace(validOIDCDevStubYAML, `"127.0.0.1:8181"`, `"0.0.0.0:8181"`, 1) + if _, err := config.Load(writeYAML(t, body)); err == nil { + t.Fatal("expected error for non-loopback dev_stub bind, got nil") + } +} + func TestLoad_Defaults(t *testing.T) { path := writeYAML(t, validForwardAuthYAML) cfg, err := config.Load(path) @@ -140,6 +187,9 @@ func TestLoad_Defaults(t *testing.T) { if cfg.Server.ShutdownGrace != 10*time.Second { t.Errorf("Server.ShutdownGrace = %s, want 10s", cfg.Server.ShutdownGrace) } + if cfg.Auth.OIDC.DevStub.TokenTTL != 24*time.Hour { + t.Errorf("OIDC.DevStub.TokenTTL = %s, want 24h", cfg.Auth.OIDC.DevStub.TokenTTL) + } } func TestLoad_EmptyAllowlistRejected(t *testing.T) {