Social sign-in
Show "Continue with …" buttons for Better Auth social and generic OAuth/OIDC providers on the admin login, and control who gets a user row.
Set admin.login.enableSocial to true (every resolved provider) or an array of provider ids to show "Continue with …" buttons on the admin login. Configure socialProviders in Better Auth as usual. The buttons reflect what Better Auth actually resolved, so a provider you switched off with enabled: false never gets a button that would fail on click.
enableSocial defaults to false and has no 'auto' setting. admin.login.socialCallbackURL sets where a successful sign-in returns; by default it is the login page, so the role gate runs. Errors always return to the login page.
Generic OAuth and OIDC (SSO)
Since Better Auth 1.7, genericOAuth() registers its providers as social providers, so Keycloak, Zitadel, Okta, Auth0 or Entra ID appear alongside the built-in ones with no custom login view. Allowlist a provider by its providerId, and give it a name to set the button label: { providerId: 'zitadel', name: 'Company SSO' } renders as "Continue with Company SSO".
plugins: [
genericOAuth({
config: [
keycloak({ clientId, clientSecret, issuer: process.env.KEYCLOAK_ISSUER }),
{ providerId: 'zitadel', name: 'Company SSO', clientId, clientSecret, discoveryUrl },
],
}),
],// admin.login
enableSocial: ['github', 'keycloak', 'zitadel'],Fixed in 0.11.3. Before that, generic providers were filtered out.
Social buttons create accounts
A social button on the public admin login lets anyone with an account at that provider create a (non-admin) user row. The role gate blocks admin access, but the row, and any creation hooks, are still created. Set Better Auth's disableImplicitSignUp (per provider or globally) if you don't want open sign-up. This is why enableSocial defaults to false.
Only create users who pass the role gate
When your identity provider is the source of truth and only some of its users should reach the admin, a role-less user who clicks "Continue with …" still gets a user row, even though the role gate then shows Access Denied. The plugin can't prevent that: Better Auth creates the row during the OAuth callback, before the admin checks roles.
Refuse it in Better Auth instead. If a databaseHooks.user.create.before hook returns false, the user and the linked account are never written, and the login page shows the returned unable_to_create_user error.
const ADMIN_ROLES = ['admin', 'editor']
betterAuth({
// Your provider's mapProfileToUser must return the roles, and `role` must be a
// declared user field, so they are already on `user` when the hook runs.
databaseHooks: {
user: {
create: {
before: async (user, ctx) => {
// Only gate OAuth sign-ups; leave admin-created users and other flows alone.
if (!ctx?.path?.includes('/callback/')) return
const roles = [user.role].flat().filter(Boolean)
if (!roles.some((r) => ADMIN_ROLES.includes(r))) return false
},
},
},
},
})- Creation only. The hook never runs for users who already exist. Someone who later loses their role keeps their row (and any content they are linked to as author or editor); the role gate blocks their admin access. Use
overrideUserInfo: trueon the provider so their roles refresh on each sign-in. - Better Auth logs an error ("Unable to create OAuth user") each time a user is refused. That is expected.
- If the whole provider is admin-only and you provision users another way,
disableImplicitSignUp: trueon the provider is simpler: no user is ever created from a sign-in. - With
firstUserAdminon, the plugin promotes the first user after this hook runs, so a first user without a role from the provider is refused here. Seed that first admin through the provider, or create them another way first.

