<#983 Frontend-editable authentication providers w...
# osctrl
g
#983 Frontend-editable authentication providers with auto-generated SAML keys Pull request opened by javuto DB-backed, frontend-editable authentication providers with auto-generated SAML keys Problem SAML and OIDC providers were configured exclusively via YAML/flags/env vars. The
saml
and
oidc
sections in
serviceconfig.SectionRegistry
were explicitly marked non-editable. Changing an IdP issuer URL, client secret, or metadata URL required editing the config file and restarting
osctrl-api
. This was the same problem log sinks had before the
pkg/logsinks
refactor — the last major subsystem without a DB/frontend story. Solution Replace the YAML-only auth provider configuration with a DB-backed, frontend-editable system that supports multiple OIDC and SAML providers, auto-generated SAML signing keys (no files on disk), a dynamic typed form (no raw JSON), IdP metadata fetch, connection testing, and first-run admin bootstrap via JIT provisioning. Architecture New package:
pkg/authproviders
•
AuthProvider
model
(
auth_providers
table): one row per IdP configuration. Fields:
Name
,
Type
(oidc/saml),
Enabled
,
Config
(JSON),
Source
(service/db),
Info
. Global — not tied to environments. Multiple rows of the same type are allowed and expected. •
Registry
: maps
oidc
and
saml
to
ProviderSpec
with typed field schema, decode, and build functions. OIDC has 10 fields (
IssuerURL
,
ClientID
,
ClientSecret
(secret),
RedirectURL
,
Scopes
,
UsernameClaim
(select),
GroupsClaim
,
RequiredGroups
,
JITProvision
,
UsePKCE
). SAML has 13 fields including
IDPMetadataURL
,
IDPMetadataXML
(multiline text),
SigningCertPEM
(multiline text),
SigningKeyPEM
(secret),
ForceAuthn
,
RequireAssertionSigned
,
ReplayWindow
. • CRUD:
Create
,
Update
(with secret merge),
Get
,
Delete
,
List
,
ListEnabled
,
RevertToService
. •
Seed(params)
: translates
flagParams.OIDC
and
flagParams.SAML
into rows (create-if-missing, only when
Enabled=true
). Stale seed rows synced; operator-edited rows never overwritten. Idempotent. •
BuildProviders(ctx)
: reads enabled rows, decodes configs, expands
{id}
placeholders in URLs, calls
oidc.NewOIDCProvider
or
saml.NewSAMLProvider
. Fail-fast on IdP unreachable. • Redaction/Merge:
ClientSecret
(OIDC) and
SigningKeyPEM
(SAML) redacted to
"***"
, merged on edit. • Tests: 9 tests covering CRUD, revert, redaction, merge, seed (enabled/disabled/idempotent). SAML provider: auto-generated signing keys
pkg/auth/saml/config.go
gained
SigningCertPEM
and
SigningKeyPEM
fields (inline PEM stored in DB config JSON — no files on disk needed).
pkg/auth/saml/provider.go
now: • Accepts inline PEM bytes via
parseSPKeyPair
(no
os.ReadFile
). • Auto-generates a self-signed RSA 2048-bit keypair with 10-year validity via
generateSPKeyPair
when no signing material is provided. • Still supports legacy file paths (`SigningCertPath`/`SigningKeyPath`) for backwards compat. API handlers `cmd/api/handlers/auth_providers.go`: full CRUD + types + test + apply + revert + fetch-metadata. All admin-only, audit-logged, gated by
serviceConfigEnabled
. | Method | Route | Purpose | | ------ | ----------------------------------------- | ----------------------------------------- | | GET | /api/v1/auth-providers | List (secrets redacted) | | GET | /api/v1/auth-providers/types | Registry: type, description, field schema | | GET | /api/v1/auth-providers/{id}?reveal={0\|1} | One provider | | POST | /api/v1/auth-providers | Create | | PUT | /api/v1/auth-providers/{id} | Update (secret merge) | | DELETE | /api/v1/auth-providers/{id} | Delete | | POST | /api/v1/auth-providers/{id}/revert | Revert to service config | | POST | /api/v1/auth-providers/test | Test connection (body = config) | | POST | /api/v1/auth-providers/fetch-metadata | Fetch IdP metadata XML from URL | | POST | /api/v1/auth-providers/apply | Queue reload-auth-providers | `cmd/api/handlers/auth_provider_registry.go`:
AuthProviderRegistry
type — holds live providers,
Get(id)
,
AllByType(typ)
,
AllProviders()
,
Replace(entries)
for hot-reload. `cmd/api/handlers/auth_methods.go`: now returns a
providers[]
array with
{type, name, id, loginUrl}
per enabled provider. Falls back to legacy `OIDCEnabled`/`SAMLEnabled` booleans when the registry is nil. Service commands New
ActionReloadAuthProviders = "reload-auth-providers"
— allowlisted and validated. Service config Dropped
saml
and
oidc
from
SectionRegistry
(both API entries). Now owned by
pkg/authproviders
. Tests updated. JIT provisioning: first-run admin bootstrap `cmd/api/handlers/auth_resolve.go:resolveFederatedUser`: when JIT provision is enabled and the user doesn't exist, the function calls
h.Users.CountAdmins()
before creating the new `AdminUser`: • Zero admin users exist (first-run bootstrap): the new user is created with
admin=true
, so the operator can immediately manage the system after their first federated login. • One or more admin users already exist: the new user is created with
admin=false
— an existing admin must promote them manually. This prevents a federated user from self-escalating to admin on a system that already has an operator. The
admin=true
path is only reachable when
CountAdmins() == 0
, so on any already-administered system JIT users are always non-admin. Five tests cover both paths plus existing cases (JIT disabled, existing user by name, local account claim rejection). Frontend •
frontend/src/api/auth-providers.ts
: typed API client. •
frontend/src/features/auth-providers/AuthProvidersPage.tsx
: full admin page — sticky header, table with per-type icons (key for OIDC, shield for SAML), two-step create flow (type picker → config form), dynamic typed form driven by the
/auth-providers/types
schema (no raw JSON — each field renders the appropriate input: text, password, checkbox, dropdown, multiline textarea), "Test connection" button, "Fetch metadata" button (fetches IdP XML from the URL field server-side, populates the XML textarea — avoids CORS issues), edit, delete, revert (for
source=db
), apply with confirm modal. Scrollable modal body for long forms. •
frontend/src/routes/_app/auth-providers.tsx
: route. •
frontend/src/components/chrome/SideNav.tsx
: nav entry with shield icon, gated on
features.auth_providers
. •
frontend/src/api/features.ts
:
auth_providers
field added. Main.go wiring `cmd/api/main.go`: seeds auth providers from
flagParams
, builds the live
AuthProviderRegistry
, wires
WithAuthProviders(registry, mgr)
, registers all routes (list, types, get, create, update, delete, revert, test, fetch-metadata, apply). Dynamic typed form The auth provider editor form is fully dynamic — no JSON textarea. Each field from the
/auth-providers/types
schema renders the appropriate input control based on its `type`: •
string
→ text input •
password
→ password input (for
ClientSecret
,
SigningKeyPEM
) •
boolean
→ checkbox (for
JITProvision
,
UsePKCE
,
ForceAuthn
,
RequireAssertionSigned
) •
select
→ dropdown (for `UsernameClaim`: preferred_username/email/sub) •
integer
→ number input (for
ReplayWindow
) •
text
→ multiline textarea (for
IDPMetadataXML
,
SigningCertPEM
) The
buildConfig
helper converts the flat field-values map back into the JSON object the API expects, with special handling for
Scopes
and
RequiredGroups
(comma-separated string →
[]string
). Secret fields are pre-filled from a reve… jmpsec/osctrl