Skip to content

Frontend-editable authentication providers with auto-generated SAML keys - #983

Merged
javuto merged 2 commits into
developfrom
auth-providers-frontend
Aug 20, 2026
Merged

Frontend-editable authentication providers with auto-generated SAML keys#983
javuto merged 2 commits into
developfrom
auth-providers-frontend

Conversation

@javuto

@javuto javuto commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

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 reveal query when editing. Each input has aria-label
for accessibility.

IdP metadata fetch

The "Fetch metadata" button appears directly below the "IdP metadata
URL" field in the SAML provider config form. When clicked:

  1. Frontend calls fetchIdPMetadata(url)
    POST /api/v1/auth-providers/fetch-metadata with the URL.
  2. Backend fetches the XML from that URL server-side (30s timeout, 1 MiB
    cap, same limits as the SAML provider's own metadata fetch). This
    avoids CORS issues and works when the IdP is on a network the browser
    can't reach but the server can.
  3. Frontend populates the IDPMetadataXML textarea field with the
    fetched XML, so the operator can review it before saving.

Validation

  • Go: 45 packages pass, 0 failures. New tests in
    pkg/authproviders (CRUD, revert, redaction, merge, seed),
    cmd/api/handlers (JIT admin bootstrap, JIT non-admin when admins
    exist, JIT disabled, existing user by name, local account claim).
  • Frontend: 242 tests pass, type check clean.
  • OpenAPI: spec regenerated and verified up to date.

Files

New (8 files):

  • pkg/authproviders/authproviders.go — model, registry, CRUD, seed,
    build, redaction, merge, revert
  • pkg/authproviders/authproviders_test.go — 9 tests
  • cmd/api/handlers/auth_providers.go — CRUD, types, test, apply,
    revert, fetch-metadata handlers
  • cmd/api/handlers/auth_provider_registry.go
    AuthProviderRegistry type
  • cmd/api/handlers/auth_resolve_test.go — 5 JIT tests
  • frontend/src/api/auth-providers.ts — typed API client
  • frontend/src/features/auth-providers/AuthProvidersPage.tsx
    full admin page with dynamic typed form
  • frontend/src/routes/_app/auth-providers.tsx — route

Modified (10 files):

  • pkg/auth/saml/config.goSigningCertPEM/SigningKeyPEM fields
  • pkg/auth/saml/provider.goparseSPKeyPair,
    generateSPKeyPair, accept inline PEM
  • pkg/servicecommands/servicecommands.go
    ActionReloadAuthProviders
  • pkg/serviceconfig/serviceconfig.go — drop saml/oidc from registry
  • pkg/serviceconfig/serviceconfig_test.go — updated assertions
  • cmd/api/handlers/auth_methods.go — return providers[] array
  • cmd/api/handlers/auth_resolve.go — JIT admin bootstrap
  • cmd/api/handlers/handlers.goWithAuthProviders, registry field
  • cmd/api/handlers/features.goauth_providers flag
  • cmd/api/main.go — seed, build, routes, fetch-metadata route
  • frontend/src/api/features.tsauth_providers field
  • frontend/src/components/chrome/SideNav.tsx — nav entry
  • frontend/src/router.tsx — route registration

@javuto javuto added osctrl-api osctrl-api related changes 🔐 security Security related issues ⭐️ frontend Frontend related issues labels Aug 20, 2026
Comment thread cmd/api/handlers/auth_providers.go Dismissed
Comment thread cmd/api/handlers/auth_providers.go Fixed
… between integer types'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@javuto
javuto merged commit c8b066d into develop Aug 20, 2026
8 checks passed
@javuto
javuto deleted the auth-providers-frontend branch August 20, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⭐️ frontend Frontend related issues osctrl-api osctrl-api related changes 🔐 security Security related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants