M config.example.yaml => config.example.yaml +5 -0
@@ 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"
M internal/config/config.go => internal/config/config.go +15 -4
@@ 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)
M internal/config/config_test.go => internal/config/config_test.go +50 -0
@@ 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) {