Skip to content

OAuth ​

Configure OAuth providers per tenant. Credentials are encrypted at rest with your Better Auth secret. When no tenant-specific config exists, the plugin falls back to globally configured social providers.

Global fallback ​

Configure social providers in your Better Auth config as usual:

ts
betterAuth({
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
  },
  plugins: [tenantAuth()],
});

Tenants without their own OAuth config use these global credentials. The callback URL is:

{baseURL}/tenant/callback/{providerId}

For example: http://localhost:3000/api/auth/tenant/callback/google when Better Auth is mounted at /api/auth.

Register per-tenant OAuth config ​

Requires management access (see Configuration).

ts
await authClient.$fetch("/tenant/oauth-config/register", {
  method: "POST",
  body: {
    tenantId: "<tenant-id>",
    providerId: "google",
    clientId: "your-client-id",
    clientSecret: "your-client-secret",
    scopes: ["email", "profile"], // optional
    redirectURI: "https://app.example.com/api/auth/tenant/callback/google", // optional
    enabled: true, // optional, default true
  },
});

Registering again for the same tenant + provider upserts the existing config.

List OAuth configs ​

Returns configs without client secrets. Client ids are decrypted for display.

ts
await authClient.$fetch("/tenant/oauth-config/list?tenantId=<tenant-id>");

Delete OAuth config ​

ts
await authClient.$fetch("/tenant/oauth-config/delete", {
  method: "POST",
  body: {
    tenantId: "<tenant-id>",
    providerId: "google",
  },
});

Social sign-in ​

Start the OAuth flow for a tenant:

ts
await authClient.signInSocialTenant({
  tenantId: "<tenant-id>",
  provider: "google",
  callbackURL: "/welcome", // where to redirect after success
  errorCallbackURL: "/error", // optional
  inviteToken: "<invite-token>", // required for first-time registration when invite-only is enabled
});

Endpoint: POST /tenant/sign-in/social

The plugin resolves the provider from the tenant's database config first, then from global socialProviders.

When requireInviteForTenantSignUp or allowedEmailDomains is configured, first-time social registration is gated the same way as email sign-up (existing OAuth users can still sign in). Pass inviteToken on social sign-in so it can be validated at the callback after the provider returns the email. Failures redirect to errorCallbackURL with codes such as invite_required, invite_invalid, or email_domain_not_allowed.

callbackURL, newUserCallbackURL, and errorCallbackURL are all validated against your Better Auth trustedOrigins before the OAuth flow starts, and the final redirect target is re-checked at the callback. Relative paths (e.g. /welcome) are always allowed; untrusted absolute URLs are rejected with INVALID_CALLBACK_URL.

Callback ​

The OAuth callback is handled at:

GET /tenant/callback/{providerId}

On success, a session is created with tenantId set and the user is redirected to callbackURL. On failure, the user is redirected to the error URL with error and optional error_description query parameters.

Provider resolution order ​

  1. Tenant's tenantOauthConfig row (if enabled)
  2. Global social provider from Better Auth config
  3. PROVIDER_NOT_FOUND error if neither exists

Only built-in Better Auth social providers (e.g. google, github) are supported for per-tenant configuration.

Security notes ​

  • Client secrets are never returned by list/get endpoints
  • Credentials are encrypted with symmetricEncrypt using your auth secret
  • Register redirect URIs in your OAuth provider's console for each tenant if using custom redirect URIs
  • Callback URLs must be in your Better Auth trustedOrigins (relative paths are OK) — see Callback URL validation

See Endpoints and the Next.js demo for a complete OAuth walkthrough.

Released under the MIT License.