Getting headers from API endpoint response
To get the `headers` from an API endpoint response on the server, pass the `returnHeaders` option set to `true`. This returns an object with `headers` and `response` properties. The `headers` is a `Headers` object which you can use to get cookies with `headers.getSetCookie()` or individual headers with `headers.get('header-name')`.
Getting Response object from API endpoint
To get the full `Response` object from an API endpoint call on the server, pass the `asResponse` option set to `true` to the endpoint.
API endpoint error handling on the server
When you call an API endpoint on the server, it will throw an error if the request fails. The error instance is an instance of `APIError`. You can check if an error is an `APIError` using the `isAPIError()` function imported from `better-auth/api`, and access properties like `error.message` and `error.status`.
Better Auth API built on better-call framework
Better Auth API endpoints are built on top of better-call, a tiny web framework that allows calling REST API endpoints as if they were regular functions and enables automatic client type inference from the server.
API object provides access to all Better Auth endpoints
When you create a new Better Auth instance, it provides an `api` object. This object exposes every endpoint that exists in your Better Auth instance, including endpoints from plugins and the core. You can use this to interact with Better Auth server-side.
Calling API endpoints on the server
To call an API endpoint on the server, import your `auth` instance and call the endpoint using the `api` object. Example: `await auth.api.getSession({ headers: await headers() })`
Server API parameter structure: body, headers, query
Unlike the client, the server needs values passed as an object with specific keys: use `body` for the request body, `headers` for HTTP headers, and `query` for query parameters. Example: `await auth.api.signInEmail({ body: { email: 'john@doe.com', password: 'password' }, headers: await headers() })`
Get OAuth access token with automatic refresh
To get the access token for a social provider, use getAccessToken(). When called, if the access token is expired, it will be refreshed automatically.
Server-side get access token
Server-side usage to get access token for a social provider:
```ts
await auth.api.getAccessToken({
body: {
providerId: "google", // or any other provider id
accountId: "accountId", // optional, if you want to get the access token for a specific account
userId: "userId", // optional, if you don't provide headers with authenticated token
},
headers: await headers() // headers containing the user's session token
});
```
Request additional OAuth scopes using linkSocial
To request additional OAuth scopes after the user has already signed up, use the linkSocial method with the same provider and pass the desired scopes. This triggers a new OAuth flow that requests the additional scopes while maintaining the existing account connection.
Better Auth version requirement for additional scopes
Better Auth version 1.2.7 or later is required for requesting additional scopes. Earlier versions like 1.2.2 may show a "Social account already linked" error when trying to link with an existing provider for additional scopes.
Pass additional data through OAuth flow
Better Auth allows passing additional data through the OAuth flow without storing it in the database. This is useful for tracking referral codes, analytics sources, or other temporary data that should be processed during authentication but not persisted.
Server-side pass additional data with sign in
Server-side example passing additional data during OAuth sign-in:
```ts
await auth.api.signInSocial({
body: {
provider: "google",
additionalData: {
referralCode: "ABC123",
source: "admin-panel",
},
},
});
```
Microsoft Entra ID provider specific notes: use profile.oid as identity anchor
For Microsoft Entra ID provider: because email is tenant-mutable and never verified, use profile.oid (immutable, stable within the tenant) as the identity anchor; treat email as a profile attribute only. Microsoft's claims validation guidance explicitly warns never to use email, preferred_username, or unique_name for authorization decisions.
Default OAuth state includes multiple fields
By default, OAuth state includes the following data: callbackURL (the callback URL for the OAuth flow), codeVerifier (the code verifier for the OAuth flow), errorURL (the error URL for the OAuth flow), newUserURL (the new user URL for the OAuth flow), link (the link for the OAuth flow, containing email and user id), requestSignUp (whether to request sign up for the OAuth flow), expiresAt (the expiration time of the OAuth state), and any additional data passed in the OAuth flow.
Synthesize placeholder email with mapProfileToUser for missing email
When a provider omits email, you can fall back to the provider's stable ID using mapProfileToUser to synthesize a placeholder email.
mapProfileToUser example with placeholder emails
Example of synthesizing placeholder emails for providers that may not return email:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
mapProfileToUser: (profile) => ({
email: profile.email ?? `${profile.id}@discord.placeholder.local`,
}),
},
apple: {
clientId: process.env.APPLE_CLIENT_ID!,
clientSecret: process.env.APPLE_CLIENT_SECRET!,
mapProfileToUser: (profile) => ({
email: profile.email ?? `${profile.sub}@apple.placeholder.local`,
}),
},
microsoft: {
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
mapProfileToUser: (profile) => ({
email: profile.email ?? `${profile.oid}@entra.placeholder.local`,
}),
},
},
});
```
Apple provider specific notes: persist email on first sign-in
For Apple provider: persist the email the first time you see it. Apple provides no user-info endpoint, so if you don't store it on first sign-in you cannot retrieve it later. Both email_verified and is_private_email are serialized as strings ("true" / "false"), not booleans.
GitHub provider specific notes: user:email scope and private emails
For GitHub provider: the user:email scope is requested by default. Private emails still return null on /user; the primary verified address is available at /user/emails.
Provider option: clientId
The OAuth 2.0 Client ID issued by the provider. For providers that verify ID tokens by audience (Google, Apple, Microsoft Entra, Facebook, Cognito), you can pass an array to accept tokens issued for any of the configured clients. The first entry is used when Better Auth drives the authorization code flow; all entries are accepted when verifying an ID token's aud claim. This enables cross-platform sign-in (Web, iOS, Android) with a single backend configuration, where each platform's native SDK issues tokens under its own Client ID. For providers that don't verify ID tokens by audience, only a single string is accepted.
Provider option: clientId array example
Example of configuring clientId as an array for cross-platform sign-in:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: [
process.env.GOOGLE_WEB_CLIENT_ID as string,
process.env.GOOGLE_IOS_CLIENT_ID as string,
process.env.GOOGLE_ANDROID_CLIENT_ID as string,
],
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
});
```
Provider option: scope
The scope of the access request. For example, email or profile.
Provider option: scope example
Example of configuring scope for a provider:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
scope: ["email", "profile"],
},
},
});
```
Provider option: redirectURI
Custom redirect URI for the provider. By default, it uses /api/auth/callback/${providerName}.
Provider option: redirectURI example
Example of configuring a custom redirect URI:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
redirectURI: "https://your-app.com/auth/callback",
},
},
});
```
Provider option: disableSignUp
Disables sign-up for new users when set for a provider.
Provider option: disableIdTokenSignIn
Disables the use of the ID token for sign-in. By default, it is enabled for some providers like Google and Apple.
Provider option: verifyIdToken
A custom function to verify the ID token. Receives the token, an optional nonce, and the request endpoint context so you can branch on headers or other request data. Providing verifyIdToken replaces the provider's built-in verification (signature, issuer, audience, and expiry). Your callback must perform those checks itself. Client-supplied headers such as x-platform are attacker-controlled — use them only to select which audience (or other claim) to verify against, not as proof of identity on their own.
Provider option: verifyIdToken example
Example of implementing a custom verifyIdToken function:
```ts
import { betterAuth } from "better-auth";
import { createRemoteJWKSet, jwtVerify } from "jose";
const appleJwks = createRemoteJWKSet(
new URL("https://appleid.apple.com/auth/keys"),
);
export const auth = betterAuth({
socialProviders: {
apple: {
clientId: "YOUR_APPLE_CLIENT_ID",
clientSecret: "YOUR_APPLE_CLIENT_SECRET",
verifyIdToken: async (token, nonce, ctx) => {
const audience =
ctx?.headers?.get("x-platform") === "ios"
? process.env.APPLE_APP_BUNDLE_IDENTIFIER!
: process.env.APPLE_CLIENT_ID!;
try {
const { payload } = await jwtVerify(token, appleJwks, {
issuer: "https://appleid.apple.com",
audience,
maxTokenAge: "1h",
});
if (nonce && payload.nonce !== nonce) {
return false;
}
return true;
} catch {
return false;
}
},
},
},
});
```
Provider option: overrideUserInfoOnSignIn
A boolean value that determines whether to override the user information in the database when signing in. By default, it is set to false, meaning that the user information will not be overridden during sign-in. If you want to update the user information every time they sign in, set this to true.
Provider option: mapProfileToUser
Use mapProfileToUser to change the default user mapping or populate additional user fields from the provider profile. Better Auth treats the function's return value as provider input, even though the function runs on your server. It applies the input rules from user.additionalFields during OAuth sign-up, sign-in profile override, and account-link profile sync. Mapped fields that allow input are parsed and stored, while mapped values for fields marked input: false are ignored.
Provider option: mapProfileToUser example
Example of using mapProfileToUser to map provider profile fields:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
mapProfileToUser: (profile) => {
return {
firstName: profile.given_name,
lastName: profile.family_name,
};
},
},
},
});
```
Enforce authorization policies before OAuth sign-in completes
If a provider claim controls who may sign in, enforce the policy before Better Auth completes OAuth sign-in. Do not defer the check until after sign-in, because Better Auth may already have issued a valid session. Prefer a provider-specific option when one exists. For flows that invoke getUserInfo, a custom implementation can verify the provider response and return null when the policy fails. Configure equivalent enforcement for separate sign-in paths that do not invoke getUserInfo.
Provider option: refreshAccessToken
A custom function to refresh the token. This feature is only supported for built-in social providers (Google, Facebook, GitHub, etc.) and is not currently supported for custom OAuth providers configured through the Generic OAuth Plugin. For built-in providers, you can provide a custom function to refresh the token if needed.
Provider option: refreshAccessToken example
Example of implementing a custom refreshAccessToken function:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
refreshAccessToken: async (token) => {
return {
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
};
},
},
},
});
```
OAuth 2.0 and OpenID Connect support
Better Auth comes with built-in support for OAuth 2.0 and OpenID Connect. This allows authentication via popular OAuth providers like Google, Facebook, GitHub, and more.
Provider option: clientKey example for TikTok
Example of configuring TikTok social provider with clientKey:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
tiktok: {
clientKey: "YOUR_TIKTOK_CLIENT_KEY",
clientSecret: "YOUR_TIKTOK_CLIENT_SECRET",
},
},
});
```
Provider option: getUserInfo
A custom function to get user info from the provider. This allows you to override the default user info retrieval process.
Provider option: getUserInfo example
Example of implementing a custom getUserInfo function:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
getUserInfo: async (token) => {
const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
});
const profile = await response.json();
return {
user: {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.picture,
emailVerified: profile.verified_email,
},
data: profile,
};
},
},
},
});
```
Provider option: disableImplicitSignUp
Disables implicit sign up for new users. When set to true for the provider, sign-in needs to be called with requestSignUp as true to create new users.
Provider option: disableImplicitSignUp example
Example of disabling implicit sign up:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
disableImplicitSignUp: true,
},
},
});
```
Provider option: prompt
The prompt to use for the authorization code request. This controls the authentication flow behavior. Valid values include: select_account, consent, login, none, or select_account+consent.
Provider option: prompt example
Example of configuring the prompt option:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
prompt: "select_account", // or "consent", "login", "none", "select_account+consent"
},
},
});
```
Provider option: responseMode
The response mode to use for the authorization code request. This determines how the authorization response is returned. Valid values are query or form_post.
Provider option: responseMode example
Example of configuring the responseMode option:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
responseMode: "query", // or "form_post"
},
},
});
```
Provider option: disableDefaultScope
Removes the default scopes of the provider. By default, providers include certain scopes like email and profile. Set this to true to remove these default scopes and use only the scopes you specify.
Provider option: disableDefaultScope example
Example of disabling default scopes:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
disableDefaultScope: true,
scope: ["https://www.googleapis.com/auth/userinfo.email"], // Only this scope will be used
},
},
});
```
Get provider account info with accountInfo
To get provider specific account info you can use the accountInfo function with the authClient or auth.api for server-side usage.
Server-side get provider account info
Server-side usage to get provider account info:
```ts
await auth.api.accountInfo({
query: {
accountId: "accountId",
userId: "userId", // optional, if you don't provide headers with authenticated token
},
headers: await headers() // headers containing the user's session token
});
```
Provider option: clientKey for TikTok
The client key of your application. This is used by TikTok Social Provider instead of clientId.
Generic OAuth Plugin for custom providers
If a desired provider is not directly supported, you can use the Generic OAuth Plugin for custom integrations.
Social provider configuration requires clientId and clientSecret
To enable a social provider, you must provide clientId and clientSecret for the provider.
Facebook provider specific notes: treat email as unverified
For Facebook provider: without a per-email verification flag, treat every Facebook email as unverified unless you run your own verification challenge.
Social provider configuration example
Example configuration of Google as a provider:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: "YOUR_GOOGLE_CLIENT_ID",
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
},
},
});
```
Server-side OAuth sign-in with auth.api
To sign in with a social provider on the server side, use auth.api.signInSocial() with the provider in the request body:
```ts
await auth.api.signInSocial({
body: {
provider: "google", // or any other provider id
},
});
```
Server-side OAuth account linking
To link an account to a social provider on the server side, use auth.api.linkSocialAccount() with the provider in the request body and headers containing the user's session token:
```ts
await auth.api.linkSocialAccount({
body: {
provider: "google", // or any other provider id
},
headers: await headers() // headers containing the user's session token
});
```
changePassword API endpoint parameters
The POST /change-password endpoint requires a session and accepts newPassword (required, string), currentPassword (required, string), and revokeOtherSessions (optional boolean, default true). When revokeOtherSessions is true, all other active sessions for the user are invalidated.