refactor: move oauth2 to core (#5135)

This commit is contained in:
Alex Yang
2025-10-06 19:22:29 -07:00
committed by GitHub
parent 2a72450fc5
commit 2a3f870a6a
52 changed files with 375 additions and 300 deletions

View File

@@ -11,13 +11,13 @@ import { getTestInstance } from "../../test-utils/test-instance";
import { parseSetCookieHeader } from "../../cookies";
import type { GoogleProfile } from "../../social-providers";
import { DEFAULT_SECRET } from "../../utils/constants";
import { getOAuth2Tokens } from "../../oauth2";
import { getOAuth2Tokens } from "@better-auth/core/oauth2";
import { signJWT } from "../../crypto/jwt";
import { BASE_ERROR_CODES } from "../../error/codes";
import type { Account } from "../../types";
let email = "";
vi.mock("../../oauth2", async (importOriginal) => {
vi.mock("@better-auth/core/oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,

View File

@@ -1,7 +1,6 @@
import * as z from "zod";
import { createAuthEndpoint } from "../call";
import { APIError } from "better-call";
import { generateState, decryptOAuthToken, setTokenUtil } from "../../oauth2";
import type { OAuth2Tokens } from "@better-auth/core/oauth2";
import {
freshSessionMiddleware,
@@ -10,6 +9,8 @@ import {
} from "./session";
import { BASE_ERROR_CODES } from "../../error/codes";
import { SocialProviderListEnum } from "../../social-providers";
import { generateState } from "../../oauth2/state";
import { decryptOAuthToken, setTokenUtil } from "../../oauth2/utils";
export const listUserAccounts = createAuthEndpoint(
"/list-accounts",

View File

@@ -1,6 +1,6 @@
import * as z from "zod";
import { setSessionCookie } from "../../cookies";
import { setTokenUtil } from "../../oauth2";
import { setTokenUtil } from "../../oauth2/utils";
import { handleOAuthUserInfo } from "../../oauth2/link-account";
import { parseState } from "../../oauth2/state";
import { HIDE_METADATA } from "../../utils/hide-metadata";

View File

@@ -1,6 +1,4 @@
export * from "./create-authorization-url";
export * from "./validate-authorization-code";
export * from "./refresh-access-token";
export * from "@better-auth/core/oauth2";
export * from "./utils";
export * from "./state";
export * from "./link-account";

View File

@@ -2,14 +2,14 @@ import { describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../test-utils/test-instance";
import type { GoogleProfile } from "../social-providers";
import { DEFAULT_SECRET } from "../utils/constants";
import { getOAuth2Tokens } from "../oauth2";
import { getOAuth2Tokens } from "@better-auth/core/oauth2";
import { signJWT } from "../crypto/jwt";
import type { User } from "../types";
let mockEmail = "";
let mockEmailVerified = true;
vi.mock("../oauth2", async (importOriginal) => {
vi.mock("@better-auth/core/oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,

View File

@@ -1,40 +1,6 @@
import type { OAuth2Tokens } from "@better-auth/core/oauth2";
import { getDate } from "../utils/date";
import { createHash } from "@better-auth/utils/hash";
import { base64Url } from "@better-auth/utils/base64";
import type { AuthContext } from "../types";
import { symmetricDecrypt, symmetricEncrypt } from "../crypto";
export async function generateCodeChallenge(codeVerifier: string) {
const codeChallengeBytes = await createHash("SHA-256").digest(codeVerifier);
return base64Url.encode(new Uint8Array(codeChallengeBytes), {
padding: false,
});
}
export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
return {
tokenType: data.token_type,
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessTokenExpiresAt: data.expires_in
? getDate(data.expires_in, "sec")
: undefined,
refreshTokenExpiresAt: data.refresh_token_expires_in
? getDate(data.refresh_token_expires_in, "sec")
: undefined,
scopes: data?.scope
? typeof data.scope === "string"
? data.scope.split(" ")
: data.scope
: [],
idToken: data.id_token,
};
}
export const encodeOAuthParameter = (value: string) =>
encodeURIComponent(value).replace(/%20/g, "+");
export function decryptOAuthToken(token: string, ctx: AuthContext) {
if (!token) return token;
if (ctx.options.account?.encryptOAuthTokens) {

View File

@@ -8,9 +8,9 @@ import { createAuthClient } from "../../client";
import type { GoogleProfile } from "../../social-providers";
import { signJWT } from "../../crypto";
import { DEFAULT_SECRET } from "../../utils/constants";
import { getOAuth2Tokens } from "../../oauth2";
import { getOAuth2Tokens } from "@better-auth/core/oauth2";
vi.mock("../../oauth2", async (importOriginal) => {
vi.mock("@better-auth/core/oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,

View File

@@ -5,10 +5,10 @@ import { createAuthClient } from "../../client";
import { anonymousClient } from "./client";
import type { GoogleProfile } from "../../social-providers";
import { DEFAULT_SECRET } from "../../utils/constants";
import { getOAuth2Tokens } from "../../oauth2";
import { getOAuth2Tokens } from "@better-auth/core/oauth2";
import { signJWT } from "../../crypto/jwt";
vi.mock("../../oauth2", async (importOriginal) => {
vi.mock("@better-auth/core/oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,

View File

@@ -8,14 +8,14 @@ import { BASE_ERROR_CODES } from "../../error/codes";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "../../oauth2";
} from "@better-auth/core/oauth2";
import type {
OAuth2Tokens,
OAuth2UserInfo,
OAuthProvider,
} from "@better-auth/core/oauth2";
import { handleOAuthUserInfo } from "../../oauth2/link-account";
import { refreshAccessToken } from "../../oauth2/refresh-access-token";
import { refreshAccessToken } from "@better-auth/core/oauth2";
import { generateState, parseState } from "../../oauth2/state";
import type {
BetterAuthPlugin,

View File

@@ -5,11 +5,11 @@ import { lastLoginMethodClient } from "./client";
import { parseCookies, parseSetCookieHeader } from "../../cookies";
import { DEFAULT_SECRET } from "../../utils/constants";
import type { GoogleProfile } from "../../social-providers/google";
import { getOAuth2Tokens } from "../../oauth2";
import { getOAuth2Tokens } from "@better-auth/core/oauth2";
import { signJWT } from "../../crypto/jwt";
// Mock OAuth2 functions to return valid tokens for testing
vi.mock("../../oauth2", async (importOriginal) => {
vi.mock("@better-auth/core/oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,

View File

@@ -3,10 +3,10 @@ import { getTestInstance } from "../../test-utils/test-instance";
import { oAuthProxy } from ".";
import type { GoogleProfile } from "../../social-providers";
import { DEFAULT_SECRET } from "../../utils/constants";
import { getOAuth2Tokens } from "../../oauth2";
import { getOAuth2Tokens } from "@better-auth/core/oauth2";
import { signJWT } from "../../crypto/jwt";
vi.mock("../../oauth2", async (importOriginal) => {
vi.mock("@better-auth/core/oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,

View File

@@ -3,17 +3,16 @@ import { APIError, createAuthEndpoint, sessionMiddleware } from "../../api";
import type { BetterAuthPlugin, User } from "../../types";
import {
createAuthorizationURL,
generateState,
parseState,
setTokenUtil,
validateAuthorizationCode,
validateToken,
} from "../../oauth2";
} from "@better-auth/core/oauth2";
import { betterFetch, BetterFetchError } from "@better-fetch/fetch";
import { decodeJwt } from "jose";
import { handleOAuthUserInfo } from "../../oauth2/link-account";
import { setSessionCookie } from "../../cookies";
import type { OAuth2Tokens } from "@better-auth/core/oauth2";
import { generateState, parseState } from "../../oauth2/state";
import { setTokenUtil } from "../../oauth2/utils";
export interface SSOOptions {
/**

View File

@@ -6,7 +6,7 @@ import {
refreshAccessToken,
createAuthorizationURL,
validateAuthorizationCode,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface AppleProfile {
/**
* The subject registered claim identifies the principal thats the subject

View File

@@ -1,9 +1,12 @@
import { betterFetch } from "@better-fetch/fetch";
import { BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { logger } from "@better-auth/core/env";
import { refreshAccessToken } from "../oauth2/refresh-access-token";
import { refreshAccessToken } from "@better-auth/core/oauth2";
export interface AtlassianProfile {
account_type?: string;

View File

@@ -2,9 +2,12 @@ import { betterFetch } from "@better-fetch/fetch";
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
import { BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { logger } from "@better-auth/core/env";
import { refreshAccessToken } from "../oauth2/refresh-access-token";
import { refreshAccessToken } from "@better-auth/core/oauth2";
import { APIError } from "better-call";
export interface CognitoProfile {

View File

@@ -1,6 +1,9 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
import {
refreshAccessToken,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
export interface DiscordProfile extends Record<string, any> {
/** the user's id (i.e. the numerical snowflake) */
id: string;

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
refreshAccessToken,
validateAuthorizationCode,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface DropboxProfile {
account_id: string;

View File

@@ -1,8 +1,11 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { createRemoteJWKSet, jwtVerify, decodeJwt } from "jose";
import { refreshAccessToken } from "../oauth2";
import { refreshAccessToken } from "@better-auth/core/oauth2";
export interface FacebookProfile {
id: string;
name: string;

View File

@@ -1,9 +1,12 @@
import { betterFetch } from "@better-fetch/fetch";
import { BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { logger } from "@better-auth/core/env";
import { refreshAccessToken } from "../oauth2/refresh-access-token";
import { refreshAccessToken } from "@better-auth/core/oauth2";
export interface FigmaProfile {
id: string;

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
refreshAccessToken,
validateAuthorizationCode,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface GithubProfile {
login: string;

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface GitlabProfile extends Record<string, any> {
id: number;

View File

@@ -2,9 +2,12 @@ import { betterFetch } from "@better-fetch/fetch";
import { decodeJwt } from "jose";
import { BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { logger } from "@better-auth/core/env";
import { refreshAccessToken } from "../oauth2/refresh-access-token";
import { refreshAccessToken } from "@better-auth/core/oauth2";
export interface GoogleProfile {
aud: string;

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface HuggingFaceProfile {
sub: string;

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
interface Partner {
/** Partner-specific ID (consent required: kakaotalk_message) */

View File

@@ -1,5 +1,8 @@
import { betterFetch } from "@better-fetch/fetch";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
export interface KickProfile {

View File

@@ -5,7 +5,7 @@ import {
createAuthorizationURL,
refreshAccessToken,
validateAuthorizationCode,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface LineIdTokenPayload {
iss: string;

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
refreshAccessToken,
validateAuthorizationCode,
} from "../oauth2";
} from "@better-auth/core/oauth2";
interface LinearUser {
id: string;

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface LinkedInProfile {
sub: string;

View File

@@ -2,7 +2,7 @@ import {
validateAuthorizationCode,
createAuthorizationURL,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { betterFetch } from "@better-fetch/fetch";
import { logger } from "@better-auth/core/env";

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface NaverProfile {
/** API response result code */

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
refreshAccessToken,
validateAuthorizationCode,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface NotionProfile {
object: "user";

View File

@@ -1,7 +1,7 @@
import { betterFetch } from "@better-fetch/fetch";
import { BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { createAuthorizationURL } from "../oauth2";
import { createAuthorizationURL } from "@better-auth/core/oauth2";
import { logger } from "@better-auth/core/env";
import { decodeJwt } from "jose";
import { base64 } from "@better-auth/utils/base64";

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
getOAuth2Tokens,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
import { base64 } from "@better-auth/utils/base64";
export interface RedditProfile {

View File

@@ -1,6 +1,9 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { validateAuthorizationCode, refreshAccessToken } from "../oauth2";
import {
validateAuthorizationCode,
refreshAccessToken,
} from "@better-auth/core/oauth2";
export interface RobloxProfile extends Record<string, any> {
/** the user's id */

View File

@@ -1,9 +1,12 @@
import { betterFetch } from "@better-fetch/fetch";
import { BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import {
createAuthorizationURL,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import { logger } from "@better-auth/core/env";
import { refreshAccessToken } from "../oauth2/refresh-access-token";
import { refreshAccessToken } from "@better-auth/core/oauth2";
export interface SalesforceProfile {
sub: string;

View File

@@ -1,6 +1,9 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { validateAuthorizationCode, refreshAccessToken } from "../oauth2";
import {
validateAuthorizationCode,
refreshAccessToken,
} from "@better-auth/core/oauth2";
export interface SlackProfile extends Record<string, any> {
ok: boolean;

View File

@@ -3,7 +3,7 @@ import { getTestInstance } from "../test-utils/test-instance";
import { DEFAULT_SECRET } from "../utils/constants";
import type { GoogleProfile } from "./google";
import { parseSetCookieHeader } from "../cookies";
import { getOAuth2Tokens, refreshAccessToken } from "../oauth2";
import { getOAuth2Tokens, refreshAccessToken } from "@better-auth/core/oauth2";
import { signJWT } from "../crypto/jwt";
import { OAuth2Server } from "oauth2-mock-server";
import { betterFetch } from "@better-fetch/fetch";
@@ -13,7 +13,7 @@ import { getMigrations } from "../db";
let server = new OAuth2Server();
let port = 8005;
vi.mock("../oauth2", async (importOriginal) => {
vi.mock("@better-auth/core/oauth2", async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface SpotifyProfile {
id: string;

View File

@@ -1,6 +1,9 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
import { refreshAccessToken, validateAuthorizationCode } from "../oauth2";
import {
refreshAccessToken,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
/**
* [More info](https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info/)

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
import { decodeJwt } from "jose";
/**

View File

@@ -4,7 +4,7 @@ import {
createAuthorizationURL,
refreshAccessToken,
validateAuthorizationCode,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface TwitterProfile {
data: {

View File

@@ -7,7 +7,7 @@ import {
createAuthorizationURL,
validateAuthorizationCode,
refreshAccessToken,
} from "../oauth2";
} from "@better-auth/core/oauth2";
export interface VkProfile {
user: {

View File

@@ -1,5 +1,8 @@
import { betterFetch } from "@better-fetch/fetch";
import { generateCodeChallenge, validateAuthorizationCode } from "../oauth2";
import {
generateCodeChallenge,
validateAuthorizationCode,
} from "@better-auth/core/oauth2";
import type { OAuthProvider, ProviderOptions } from "@better-auth/core/oauth2";
export type LoginType =

View File

@@ -86,10 +86,18 @@
"typecheck": "tsc --project tsconfig.json"
},
"devDependencies": {
"@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "catalog:",
"jose": "^6.1.0",
"unbuild": "catalog:"
},
"dependencies": {
"better-call": "catalog:",
"zod": "^4.1.5"
},
"peerDependencies": {
"@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "catalog:",
"jose": "^6.1.0"
}
}

View File

@@ -1,6 +1,6 @@
import { betterFetch } from "@better-fetch/fetch";
import { base64Url } from "@better-auth/utils/base64";
import type { OAuth2Tokens, ProviderOptions } from "@better-auth/core/oauth2";
import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
export function createClientCredentialsTokenRequest({
options,

View File

@@ -1,4 +1,4 @@
import type { ProviderOptions } from "@better-auth/core/oauth2";
import type { ProviderOptions } from "./index";
import { generateCodeChallenge } from "./utils";
export async function createAuthorizationURL({

View File

@@ -1,194 +1,22 @@
import type { LiteralString } from "../types/helper";
export type {
OAuth2Tokens,
OAuthProvider,
OAuth2UserInfo,
ProviderOptions,
} from "./oauth-provider";
export interface OAuth2Tokens {
tokenType?: string;
accessToken?: string;
refreshToken?: string;
accessTokenExpiresAt?: Date;
refreshTokenExpiresAt?: Date;
scopes?: string[];
idToken?: string;
}
export type OAuth2UserInfo = {
id: string | number;
name?: string;
email?: string | null;
image?: string;
emailVerified: boolean;
};
export interface OAuthProvider<
T extends Record<string, any> = Record<string, any>,
O extends Record<string, any> = Partial<ProviderOptions>,
> {
id: LiteralString;
createAuthorizationURL: (data: {
state: string;
codeVerifier: string;
scopes?: string[];
redirectURI: string;
display?: string;
loginHint?: string;
}) => Promise<URL> | URL;
name: string;
validateAuthorizationCode: (data: {
code: string;
redirectURI: string;
codeVerifier?: string;
deviceId?: string;
}) => Promise<OAuth2Tokens>;
getUserInfo: (
token: OAuth2Tokens & {
/**
* The user object from the provider
* This is only available for some providers like Apple
*/
user?: {
name?: {
firstName?: string;
lastName?: string;
};
email?: string;
};
},
) => Promise<{
user: OAuth2UserInfo;
data: T;
} | null>;
/**
* Custom function to refresh a token
*/
refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>;
revokeToken?: (token: string) => Promise<void>;
/**
* Verify the id token
* @param token - The id token
* @param nonce - The nonce
* @returns True if the id token is valid, false otherwise
*/
verifyIdToken?: (token: string, nonce?: string) => Promise<boolean>;
/**
* Disable implicit sign up for new users. When set to true for the provider,
* sign-in need to be called with with requestSignUp as true to create new users.
*/
disableImplicitSignUp?: boolean;
/**
* Disable sign up for new users.
*/
disableSignUp?: boolean;
/**
* Options for the provider
*/
options?: O;
}
export type ProviderOptions<Profile extends Record<string, any> = any> = {
/**
* The client ID of your application.
*
* This is usually a string but can be any type depending on the provider.
*/
clientId?: unknown;
/**
* The client secret of your application
*/
clientSecret?: string;
/**
* The scopes you want to request from the provider
*/
scope?: string[];
/**
* Remove default scopes of the provider
*/
disableDefaultScope?: boolean;
/**
* The redirect URL for your application. This is where the provider will
* redirect the user after the sign in process. Make sure this URL is
* whitelisted in the provider's dashboard.
*/
redirectURI?: string;
/**
* The client key of your application
* Tiktok Social Provider uses this field instead of clientId
*/
clientKey?: string;
/**
* Disable provider from allowing users to sign in
* with this provider with an id token sent from the
* client.
*/
disableIdTokenSignIn?: boolean;
/**
* verifyIdToken function to verify the id token
*/
verifyIdToken?: (token: string, nonce?: string) => Promise<boolean>;
/**
* Custom function to get user info from the provider
*/
getUserInfo?: (token: OAuth2Tokens) => Promise<{
user: {
id: string;
name?: string;
email?: string | null;
image?: string;
emailVerified: boolean;
[key: string]: any;
};
data: any;
}>;
/**
* Custom function to refresh a token
*/
refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>;
/**
* Custom function to map the provider profile to a
* user.
*/
mapProfileToUser?: (profile: Profile) =>
| {
id?: string;
name?: string;
email?: string | null;
image?: string;
emailVerified?: boolean;
[key: string]: any;
}
| Promise<{
id?: string;
name?: string;
email?: string | null;
image?: string;
emailVerified?: boolean;
[key: string]: any;
}>;
/**
* Disable implicit sign up for new users. When set to true for the provider,
* sign-in need to be called with with requestSignUp as true to create new users.
*/
disableImplicitSignUp?: boolean;
/**
* Disable sign up for new users.
*/
disableSignUp?: boolean;
/**
* The prompt to use for the authorization code request
*/
prompt?:
| "select_account"
| "consent"
| "login"
| "none"
| "select_account consent";
/**
* The response mode to use for the authorization code request
*/
responseMode?: "query" | "form_post";
/**
* If enabled, the user info will be overridden with the provider user info
* This is useful if you want to use the provider user info to update the user info
*
* @default false
*/
overrideUserInfoOnSignIn?: boolean;
};
export { generateCodeChallenge, getOAuth2Tokens } from "./utils";
export { createAuthorizationURL } from "./create-authorization-url";
export {
createAuthorizationCodeRequest,
validateAuthorizationCode,
validateToken,
} from "./validate-authorization-code";
export {
createRefreshAccessTokenRequest,
refreshAccessToken,
} from "./refresh-access-token";
export {
clientCredentialsToken,
createClientCredentialsTokenRequest,
} from "./client-credentials-token";

View File

@@ -0,0 +1,194 @@
import type { LiteralString } from "../types";
export interface OAuth2Tokens {
tokenType?: string;
accessToken?: string;
refreshToken?: string;
accessTokenExpiresAt?: Date;
refreshTokenExpiresAt?: Date;
scopes?: string[];
idToken?: string;
}
export type OAuth2UserInfo = {
id: string | number;
name?: string;
email?: string | null;
image?: string;
emailVerified: boolean;
};
export interface OAuthProvider<
T extends Record<string, any> = Record<string, any>,
O extends Record<string, any> = Partial<ProviderOptions>,
> {
id: LiteralString;
createAuthorizationURL: (data: {
state: string;
codeVerifier: string;
scopes?: string[];
redirectURI: string;
display?: string;
loginHint?: string;
}) => Promise<URL> | URL;
name: string;
validateAuthorizationCode: (data: {
code: string;
redirectURI: string;
codeVerifier?: string;
deviceId?: string;
}) => Promise<OAuth2Tokens>;
getUserInfo: (
token: OAuth2Tokens & {
/**
* The user object from the provider
* This is only available for some providers like Apple
*/
user?: {
name?: {
firstName?: string;
lastName?: string;
};
email?: string;
};
},
) => Promise<{
user: OAuth2UserInfo;
data: T;
} | null>;
/**
* Custom function to refresh a token
*/
refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>;
revokeToken?: (token: string) => Promise<void>;
/**
* Verify the id token
* @param token - The id token
* @param nonce - The nonce
* @returns True if the id token is valid, false otherwise
*/
verifyIdToken?: (token: string, nonce?: string) => Promise<boolean>;
/**
* Disable implicit sign up for new users. When set to true for the provider,
* sign-in need to be called with with requestSignUp as true to create new users.
*/
disableImplicitSignUp?: boolean;
/**
* Disable sign up for new users.
*/
disableSignUp?: boolean;
/**
* Options for the provider
*/
options?: O;
}
export type ProviderOptions<Profile extends Record<string, any> = any> = {
/**
* The client ID of your application.
*
* This is usually a string but can be any type depending on the provider.
*/
clientId?: unknown;
/**
* The client secret of your application
*/
clientSecret?: string;
/**
* The scopes you want to request from the provider
*/
scope?: string[];
/**
* Remove default scopes of the provider
*/
disableDefaultScope?: boolean;
/**
* The redirect URL for your application. This is where the provider will
* redirect the user after the sign in process. Make sure this URL is
* whitelisted in the provider's dashboard.
*/
redirectURI?: string;
/**
* The client key of your application
* Tiktok Social Provider uses this field instead of clientId
*/
clientKey?: string;
/**
* Disable provider from allowing users to sign in
* with this provider with an id token sent from the
* client.
*/
disableIdTokenSignIn?: boolean;
/**
* verifyIdToken function to verify the id token
*/
verifyIdToken?: (token: string, nonce?: string) => Promise<boolean>;
/**
* Custom function to get user info from the provider
*/
getUserInfo?: (token: OAuth2Tokens) => Promise<{
user: {
id: string;
name?: string;
email?: string | null;
image?: string;
emailVerified: boolean;
[key: string]: any;
};
data: any;
}>;
/**
* Custom function to refresh a token
*/
refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>;
/**
* Custom function to map the provider profile to a
* user.
*/
mapProfileToUser?: (profile: Profile) =>
| {
id?: string;
name?: string;
email?: string | null;
image?: string;
emailVerified?: boolean;
[key: string]: any;
}
| Promise<{
id?: string;
name?: string;
email?: string | null;
image?: string;
emailVerified?: boolean;
[key: string]: any;
}>;
/**
* Disable implicit sign up for new users. When set to true for the provider,
* sign-in need to be called with with requestSignUp as true to create new users.
*/
disableImplicitSignUp?: boolean;
/**
* Disable sign up for new users.
*/
disableSignUp?: boolean;
/**
* The prompt to use for the authorization code request
*/
prompt?:
| "select_account"
| "consent"
| "login"
| "none"
| "select_account consent";
/**
* The response mode to use for the authorization code request
*/
responseMode?: "query" | "form_post";
/**
* If enabled, the user info will be overridden with the provider user info
* This is useful if you want to use the provider user info to update the user info
*
* @default false
*/
overrideUserInfoOnSignIn?: boolean;
};

View File

@@ -1,5 +1,5 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuth2Tokens, ProviderOptions } from "@better-auth/core/oauth2";
import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
import { base64 } from "@better-auth/utils/base64";
export function createRefreshAccessTokenRequest({

View File

@@ -0,0 +1,36 @@
import { base64Url } from "@better-auth/utils/base64";
import type { OAuth2Tokens } from "./oauth-provider";
export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
const getDate = (seconds: number) => {
const now = new Date();
return new Date(now.getTime() + seconds * 1000);
};
return {
tokenType: data.token_type,
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessTokenExpiresAt: data.expires_in
? getDate(data.expires_in)
: undefined,
refreshTokenExpiresAt: data.refresh_token_expires_in
? getDate(data.refresh_token_expires_in)
: undefined,
scopes: data?.scope
? typeof data.scope === "string"
? data.scope.split(" ")
: data.scope
: [],
idToken: data.id_token,
};
}
export async function generateCodeChallenge(codeVerifier: string) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hash = await crypto.subtle.digest("SHA-256", data);
return base64Url.encode(new Uint8Array(hash), {
padding: false,
});
}

View File

@@ -1,7 +1,7 @@
import { betterFetch } from "@better-fetch/fetch";
import { jwtVerify } from "jose";
import type { ProviderOptions } from "@better-auth/core/oauth2";
import { getOAuth2Tokens } from "./utils";
import type { ProviderOptions } from "./index";
import { getOAuth2Tokens } from "./index";
import { base64 } from "@better-auth/utils/base64";
export function createAuthorizationCodeRequest({

23
pnpm-lock.yaml generated
View File

@@ -355,7 +355,7 @@ importers:
version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
geist:
specifier: ^1.4.2
version: 1.4.2(next@15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0))
version: 1.4.2(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0))
input-otp:
specifier: ^1.4.2
version: 1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -599,7 +599,7 @@ importers:
version: 15.8.3(@oramacloud/client@2.1.4)(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(algoliasearch@5.36.0)(lucide-react@0.542.0(react@19.1.1))(next@15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tailwindcss@4.1.13)
geist:
specifier: ^1.4.2
version: 1.4.2(next@15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0))
version: 1.4.2(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0))
gray-matter:
specifier: ^4.0.3
version: 4.0.3
@@ -1092,6 +1092,15 @@ importers:
specifier: ^4.1.5
version: 4.1.5
devDependencies:
'@better-auth/utils':
specifier: 0.3.0
version: 0.3.0
'@better-fetch/fetch':
specifier: 'catalog:'
version: 1.1.18
jose:
specifier: ^6.1.0
version: 6.1.0
unbuild:
specifier: 'catalog:'
version: 3.6.1(sass@1.90.0)(typescript@5.9.2)(vue@3.5.19(typescript@5.9.2))
@@ -15245,7 +15254,7 @@ snapshots:
postcss: 8.4.49
resolve-from: 5.0.0
optionalDependencies:
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -15330,7 +15339,7 @@ snapshots:
'@expo/json-file': 10.0.7
'@react-native/normalize-colors': 0.81.4
debug: 4.4.1
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
resolve-from: 5.0.0
semver: 7.7.2
xml2js: 0.6.0
@@ -19493,7 +19502,7 @@ snapshots:
resolve-from: 5.0.0
optionalDependencies:
'@babel/runtime': 7.28.4
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
transitivePeerDependencies:
- '@babel/core'
- supports-color
@@ -21093,7 +21102,7 @@ snapshots:
expo-keep-awake@15.0.7(expo@54.0.10)(react@19.1.1):
dependencies:
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
react: 19.1.1
expo-linking@7.1.7(expo@54.0.10)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1):
@@ -21751,7 +21760,7 @@ snapshots:
function-bind@1.1.2: {}
geist@1.4.2(next@15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0)):
geist@1.4.2(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0)):
dependencies:
next: 15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.90.0)