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-0135 D6) 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 there is no endpoint behind them, so both steps answer 400
DOMAIN_VERIFICATION_DISABLED. With it on, either step answers an unknown
providerId with 404 RESOURCE_NOT_FOUND; step 2's NO_PENDING_VERIFICATION
is a 404 too, so match on the code rather than the status. 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):
Social providers (Google, GitHub, Microsoft, Apple):
User clicks Continue with Google (or similar).
Client calls POST /api/v1/auth/sign-in/social with { provider: "google", callbackURL }.
Browser redirects to the provider's consent screen.
Provider redirects to /api/v1/auth/callback/google.
better-auth creates a session and redirects to callbackURL.
OIDC/enterprise providers (oidcProviders): better-auth's generic-OAuth plugin
registers each one as a social provider and adds no endpoints of its own, so the flow uses
the same two routes as above.
User clicks Continue with Okta SSO.
Client calls POST /api/v1/auth/sign-in/social with { provider: "okta", callbackURL },
where provider is the entry's providerId.
Browser redirects to the provider's authorization endpoint.
Provider redirects to /api/v1/auth/callback/okta. better-auth exchanges the code, reads
the user's profile from the ID token or userInfoUrl, creates a session and redirects to
callbackURL.
A social, OIDC or enterprise SSO sign-in may arrive with an email that already belongs to a
local account, one that is not linked to this provider yet. The platform then links the
identity to that account automatically (an implicit link) only under these rules:
The local account's email must be verified. If it is not, no link is written and the
callback redirects with error=account_not_linked. The user signs in with their existing
method and links the provider from account settings, or verifies their email first.
The platform identity provider is the exception.objectstack-cloud links to an
unverified local account, because it creates the environment owner's account itself,
without a mailbox round-trip.
An unlink is honoured. After a user unlinks a provider, signing in with that provider
no longer re-links it, for any provider, objectstack-cloud included. The user links it
again explicitly from account settings (POST /api/v1/auth/link-social while signed in),
which also ends the refusal. If the unlink cannot be recorded, the unlink itself fails and
the provider stays linked.
An explicit link from account settings does not depend on whether the signed-in user's
email is verified. It still requires the provider to vouch for the email (a trusted
provider or an email_verified claim) and the email to match the account's.
account.accountLinking.requireLocalEmailVerified in the auth plugin config changes the first
rule. Unset (the default) gives the rules above. true applies the verified-email
requirement to every provider, objectstack-cloud included. false turns the requirement
off, which the library warns is an account-takeover risk; the unlink rule still applies.
account.accountLinking.trustedProviders relaxes only the provider-side email_verified
claim and never the local requirement.
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>.