OAuth / Social Login
Sign in with built-in OAuth providers or configure Generic OAuth and OpenID Connect.
The OAuthPlugin supports OAuth 2.0 and OpenID Connect (OIDC). It handles authorization code exchange, provider profiles, account linking, and provider logout.
Setup
These examples assume you've already defined AppAuthSchema. If you want a complete setup from entity definitions through Axum mounting, start with the Axum integration guide.
use better_auth::plugins::OAuthPlugin;
use better_auth::plugins::oauth::OAuthProvider;
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(
OAuthPlugin::new()
.add_provider("google", OAuthProvider::google(
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
))
.add_provider("github", OAuthProvider::github(
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
))
)
.build()
.await?;Built-in Providers
Better Auth RS includes pre-configured providers with correct URLs and user info mapping:
| Provider | Constructor | Default Scopes |
|---|---|---|
OAuthProvider::google(client_id, client_secret) | openid, email, profile | |
| GitHub | OAuthProvider::github(client_id, client_secret) | read:user, user:email |
| Discord | OAuthProvider::discord(client_id, client_secret) | identify, email |
Generic OAuth / OIDC
Use GenericOAuthConfig for providers with explicit OAuth endpoints or OIDC discovery. Register the callback URL {base_url}{base_path}/callback/{provider} with the identity provider. For example, a provider named oidc uses /auth/callback/oidc when the auth base path is /auth.
use better_auth::plugins::oauth::{GenericOAuthConfig, OAuthPlugin};
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(OAuthPlugin::new().add_generic_provider("oidc", GenericOAuthConfig {
client_id: std::env::var("OIDC_CLIENT_ID")?,
client_secret: Some(std::env::var("OIDC_CLIENT_SECRET")?),
discovery_url: Some(std::env::var("OIDC_DISCOVERY_URL")?),
require_id_token_verification: true,
scopes: vec!["email".to_string(), "profile".to_string()],
..Default::default()
}))
.build()
.await?;Use the same client routes as built-in providers:
await authClient.signIn.social({ provider: "oidc", callbackURL: "/dashboard" });
await authClient.linkSocial({ provider: "oidc", callbackURL: "/settings" });Discovery runs when the auth instance initializes. Explicit authorization_url, token_url, user_info_url, and end_session_endpoint values override discovered endpoints. OIDC providers also request openid. For an OAuth provider without discovery, configure the endpoints directly.
Discovery failures can skip a provider while allowing the auth instance to initialize, matching upstream. If a provider is required, check the configured plugin before registering it:
let oauth = OAuthPlugin::new().add_generic_provider("oidc", provider_config);
if !oauth.has_provider("oidc").await? {
return Err(better_auth::AuthError::config("Required OIDC provider is unavailable").into());
}
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(oauth)
.build()
.await?;has_provider resolves and caches discovery. Auth initialization reuses the result. The method reports the available provider set, propagates invalid static configuration, and does not contact token, userinfo, or JWKS endpoints. Finish configuring the plugin before checking; subsequent configuration changes invalidate the cached result.
Set require_id_token_verification: true when using ID tokens for identity. Discovery must supply an issuer and a JWKS URI. Received ID tokens are checked for signature, issuer, audience, expiration, and the authorization request's nonce. Without discovery verification, Generic OAuth retains upstream's unverified ID-token decoding behavior. This option requires verification capability; a provider may still return an access token and use its userinfo endpoint without an ID token.
OIDC signature verification requires OpenSSL 3.0 or newer. OpenSSL 3.5 or newer is required for the complete ML-DSA algorithm set. The development shell supplies the native dependencies.
Token endpoint authentication
token_endpoint_auth applies to both authorization-code exchange and token refresh:
| Value | Configuration |
|---|---|
TokenEndpointAuth::ClientSecretPost | Send client_id and client_secret in the form body |
TokenEndpointAuth::ClientSecretBasic | Send form-encoded credentials in the Basic authorization header |
TokenEndpointAuth::None | Send client_id without a client secret |
TokenEndpointAuth::PrivateKeyJwt(handler) | Implement ClientAssertion to sign a fresh assertion for each request |
TokenEndpointAuth::Custom(handler) | Implement TokenRequestHook to modify authentication headers and form parameters |
When token_endpoint_auth is omitted, a configured secret selects ClientSecretPost; otherwise the public-client method is used. Set the method required by the provider. None and PrivateKeyJwt cannot be combined with client_secret.
Use authorization_url_params for additional authorization parameters, token_url_params and authorization_headers for code exchange, and refresh_token_params for token refresh. Refresh parameters may be static or implement OAuthRefreshParameters to use the authenticated request. Reserved grant and authorization parameters cannot be replaced by these extra parameters. Implement OAuthTokenHandler through get_token when the provider needs a custom code exchange.
Profiles and account identity
Generic OAuth reads an ID-token profile when it contains sub and email, then falls back to the userinfo endpoint. get_user_info accepts a GenericOAuthUserInfoHandler for a custom raw profile. map_profile_to_user accepts an OAuthProfileMapper to return local user fields independently of account identity.
For OAuthProfile.name and OAuthProfile.image, None preserves the provider value, Some(None) clears the value, and Some(Some(value)) replaces it. Other optional mapper fields preserve the provider value when omitted. Set override_user_info: true to update an existing user's profile on sign-in.
The account subject comes from the original profile's sub for OIDC or id for OAuth. Implement OAuthAccountSubject through account_subject for providers with another stable identifier. Mapped user fields cannot change that identifier. /account-info fetches the profile for an existing linked account and keeps the stored account subject.
Sign-up and email verification
Set disable_sign_up: true to reject new users through this provider. Existing users can continue signing in. disable_implicit_sign_up: true instead requires requestSignUp: true for new users. These settings apply to the configured provider; configure other sign-up mechanisms separately.
Set require_email_verification: true to withhold sessions from users whose email remains unverified. Attach an email verification plugin to send verification messages:
use std::sync::Arc;
use better_auth::plugins::email_verification::{EmailVerificationConfig, EmailVerificationPlugin};
let verification = Arc::new(EmailVerificationPlugin::with_config(EmailVerificationConfig {
send_on_sign_up: None,
send_on_sign_in: true,
send_verification_email: Some(verification_sender),
..Default::default()
}));
let oauth = OAuthPlugin::new()
.with_email_verification(verification)
.add_generic_provider("oidc", provider_config);verification_sender is an Arc implementing SendVerificationEmail; see the email verification guide. Register a separate EmailVerificationPlugin with the same sender and token expiry configuration to expose /verify-email.
EmailVerificationConfig.send_on_sign_up is Option<bool>. None follows the provider's require_email_verification, Some(true) sends for new unverified users, and Some(false) disables that send. Existing unverified users receive a message only when the provider requires email verification and send_on_sign_in is enabled. Sender failures are logged and do not change the verification decision, matching upstream.
Provider Logout
When end_session_endpoint is configured, POST /sign-out also returns the provider logout url and a redirect flag. The endpoint includes the linked account's ID token as id_token_hint. A request callbackURL overrides post_logout_redirect_uri. A post-logout redirect also includes client_id and an optional request state. Without a redirect or ID token, the URL includes client_id only.
Set disableRedirect: true in the sign-out request to omit the Location header. Local logout completes even if the provider endpoint cannot be used. The most recently updated linked account with a configured logout endpoint takes precedence.
The sign-out body is optional. When present, send a JSON object with Content-Type: application/json. callbackURL and state must be strings; disableRedirect must be a boolean. Invalid bodies leave the session active.
API Endpoints
The OAuth plugin exposes the following endpoints. For full request/response details, see the OpenAPI Reference.
| Endpoint | Method | Description |
|---|---|---|
/sign-in/social | POST | Initiate OAuth sign-in flow |
/callback/{provider} | GET, POST | OAuth callback handler, including form_post responses |
/link-social | POST | Link a social account to an existing user |
/get-access-token | POST | Retrieve stored OAuth access token |
/refresh-token | POST | Refresh the OAuth access token |
/account-info | GET | Retrieve provider user info for a linked account |
/list-accounts | GET | List all linked accounts |
/unlink-account | POST | Unlink a social account |
errorCallbackURL may be an absolute URL or a root-relative path. OAuth errors preserve existing query parameters and append error and error_description with URL form encoding. Root-relative error callbacks remain relative. Redirect targets must pass the configured origin check.
Provider Account Info
Use GET /account-info to fetch provider-specific user info for a linked account.
Pass the local account id returned by /list-accounts as the accountId query parameter.
GET /account-info?accountId=local-account-id
Authorization: Bearer <token>The response includes provider user info and an account object with id, providerId, and the provider's accountId. The example's Authorization header requires explicit SessionConfig.bearer configuration; browser clients may use signed session cookies.
For POST /get-access-token and POST /refresh-token, send { "accountId": "local-account-id" }. When account.store_account_cookie is enabled, these POST endpoints also accept { "useAccountCookie": true } to select the signed account cookie. Select exactly one source. The authenticated session determines the user.
For POST /unlink-account, send { "accountId": "local-account-id" }. Access-token, refresh-token, and account-info routes check the current database session even when a session cookie cache is enabled. A revoked session cannot read or refresh provider credentials through a cached session.
How It Works
PKCE Flow
Authorization-code flows use PKCE (Proof Key for Code Exchange). Generic OAuth enables PKCE by default; set GenericOAuthConfig.pkce: false for a provider that does not support it. Direct ID-token sign-in does not perform a code exchange.
- A random
code_verifieris generated - A
code_challengeis derived using SHA-256 - The challenge is sent in the authorization URL
- The verifier is stored in a verification record
- During token exchange, the verifier proves the request originated from the same client
Account Linking Behavior
When the OAuth callback completes:
- Existing account for provider: Updates the stored tokens and creates a new session
- New provider, existing email: Links the provider when account-linking policy permits it
- New provider, new email: Creates a user and links the provider when sign-up policy permits it
State Management
OAuth state expires after 10 minutes. account.store_state_strategy selects database storage or a signed cookie. State includes the callback URLs, PKCE verifier, link target, sign-up intent, and OIDC nonce when nonce binding is enabled.
With allow_idp_initiated: true, a callback without state starts a fresh authorization request. The new request establishes state and nonce binding before a session is issued.