Frontend-editable authentication providers with auto-generated SAML keys - #983
Merged
Conversation
… between integer types' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
samlandoidcsections inserviceconfig.SectionRegistrywere 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 thepkg/logsinksrefactor — the last major subsystem without a DB/frontendstory.
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/authprovidersAuthProvidermodel (auth_providerstable): one row per IdPconfiguration. Fields:
Name,Type(oidc/saml),Enabled,Config(JSON),
Source(service/db),Info. Global — not tied toenvironments. Multiple rows of the same type are allowed and expected.
Registry: mapsoidcandsamltoProviderSpecwith typedfield 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 includingIDPMetadataURL,IDPMetadataXML(multiline text),SigningCertPEM(multiline text),
SigningKeyPEM(secret),ForceAuthn,RequireAssertionSigned,ReplayWindow.Create,Update(with secret merge),Get,Delete,List,ListEnabled,RevertToService.Seed(params): translatesflagParams.OIDCandflagParams.SAMLinto rows (create-if-missing, only when
Enabled=true). Stale seedrows synced; operator-edited rows never overwritten. Idempotent.
BuildProviders(ctx): reads enabled rows, decodes configs,expands
{id}placeholders in URLs, callsoidc.NewOIDCProviderorsaml.NewSAMLProvider. Fail-fast on IdP unreachable.ClientSecret(OIDC) andSigningKeyPEM(SAML)redacted to
"***", merged on edit.(enabled/disabled/idempotent).
SAML provider: auto-generated signing keys
pkg/auth/saml/config.gogainedSigningCertPEMandSigningKeyPEMfields (inline PEM stored in DB config JSON — no files on disk needed).
pkg/auth/saml/provider.gonow:parseSPKeyPair(noos.ReadFile).validity via
generateSPKeyPairwhen no signing material is provided.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.GET/api/v1/auth-providersGET/api/v1/auth-providers/typesGET/api/v1/auth-providers/{id}?reveal={0|1}POST/api/v1/auth-providersPUT/api/v1/auth-providers/{id}DELETE/api/v1/auth-providers/{id}POST/api/v1/auth-providers/{id}/revertPOST/api/v1/auth-providers/testPOST/api/v1/auth-providers/fetch-metadataPOST/api/v1/auth-providers/applyreload-auth-providerscmd/api/handlers/auth_provider_registry.go:AuthProviderRegistrytype — holds live providers,
Get(id),AllByType(typ),AllProviders(),Replace(entries)for hot-reload.cmd/api/handlers/auth_methods.go: now returns aproviders[]arraywith
{type, name, id, loginUrl}per enabled provider. Falls back tolegacy
OIDCEnabled/SAMLEnabledbooleans when the registry is nil.Service commands
New
ActionReloadAuthProviders = "reload-auth-providers"— allowlistedand validated.
Service config
Dropped
samlandoidcfromSectionRegistry(both API entries). Nowowned by
pkg/authproviders. Tests updated.JIT provisioning: first-run admin bootstrap
cmd/api/handlers/auth_resolve.go:resolveFederatedUser: when JITprovision is enabled and the user doesn't exist, the function calls
h.Users.CountAdmins()before creating the newAdminUser:created with
admin=true, so the operator can immediately manage thesystem after their first federated login.
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=truepath is only reachable whenCountAdmins() == 0, soon 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/typesschema(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. Scrollablemodal body for long forms.
frontend/src/routes/_app/auth-providers.tsx: route.frontend/src/components/chrome/SideNav.tsx: nav entry with shieldicon, gated on
features.auth_providers.frontend/src/api/features.ts:auth_providersfield added.Main.go wiring
cmd/api/main.go: seeds auth providers fromflagParams, builds thelive
AuthProviderRegistry, wiresWithAuthProviders(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/typesschema renders the appropriateinput control based on its
type:string→ text inputpassword→ password input (forClientSecret,SigningKeyPEM)boolean→ checkbox (forJITProvision,UsePKCE,ForceAuthn,RequireAssertionSigned)select→ dropdown (forUsernameClaim:preferred_username/email/sub)
integer→ number input (forReplayWindow)text→ multiline textarea (forIDPMetadataXML,SigningCertPEM)The
buildConfighelper converts the flat field-values map back intothe JSON object the API expects, with special handling for
ScopesandRequiredGroups(comma-separated string →[]string). Secret fields arepre-filled from a reveal query when editing. Each input has
aria-labelfor accessibility.
IdP metadata fetch
The "Fetch metadata" button appears directly below the "IdP metadata
URL" field in the SAML provider config form. When clicked:
fetchIdPMetadata(url)→POST /api/v1/auth-providers/fetch-metadatawith the URL.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.
IDPMetadataXMLtextarea field with thefetched XML, so the operator can review it before saving.
Validation
pkg/authproviders(CRUD, revert, redaction, merge, seed),cmd/api/handlers(JIT admin bootstrap, JIT non-admin when adminsexist, JIT disabled, existing user by name, local account claim).
Files
New (8 files):
pkg/authproviders/authproviders.go— model, registry, CRUD, seed,build, redaction, merge, revert
pkg/authproviders/authproviders_test.go— 9 testscmd/api/handlers/auth_providers.go— CRUD, types, test, apply,revert, fetch-metadata handlers
cmd/api/handlers/auth_provider_registry.go—AuthProviderRegistrytypecmd/api/handlers/auth_resolve_test.go— 5 JIT testsfrontend/src/api/auth-providers.ts— typed API clientfrontend/src/features/auth-providers/AuthProvidersPage.tsx—full admin page with dynamic typed form
frontend/src/routes/_app/auth-providers.tsx— routeModified (10 files):
pkg/auth/saml/config.go—SigningCertPEM/SigningKeyPEMfieldspkg/auth/saml/provider.go—parseSPKeyPair,generateSPKeyPair, accept inline PEMpkg/servicecommands/servicecommands.go—ActionReloadAuthProviderspkg/serviceconfig/serviceconfig.go— drop saml/oidc from registrypkg/serviceconfig/serviceconfig_test.go— updated assertionscmd/api/handlers/auth_methods.go— returnproviders[]arraycmd/api/handlers/auth_resolve.go— JIT admin bootstrapcmd/api/handlers/handlers.go—WithAuthProviders, registry fieldcmd/api/handlers/features.go—auth_providersflagcmd/api/main.go— seed, build, routes, fetch-metadata routefrontend/src/api/features.ts—auth_providersfieldfrontend/src/components/chrome/SideNav.tsx— nav entryfrontend/src/router.tsx— route registration