Social providers
35 OAuth2/OIDC providers are built in. Every one gets PKCE where the provider supports it, single-use database-backed state with a signed state cookie, token refresh, and JWKS or id-token verification where the provider is OIDC.
Configuring
Two equivalent forms. By instance:
from better_auth import BetterAuth, GitHub, Google
auth = BetterAuth(
secret=...,
social_providers={
"github": GitHub(client_id="…", client_secret="…"),
"google": Google(client_id="…", client_secret="…"),
},
)Or name-keyed, resolved against PROVIDER_REGISTRY:
auth = BetterAuth(
secret=...,
social_providers={
"gitlab": {"client_id": "…", "client_secret": "…"},
"slack": {"client_id": "…", "client_secret": "…"},
},
)Import path
GitHub, Google and Discord are re-exported at the package root. The other 32 classes live in better_auth.oauth.providers_ext:
from better_auth.oauth.providers_ext import Apple, MicrosoftEntraId, SlackThe name-keyed form needs no import at all.
The flow
curl -s -X POST localhost:8000/api/auth/sign-in/social \
-H 'content-type: application/json' \
-d '{"provider": "github", "callbackURL": "/dashboard"}'{ "url": "https://github.com/login/oauth/authorize?…", "redirect": true }Send the browser to url. The provider comes back to {base_url}/api/auth/callback/{provider}, which sets the session cookie and redirects to callbackURL. Every callbackURL is validated against base_url and trusted_origins, so it cannot be turned into an open redirect.
The redirect URI you register with the provider is {base_url}{base_path}/callback/{provider_id} — for example https://example.com/api/auth/callback/github. Override it with redirect_uri= when the provider insists on something else.
The 35 providers
One page per provider — endpoints, real dataclass options, default scopes, and per-provider quirks:
- Apple
- Atlassian
- Amazon Cognito
- Discord
- Dropbox
- Figma
- GitHub
- GitLab
- Hugging Face
- Kakao
- Kick
- LINE
- Linear
- Microsoft Entra ID
- Naver
- Notion
- Paybin
- PayPal
- Polar
- Railway
- Roblox
- Salesforce
- Slack
- Spotify
- TikTok
- Twitch
- Twitter (X)
- Vercel
- VK
- Zoom
The link slug is the registry key — the name you use in social_providers and in the callback path. better_auth.oauth.PROVIDER_REGISTRY is the same map at runtime:
from better_auth.oauth import PROVIDER_REGISTRY
len(PROVIDER_REGISTRY) # 35Per-provider options
Every provider inherits the same option surface (ProviderConfig):
from better_auth import GitHub
GitHub(
client_id="…",
client_secret="…",
scopes=["read:user", "user:email"],
redirect_uri=None, # overrides {base_url}{base_path}/callback/{id}
authorize_params={"prompt": "consent"}, # extra authorize-URL params
disable_default_scope=False, # drop the baked-in scopes first
disable_sign_up=False, # never create a user via this provider
disable_implicit_sign_up=False, # require requestSignUp:true to register
override_user_info_on_sign_in=False, # re-sync the profile on every sign-in
authentication="post", # or "basic" for the token endpoint
)A custom provider
Anything not in the registry is one dataclass:
from better_auth import OAuthProvider
okta = OAuthProvider(
client_id="…",
client_secret="…",
provider_id="okta",
authorization_endpoint="https://your-org.okta.com/oauth2/v1/authorize",
token_endpoint="https://your-org.okta.com/oauth2/v1/token",
userinfo_endpoint="https://your-org.okta.com/oauth2/v1/userinfo",
scopes=["openid", "email", "profile"],
use_pkce=True,
)
auth = BetterAuth(secret=..., social_providers={"okta": okta})OAuthProvider is an alias of ProviderConfig. The default fetch_user() expects an OIDC-shaped userinfo payload (sub, email, email_verified, name, picture). For a provider whose payload differs, either supply a profile_mapper, or subclass and override fetch_user() — the GitHub and Discord sources are the two worked examples in the codebase.
For a provider you would rather configure at runtime than as a class — from a database row, say — use the Generic OAuth plugin, which also supports OIDC discovery URLs.
Account linking
A social sign-in whose verified email matches an existing user links to that user instead of creating a second one. This is guarded: AccountLinking(require_local_email_verified=True) is the default, and trusted_providers controls which providers may link at all.
from better_auth import AccountLinking, AccountOptions
AccountOptions(
encrypt_oauth_tokens=True,
account_linking=AccountLinking(
enabled=True,
trusted_providers=["github", "google"],
allow_different_emails=False,
),
)/link-social, /list-accounts, /unlink-account and /account-info manage links for an already-signed-in user. allow_unlinking_all=False (the default) stops a user from removing their last credential.