AuthServer
SAML SSO
Configure SAML enterprise SSO for organizations.
SqlOS supports SAML 2.0 enterprise SSO. Each organization can have its own connection. Home realm discovery routes a matching email domain only when that connection is complete, enabled, and its enrollment policy applies to the user.
For a complete customer IT administrator journey, use Let customer admins set up enterprise SSO. For the platform-team workflow, use Set up SAML SSO.
You can configure SAML in three ways:
Use a seed when the organization and IdP configuration are deployment topology that should be reproducible in source control. Keep the public signing certificate or metadata in host configuration; SqlOS does not call a cloud key service or fetch a remote metadata URL.
var metadataXml = builder.Configuration["SqlOS:Saml:Acme:MetadataXml"]
?? throw new InvalidOperationException("Acme SAML metadata is not configured.");
options.AuthServer.SeedSamlConnection("acme-workforce", saml =>
{
saml.OrganizationSlug = "acme";
saml.DisplayName = "Acme workforce SSO";
saml.MetadataXml = metadataXml;
saml.PrimaryDomain = "acme.com";
saml.AutoProvisionUsers = false;
saml.AutoLinkByEmail = true;
});You can provide IdentityProviderEntityId, SingleSignOnUrl, and X509CertificatePem instead of MetadataXml. Do not provide both modes. The certificate is public verification material, but treating deployment-specific metadata as configuration keeps customer topology out of a shared source tree.
The stable key—not the display name—owns reconciliation. Code-owned fields update on restart, renames preserve the same record, and unrelated dashboard-owned connections are never adopted. Removing a seed marks the record orphaned without deleting or silently disabling a live enterprise connection. A dashboard emergency disable survives restart; setting IsEnabled = false in code can disable an existing connection, while true only controls initial creation and does not undo an operator disable.
The dashboard labels code-owned connections and makes their metadata editor read-only. Operators can still emergency-disable or re-enable the connection. Metadata changes are normalized through the same parser used by the dashboard/API, must contain an absolute HTTPS SSO URL and a currently valid X.509 certificate, and are written to the audit log without certificate contents.
SDK:
var draft = await adminService.CreateSsoConnectionDraftAsync(
new SqlOSCreateSsoConnectionDraftRequest(
OrganizationId: org.Id,
DisplayName: "Acme Entra SSO",
PrimaryDomain: "acme.com",
AutoProvisionUsers: false,
AutoLinkByEmail: true));Admin API:
curl -X POST http://localhost:5062/sqlos/admin/auth/api/sso-connections/draft \
-b "$SQLOS_DASHBOARD_COOKIE_JAR" \
-H "Content-Type: application/json" \
-d '{
"organizationId": "org_...",
"displayName": "Acme Entra SSO",
"primaryDomain": "acme.com",
"autoProvisionUsers": false,
"autoLinkByEmail": true
}'After creation, SqlOS generates two values you need for your IdP:
| Value | IdP field (Entra) |
|---|---|
| SP Entity ID | Identifier (Entity ID) |
| ACS URL | Reply URL (Assertion Consumer Service URL) |
In Microsoft Entra ID (or any SAML IdP):
curl -X POST http://localhost:5062/sqlos/admin/auth/api/sso-connections/{connectionId}/metadata \
-b "$SQLOS_DASHBOARD_COOKIE_JAR" \
-H "Content-Type: application/json" \
-d '{"metadataXml": "<?xml version=\"1.0\" ...>"}'The connection is now active. With the conservative examples above, existing organization members with verified @acme.com email addresses are routed to Entra on their next sign-in, while unknown users are not JIT-provisioned. Set AutoProvisionUsers/autoProvisionUsers to true only when you intentionally want a valid SAML assertion to create the missing user or membership.
Create a portal session from the dashboard organization SSO tab or through the admin API:
curl -X POST http://localhost:5062/sqlos/admin/auth/api/sso-portal/sessions \
-b "$SQLOS_DASHBOARD_COOKIE_JAR" \
-H "Content-Type: application/json" \
-d '{
"organizationId": "org_...",
"provider": "microsoft-entra"
}'The response includes a setupUrl. Send that URL with your own mailer or admin workflow. The first browser open consumes the URL token and stores an HttpOnly server-side portal session cookie. Reuse requires a new setup link.
The portal is scoped to one organization and supports:
Hosts can expose the same launch behavior from their own admin surfaces by wrapping SqlOSSsoPortalService.CreateSessionAsync, as the example app does with POST /api/sso-portal-links.
Revoke a setup link or portal session:
curl -X POST http://localhost:5062/sqlos/admin/auth/api/sso-portal/sessions/{sessionId}/revoke \
-b "$SQLOS_DASHBOARD_COOKIE_JAR" \
-H "Content-Type: application/json" \
-d '{"reason": "customer_cancelled"}'These platform-admin endpoints require an operator session; see Authenticate operator API calls. Portal-scoped endpoints use the separate organization setup-session cookie described below.
Portal-scoped APIs live under /sqlos/admin/auth/sso-portal/api and require the portal session cookie. They cannot list users, clients, other organizations, or global dashboard pages.
Self-serve SSO setup can claim an organization's email domain before home realm discovery trusts it. The portal asks the org admin for a domain such as acme.com, creates a pending claim, and returns a DNS TXT record:
| Field | Value |
|---|---|
| Type | TXT |
| Name | _sqlos-verify.acme.com |
| Value | sqlos-domain-verification=... |
When the TXT record is visible, the portal marks the domain active. Home realm discovery checks active verified domains first, then falls back to the legacy operator-managed PrimaryDomain field. This keeps existing platform-admin setups working while making self-serve domain claims authoritative.
SqlOS has no Azure dependency for this. The default verifier uses public DNS-over-HTTPS and is registered behind ISqlOSDomainDnsVerifier, so hosts can replace it with internal DNS, provider-specific DNS APIs, or a test fake.
Activation requires a verified self-serve domain by default when the organization does not already have an operator-managed primary domain. Disable that gate only when your own host app enforces equivalent ownership outside SqlOS.
options.AuthServer.ConfigureSsoPortal(portal =>
{
portal.ReservedDomainRoots.Add("yourapp.com");
portal.RequireVerifiedDomainForActivation = true;
});Portal-created connections default to:
RequireSsoForExistingMembers = trueAllowJitProvisioning = falseInternally these map to the existing connection flags:
RequireSsoForExistingMembers maps to AutoLinkByEmailAllowJitProvisioning maps to AutoProvisionUsersWhen RequireSsoForExistingMembers is enabled, a user who already belongs to the organization and has a verified email on the SSO domain is sent to SSO. The first successful SAML sign-in links the IdP subject to that existing user. SqlOS does not create a user or membership on this path.
An active SCIM-provisioned member can also complete that first subject link when AutoLinkByEmail is false. This narrow path requires an enabled SCIM connection, an active and non-deleted SCIM user record, and an active membership in the same organization, plus an exact normalized match between the signed SAML assertion's email attribute and the SCIM record's current primary email. A login hint is not sufficient. A disabled connection, a SCIM record from another organization, a stale email alias, or any deactivated/deleted SCIM state is not accepted. This does not enable general email auto-linking.
Home realm discovery still follows RequireSsoForExistingMembers: when it is disabled, email-first discovery does not force an existing member into SSO. The SCIM-provenance exception applies after a client explicitly starts that organization's SAML connection. Enable RequireSsoForExistingMembers when existing members should be routed to SSO automatically.
When AllowJitProvisioning is enabled, a successful SAML sign-in may create the missing user or organization membership when no existing organization member matches the SAML email. Keep this disabled when access should be invitation- or admin-managed.
Every first-time email link and every JIT provision requires the configured email attribute in the signed SAML assertion. The login hint used for discovery and routing is never accepted as an authenticated email claim. An already-linked IdP subject can continue to resolve its existing user without an email attribute.
JIT provisioning never reactivates an existing inactive membership, and SAML callbacks reject inactive users and inactive organizations. This preserves offboarding even when the IdP still sends a valid assertion for a previously linked subject. Re-enable the user or membership through an explicit trusted workflow before allowing another SAML login.
Activation only enables the SSO connection and HRD behavior. It does not revoke active sessions. Use the separate session revocation action when an admin intentionally wants existing matching-domain sessions to sign in again through SSO. That action invalidates matching-domain OAuth and hosted AuthPage sessions for the organization.
The hosted portal is optional. Configure BuildUiUrl to hand the opened portal session to your own admin UI, and call the setup API for state transitions.
options.AuthServer.ConfigureSsoPortal(portal =>
{
portal.UseHostedPortal = false;
portal.BuildUiUrl = ctx =>
$"https://admin.example.com/sso/setup?session_id={ctx.SessionId}&view={ctx.View}";
});Default setup API base path:
/sqlos/admin/auth/sso-portal/api/setupOverride it with SsoPortal.HeadlessApiBasePath. Keep it on the SqlOS origin or configure your host for credentialed browser requests, because the portal session is stored in an HttpOnly cookie.
The setup API returns SqlOSSsoSetupActionResult:
| Field | Description |
|---|---|
type | view or redirect |
redirectUrl | Optional browser redirect |
viewModel | Current setup state, service-provider values, provider guides, domain claim, latest test, and allowed actions |
State-machine endpoints:
| Method | Endpoint | Description |
|---|---|---|
| GET | / | Current setup view |
| PUT | /provider | Select SAML provider |
| PUT | /enrollment-policy | Update existing-member SSO and JIT provisioning policy |
| POST | /domain | Start DNS TXT domain verification |
| POST | /domains/{domainId}/confirm | Check the TXT record and activate the claim |
| POST | /metadata/validate | Validate SAML metadata XML |
| POST | /metadata | Import metadata without activating |
| POST | /activate | Activate when metadata and domain state allow it |
| POST | /disable | Disable the connection |
| POST | /organization-sessions/revoke | Confirm-gated revocation for active org sessions on the verified SSO domain |
| POST | /test | Record readiness or generate a test redirect |
| POST | /signout | Clear the portal session |
When /test includes clientId and redirectUri, it must also include a fresh state, an S256 codeChallenge, and codeChallengeMethod: "S256". The bundled portal generates these values with Web Crypto. A host-owned portal should do the same; first-factor or portal-session state is never a substitute for PKCE.
| Option | Default | Description |
|---|---|---|
autoProvisionUsers | false for portal-created connections | Create SqlOS users or memberships from SAML assertions on first login |
autoLinkByEmail | true for portal-created connections | Require existing verified org members to enroll and sign in through SSO |
trustUpstreamMfa | false | Allow this connection to satisfy MFA only through accepted signed AuthnContextClassRef values |
acceptedAuthnContextClassRefs | empty | Exact SAML authentication-context URIs accepted as upstream MFA |
SamlConnectionSeeds | empty | Stable-keyed code-first upstream SAML connections; configured with SeedSamlConnection |
SsoPortal.EnableApi | true | Expose portal and headless setup APIs |
SsoPortal.UseHostedPortal | true | Serve the bundled setup portal |
SsoPortal.BuildUiUrl | null | Redirect opened setup sessions to a host-owned UI |
SsoPortal.HeadlessApiBasePath | /sqlos/admin/auth/sso-portal/api/setup | Base path for the headless setup API |
SsoPortal.RequireVerifiedDomainForActivation | true | Require a verified domain before activating self-serve SSO |
SsoPortal.DomainVerificationRecordPrefix | _sqlos-verify | TXT record name prefix |
SsoPortal.DomainVerificationRecordValuePrefix | sqlos-domain-verification | TXT record value prefix |
SsoPortal.ReservedDomainRoots | empty | Domain roots customers cannot claim |
SqlOS extracts AuthnContextClassRef only from the signature-covered assertion. A SAML connection does not satisfy local MFA merely because the primary method is saml.
Platform-admin connection creation can opt in explicitly:
{
"organizationId": "org_example",
"displayName": "Workforce SSO",
"identityProviderEntityId": "https://idp.example.com",
"singleSignOnUrl": "https://idp.example.com/sso",
"x509CertificatePem": "-----BEGIN CERTIFICATE-----...",
"autoProvisionUsers": true,
"autoLinkByEmail": false,
"trustUpstreamMfa": true,
"acceptedAuthnContextClassRefs": [
"urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken"
]
}Comparisons are exact per connection. Missing or unrecognized context leaves MFA unsatisfied, so the normal local step-up policy runs. Accepted evidence adds upstream_mfa alongside the saml primary method and records the assertion issuer, accepted class reference, and decision in the audit event.
Only allowlist class references whose meaning you have verified for that IdP tenant. A URI name alone is not a guarantee that the upstream policy actually required a second factor.
| Provider | SP Entity ID field | ACS field | Metadata source |
|---|---|---|---|
| Microsoft Entra | Identifier (Entity ID) | Reply URL | Federation Metadata XML |
| Okta | Audience URI (SP Entity ID) | Single sign-on URL | IdP metadata |
| Google Workspace | Entity ID | ACS URL | IdP metadata |
| Generic SAML | SP Entity ID | ACS URL | SAML metadata XML |
For metadata rotation, open the delegated portal again or use the dashboard, paste or upload the new XML, review the parsed IdP values, and activate. New metadata is validated before it replaces the active configuration.
@acme.com domain and checks the connection enrollment policy.SubjectConfirmationData, and atomically records the signed Response ID and Assertion ID before returning an authorization code. Reuse of either identifier on the same connection is rejected, including concurrent replay./sqlos/auth/token. Missing, wrong, downgraded, or plain PKCE is rejected.Use canonical /sqlos/auth/authorize and /sqlos/auth/token for hosted clients. The explicit /sqlos/auth/sso/authorization-url helper is retained for connection-directed flows, but it requires state, codeChallenge, and codeChallengeMethod: "S256" and produces the same PKCE-bound authorization-code record. The old /sqlos/auth/token/exchange and /sqlos/auth/saml/login/{connectionId} routes are retired.
Replay protection uses SqlOS's existing SQL database and expires records after the assertion validity window plus clock skew. It is automatic: applications do not configure a cache or external coordination service.