Enable Google sign-in in open source, and extend auth providers through packages.
ObjectStack open source ships two built-in social sign-in implementations, both wired by os serve from deployment env vars:
Google OAuth: configure in Setup → Authentication, or from
GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET deployment env vars
GitHub OAuth: configure from GITHUB_CLIENT_ID and
GITHUB_CLIENT_SECRET deployment env vars
Additional providers should be contributed by product or enterprise packages through
the auth:configure hook. Those packages can add better-auth socialProviders
or OIDC/generic OAuth providers without forking @objectstack/plugin-auth.
The Console login and registration pages automatically render Continue with ...
buttons for every enabled provider returned by /api/v1/auth/config.
The providers below are examples for enterprise or product packages, not built-in
open-source settings. A package can register them by listening to auth:configure
and mutating the draft auth config.
The env-var names shown below (e.g. MICROSOFT_CLIENT_ID, APPLE_CLIENT_ID) are
illustrative for an extension package's own reader code — unlike GOOGLE_* and
GITHUB_*, the framework does not read them out of the box, so an extension
must wire them in its auth:configure handler.
Under Certificates & secrets, create a new client secret.
MICROSOFT_CLIENT_ID=your-azure-app-client-idMICROSOFT_CLIENT_SECRET=your-azure-client-secret# Optional: restrict to a single tenant (default: "common" — all Microsoft accounts)# MICROSOFT_TENANT_ID=your-tenant-id
Admin-managed OIDC SSO ships in the open framework: @objectstack/plugin-auth
registers external IdPs through @better-auth/sso, and admins add them without
code from Setup → SSO Providers (see the ADR-0069 note below). The
oidcProviders config shown here is the in-process path for framework or product
packages that prefer wiring providers in code, or contributing them through
auth:configure.
Admin-managed external IdP (ADR-0069). Recent releases add a
per-environment external-IdP path built on @better-auth/sso and surface an
sso flag in the public /auth/config (features.sso) so a client can show
an enterprise-login button when SSO is configured. Admins register providers
without code from Setup → SSO Providers — both OIDC (Okta, Entra,
Auth0, Keycloak, …) and SAML 2.0 (see SAML 2.0
below). The oidcProviders extension shown here remains the in-process path
for framework/enterprise packages that prefer wiring providers in code.
Setup → SSO Providers → Register Provider posts to the env-side bridge at
POST /api/v1/auth/admin/sso/register, the OIDC counterpart of the SAML bridge
below. It takes the flat form fields of the
OIDC provider fields table, reshapes them for
@better-auth/sso, and is gated on a platform admin (ADR-0068 D4) before it
delegates — an organization owner or admin is not sufficient, because registering
an identity provider decides how the whole environment authenticates.
This is an ObjectStack mount, distinct from @better-auth/sso's own
POST /api/v1/auth/sso/register. The bridge exists so the no-code Setup form can
post flat fields; both doors apply the platform-admin rule.
A provider can claim an email domain, so that anyone signing in with an
address at that domain is routed to it. Proving the claim is opt-in per
environment (ADR-0024 ②) and off by default:
# Off by default. Turn on to require a DNS proof before a domain claim counts.OS_SSO_DOMAIN_VERIFICATION=true
With it on, Setup → SSO Providers exposes a two-step flow, one route per
step. Both are platform-admin gated (ADR-0068 D4) and both take the provider the
domain is being claimed for:
POST /api/v1/auth/admin/sso/request-domain-verification — returns a DNS
TXT record to publish on the domain. Copy it into your DNS zone.
POST /api/v1/auth/admin/sso/verify-domain — call once the record has
propagated. It re-checks DNS and marks the domain verified, or reports why it
could not.
// Step 1 — ask for the TXT record to publish.// Body: { providerId, domain? }. `domain` only shapes the record NAME shown back// to you; omit it and you get the bare label to place on the zone yourself.const req = await fetch('http://localhost:3000/api/v1/auth/admin/sso/request-domain-verification', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ providerId: 'okta', domain: 'acme.example' })});// → { success: true, data: { providerId, domain, token,// dnsRecordType: 'TXT', dnsRecordName, dnsRecordValue } }// …publish dnsRecordName / dnsRecordValue, wait for DNS to propagate, then:// Step 2 — verify the claim. Body: { providerId }.const done = await fetch('http://localhost:3000/api/v1/auth/admin/sso/verify-domain', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ providerId: 'okta' })});// → { success: true, data: { providerId, verified: true, message } }
Both take providerId and refuse with 400 INVALID_REQUEST when it is
missing. Step 2 reports NO_PENDING_VERIFICATION if you call it before step 1,
and DOMAIN_VERIFICATION_FAILED when the TXT record is not visible yet — retry
after DNS propagates.
The mounts are unconditional; the switch controls the endpoint behind them.
Both routes exist whether or not OS_SSO_DOMAIN_VERIFICATION is set — and with
it unset the two halves report that differently, so match on the code rather than
the status: step 1 answers 400 DOMAIN_VERIFICATION_DISABLED, step 2 passes
the inner 404 through with an explanatory message. Either way an anonymous
caller gets 401 UNAUTHENTICATED and a signed-in non-platform-admin 403
PERMISSION_DENIED — identity is answered before capability, so a stranger
cannot use these routes to probe which features an environment has enabled.
SAML 2.0 is provided natively by @better-auth/sso (the same package behind the
OIDC path) — no custom plugin or extension is needed. Admins register a SAML
IdP from Setup → SSO Providers → Register SAML Provider, which posts to the
env-side bridge at POST /api/v1/auth/admin/sso/register-saml.
User enters their work email (or clicks the enterprise-login button).
Client calls POST /api/v1/auth/sign-in/sso with { email, callbackURL }.
The email domain is matched to the registered provider; the response is a
redirect to the IdP's entryPoint carrying a signed SAMLRequest.
The IdP authenticates the user and POSTs a SAML assertion back to the SP ACS
URL; @better-auth/sso (samlify) validates the signature/timestamp, then
creates a session and redirects to callbackURL.
Signature, timestamp, and replay validation are handled by @better-auth/sso
(samlify). You only supply the IdP's signing certificate at registration time.
Expected response (the endpoint wraps getPublicConfig() in a { success, data } envelope; abbreviated — getPublicConfig() also returns disableSignUp/requireEmailVerification on emailPassword and additional features keys such as multiOrgEnabled, oidcProvider, deviceAuthorization, and admin):
OS_AUTH_SECRET must be set to a random string in production (a long random value of 32+ characters is recommended). AUTH_SECRET and BETTER_AUTH_SECRET are accepted as legacy aliases (read silently, without a deprecation warning). If no secret is set, the auth plugin is skipped in production.
OS_AUTH_URL (or OS_BASE_URL as a fallback) must match the domain registered with each provider; it determines the base URL used to build OAuth callback URLs.
Never commit client secrets to source control — use a secrets manager in production.
The redirect URI registered with each provider must match exactly: https://<your-domain>/api/v1/auth/callback/<provider-id>.