Enterprise SSO
Lets one organization bring its own identity provider, so its people sign in at Okta or Entra instead of with a password here. Three things have to line up: a provider registered against the organization, at least one email domain proved to belong to it, and a policy saying whether that route is optional or mandatory.
There are two layers of provider, and they are easy to confuse. Platform
providers under /api/v1/auth/providers are the deployment-wide social
logins (Google, GitHub) and are admin-only - a normal user listing them gets
403 Admin access required. Organization providers under
/api/v1/auth/org/{id}/providers belong to one customer and are managed by
that organization's own admins. This page is about the second kind.
Register a provider
snug auth idp create --org <org_id> --name 'Acme Okta' \
--client-id 0oaEXAMPLE --client-secret '<client secret>' \
--authorization-endpoint https://acme.example.com/oauth2/v1/authorize \
--token-endpoint https://acme.example.com/oauth2/v1/token \
--userinfo-endpoint https://acme.example.com/oauth2/v1/userinfo \
--scope openid --scope email
snug auth idp list --org <org_id>
snug auth idp get --org <org_id> --provider <provider_id>
snug auth idp update --org <org_id> --provider <provider_id> --name 'Acme SSO'
snug auth idp delete --org <org_id> --provider <provider_id>{
"provider_id": "idp_b18a604266984630a22787b9c7ce7f78",
"org_id": "dqxPXsVANGJNbqchQTgS",
"name": "Docs4 Okta",
"enabled": false,
"teardown_in_progress": false,
"client_id": "0oaDOCS4EXAMPLE",
"authorization_endpoint": "https://docs4.example.com/oauth2/v1/authorize",
"token_endpoint": "https://docs4.example.com/oauth2/v1/token",
"userinfo_endpoint": "https://docs4.example.com/oauth2/v1/userinfo",
"scopes": ["openid", "email"],
"redirect_uri": "http://localhost:4000/auth/oauth/callback/idp_b18a604266984630a22787b9c7ce7f78",
"created_at": "2026-08-28T23:02:28.456304Z"
}Three things to notice. The provider arrives disabled, so registering it
cannot break sign-in for anyone. The client secret is encrypted on the way
in and is never echoed back by any endpoint, so store it wherever you got it
from. And redirect_uri is derived from the generated provider_id - copy
that exact value into the provider's application configuration, because it
is the address the callback arrives on.
update is a patch: omitted fields keep their current values. delete
removes the provider along with its domain claims and its user links.
Claim and verify a domain
A provider only serves the email domains it has proved it controls.
snug auth idp add-domain --org <org_id> --provider <provider_id> --domain acme.example.com
snug auth idp domains --org <org_id> --provider <provider_id>
snug auth idp verify-domain --org <org_id> --provider <provider_id> --domain-id <domain_id>
snug auth idp remove-domain --org <org_id> --provider <provider_id> --domain-id <domain_id>Adding a domain hands back the DNS record to publish:
{
"domain_id": "PxFfrcgWSXxyWMVzqUBY",
"provider_id": "idp_b18a604266984630a22787b9c7ce7f78",
"domain": "docs4.example.com",
"verified": false,
"verification_record_name": "_snugnut-verify.docs4.example.com",
"verification_record_value": "snugnut-verify=7bc0215e...",
"created_at": "2026-08-28T23:02:35.232441Z"
}Publish that as a TXT record, then call verify-domain. It re-checks DNS
and returns the domain's current state rather than an error, so a run
against a domain whose record is missing or not yet propagated simply comes
back with "verified": false and the same record to publish. It is safe to
call repeatedly, and the returned verified field is the only thing worth
reading. Until a domain verifies, it steers nobody: discovery for an address
in it answers {"sso_required": false}.
The federated sign-in flow
snug auth sso-discover --email person@acme.example.com
snug auth sso-exchange --code <code>Discovery is the first step a login form takes: given an email address, it
answers whether that address must go through a provider and, when it must,
which one. For an address in no verified domain the answer is
{"sso_required": false} and the form falls back to a password field.
After the user returns from the provider, the redirect carries a single-use
code that is traded for a session at POST /auth/sso/exchange. Codes are
consumed on first use; a stale or invented one is
400 invalid_request: sso code is invalid or has already been used. A
provider that starts the flow on its own side lands on
/auth/sso/idp-initiated/{provider}, and a federated login that named no
application destination finishes on the /auth/sso/complete page.
The exchange half needs a real provider round-trip, so it is documented here from the contract and from its error path rather than from a completed login; the request and response bodies are in the API reference.
One deployment-level prerequisite is easy to miss:
AUTH_SERVICE_ALLOWED_REDIRECT_URLS must list the origins you intend to
send users back to. When that list is empty a federated login is refused
outright rather than downgraded, because unlike an email link it delivers
a live session rather than a single-use token.
Enforcement, and how not to lock yourself out
Requiring SSO is a platform admin operation, not an organization one.
snug auth admin org-sso --org <org_id> --enforced true \
--exempt <user_id> --exempt <other_user_id>{
"org_id": "dqxPXsVANGJNbqchQTgS",
"name": "Docs4 Auth Org",
"slug": "docs4-auth-org",
"member_count": 2,
"sso_enforced": true,
"sso_exempt_user_ids": []
}With enforcement on, password sign-in for a member is refused:
{
"status": 403,
"msg": "organization dqxPXsVANGJNbqchQTgS requires single sign-on",
"error": "sso_required"
}This includes the owner. Enforcement is evaluated per member with no
implicit exception for whoever created the organization, so turning it on
before the provider actually works locks every member out of the password
route at once. Verified: after enforcing, both the member and the owner were
refused; adding the member to sso_exempt_user_ids restored their password
sign-in immediately.
The exemption list is the break-glass path for exactly that situation, and for a provider outage. Two habits make it safe:
- Verify a domain and complete one real federated sign-in before enforcing, not after.
- Keep at least one exempt account, and remember the endpoint replaces
the policy rather than amending it.
--enforcedtakes an explicittrue/falsefor this reason, and a call that passes--exemptwithout restating--enforced truewould switch enforcement off for everyone.
Auditing drift
snug auth idp drift --org <org_id> --provider <provider_id>{
"unlinked_members": [
{ "user_id": "xhMLHYzgKQPSnTSsfcmz", "email": "docs4-auth@example.com", "external_id": null }
],
"orphaned_links": [],
"links_truncated": false,
"recent_failures": []
}unlinked_members are people in the organization with no identity at the
provider - the ones who would be locked out the moment you enforce.
orphaned_links is the mirror image: links pointing at users who are no
longer members. recent_failures collects provisioning errors. Run this
before flipping enforcement on, and treat a non-empty unlinked_members
list as the work remaining.
Reference
- Auth API - every provider, domain, and SSO endpoint
- SCIM - directory-driven user provisioning, which pairs with SSO and needs a provider-bound API key
- Organizations - the organizations these providers attach to