@better-auth/core 1.5.0-beta.15 → 1.5.0-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.mts +2 -0
- package/dist/context/global.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/social-providers/apple.mjs +17 -13
- package/dist/social-providers/apple.mjs.map +1 -1
- package/dist/social-providers/google.mjs +14 -10
- package/dist/social-providers/google.mjs.map +1 -1
- package/dist/types/context.d.mts +5 -0
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +97 -9
- package/package.json +1 -1
- package/src/social-providers/apple.ts +25 -21
- package/src/social-providers/google.ts +16 -12
- package/src/types/context.ts +5 -0
- package/src/types/index.ts +2 -0
- package/src/types/init-options.ts +116 -8
package/dist/api/index.d.mts
CHANGED
|
@@ -19,6 +19,7 @@ declare const createAuthMiddleware: {
|
|
|
19
19
|
} & _better_auth_core0.PluginContext<_better_auth_core0.BetterAuthOptions> & _better_auth_core0.InfoContext & {
|
|
20
20
|
options: _better_auth_core0.BetterAuthOptions;
|
|
21
21
|
trustedOrigins: string[];
|
|
22
|
+
trustedProviders: string[];
|
|
22
23
|
isTrustedOrigin: (url: string, settings?: {
|
|
23
24
|
allowRelativePaths: boolean;
|
|
24
25
|
}) => boolean;
|
|
@@ -146,6 +147,7 @@ declare const createAuthMiddleware: {
|
|
|
146
147
|
} & _better_auth_core0.PluginContext<_better_auth_core0.BetterAuthOptions> & _better_auth_core0.InfoContext & {
|
|
147
148
|
options: _better_auth_core0.BetterAuthOptions;
|
|
148
149
|
trustedOrigins: string[];
|
|
150
|
+
trustedProviders: string[];
|
|
149
151
|
isTrustedOrigin: (url: string, settings?: {
|
|
150
152
|
allowRelativePaths: boolean;
|
|
151
153
|
}) => boolean;
|
package/dist/context/global.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Awaitable, AwaitableFunction, LiteralString, LiteralUnion, Prettify, Primitive, UnionToIntersection } from "./types/helper.mjs";
|
|
2
2
|
import { BetterAuthPlugin, BetterAuthPluginErrorCodePart, HookEndpointContext } from "./types/plugin.mjs";
|
|
3
|
-
import { BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, GenerateIdFn, StoreIdentifierOption } from "./types/init-options.mjs";
|
|
3
|
+
import { BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, DynamicBaseURLConfig, GenerateIdFn, StoreIdentifierOption } from "./types/init-options.mjs";
|
|
4
4
|
import { BetterAuthCookie, BetterAuthCookies } from "./types/cookie.mjs";
|
|
5
5
|
import { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext } from "./types/context.mjs";
|
|
6
6
|
import { BetterAuthClientOptions, BetterAuthClientPlugin, ClientAtomListener, ClientFetchOption, ClientStore } from "./types/plugin-client.mjs";
|
|
7
7
|
import { StandardSchemaV1 } from "./types/index.mjs";
|
|
8
|
-
export { AuthContext, Awaitable, AwaitableFunction, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookie, BetterAuthCookies, BetterAuthDBOptions, BetterAuthOptions, BetterAuthPlugin, BetterAuthPluginErrorCodePart, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, ClientAtomListener, ClientFetchOption, ClientStore, GenerateIdFn, GenericEndpointContext, HookEndpointContext, InfoContext, InternalAdapter, LiteralString, LiteralUnion, PluginContext, Prettify, Primitive, StandardSchemaV1, StoreIdentifierOption, UnionToIntersection };
|
|
8
|
+
export { AuthContext, Awaitable, AwaitableFunction, BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookie, BetterAuthCookies, BetterAuthDBOptions, BetterAuthOptions, BetterAuthPlugin, BetterAuthPluginErrorCodePart, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, ClientAtomListener, ClientFetchOption, ClientStore, DynamicBaseURLConfig, GenerateIdFn, GenericEndpointContext, HookEndpointContext, InfoContext, InternalAdapter, LiteralString, LiteralUnion, PluginContext, Prettify, Primitive, StandardSchemaV1, StoreIdentifierOption, UnionToIntersection };
|
|
@@ -39,19 +39,23 @@ const apple = (options) => {
|
|
|
39
39
|
async verifyIdToken(token, nonce) {
|
|
40
40
|
if (options.disableIdTokenSignIn) return false;
|
|
41
41
|
if (options.verifyIdToken) return options.verifyIdToken(token, nonce);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
try {
|
|
43
|
+
const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
|
|
44
|
+
if (!kid || !jwtAlg) return false;
|
|
45
|
+
const { payload: jwtClaims } = await jwtVerify(token, await getApplePublicKey(kid), {
|
|
46
|
+
algorithms: [jwtAlg],
|
|
47
|
+
issuer: "https://appleid.apple.com",
|
|
48
|
+
audience: options.audience && options.audience.length ? options.audience : options.appBundleIdentifier ? options.appBundleIdentifier : options.clientId,
|
|
49
|
+
maxTokenAge: "1h"
|
|
50
|
+
});
|
|
51
|
+
["email_verified", "is_private_email"].forEach((field) => {
|
|
52
|
+
if (jwtClaims[field] !== void 0) jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
53
|
+
});
|
|
54
|
+
if (nonce && jwtClaims.nonce !== nonce) return false;
|
|
55
|
+
return !!jwtClaims;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
55
59
|
},
|
|
56
60
|
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
57
61
|
return refreshAccessToken({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apple.mjs","names":[],"sources":["../../src/social-providers/apple.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\n\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport { APIError } from \"../error\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\nexport interface AppleProfile {\n\t/**\n\t * The subject registered claim identifies the principal that’s the subject\n\t * of the identity token. Because this token is for your app, the value is\n\t * the unique identifier for the user.\n\t */\n\tsub: string;\n\t/**\n\t * A String value representing the user's email address.\n\t * The email address is either the user's real email address or the proxy\n\t * address, depending on their status private email relay service.\n\t */\n\temail: string;\n\t/**\n\t * A string or Boolean value that indicates whether the service verifies\n\t * the email. The value can either be a string (\"true\" or \"false\") or a\n\t * Boolean (true or false). The system may not verify email addresses for\n\t * Sign in with Apple at Work & School users, and this claim is \"false\" or\n\t * false for those users.\n\t */\n\temail_verified: true | \"true\";\n\t/**\n\t * A string or Boolean value that indicates whether the email that the user\n\t * shares is the proxy address. The value can either be a string (\"true\" or\n\t * \"false\") or a Boolean (true or false).\n\t */\n\tis_private_email: boolean;\n\t/**\n\t * An Integer value that indicates whether the user appears to be a real\n\t * person. Use the value of this claim to mitigate fraud. The possible\n\t * values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal). For\n\t * more information, see ASUserDetectionStatus. This claim is present only\n\t * in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14\n\t * and later. The claim isn’t present or supported for web-based apps.\n\t */\n\treal_user_status: number;\n\t/**\n\t * The user’s full name in the format provided during the authorization\n\t * process.\n\t */\n\tname: string;\n\t/**\n\t * The URL to the user's profile picture.\n\t */\n\tpicture: string;\n\tuser?: AppleNonConformUser | undefined;\n}\n\n/**\n * This is the shape of the `user` query parameter that Apple sends the first\n * time the user consents to the app.\n * @see https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server./\n */\nexport interface AppleNonConformUser {\n\tname: {\n\t\tfirstName: string;\n\t\tlastName: string;\n\t};\n\temail: string;\n}\n\nexport interface AppleOptions extends ProviderOptions<AppleProfile> {\n\tclientId: string;\n\tappBundleIdentifier?: string | undefined;\n\taudience?: (string | string[]) | undefined;\n}\n\nexport const apple = (options: AppleOptions) => {\n\tconst tokenEndpoint = \"https://appleid.apple.com/auth/token\";\n\treturn {\n\t\tid: \"apple\",\n\t\tname: \"Apple\",\n\t\tasync createAuthorizationURL({ state, scopes, redirectURI }) {\n\t\t\tconst _scope = options.disableDefaultScope ? [] : [\"email\", \"name\"];\n\t\t\tif (options.scope) _scope.push(...options.scope);\n\t\t\tif (scopes) _scope.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"apple\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://appleid.apple.com/auth/authorize\",\n\t\t\t\tscopes: _scope,\n\t\t\t\tstate,\n\t\t\t\tredirectURI,\n\t\t\t\tresponseMode: \"form_post\",\n\t\t\t\tresponseType: \"code id_token\",\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint,\n\t\t\t});\n\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\t\t\tconst decodedHeader = decodeProtectedHeader(token);\n\t\t\tconst { kid, alg: jwtAlg } = decodedHeader;\n\t\t\tif (!kid || !jwtAlg) return false;\n\t\t\tconst publicKey = await getApplePublicKey(kid);\n\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\tissuer: \"https://appleid.apple.com\",\n\t\t\t\taudience:\n\t\t\t\t\toptions.audience && options.audience.length\n\t\t\t\t\t\t? options.audience\n\t\t\t\t\t\t: options.appBundleIdentifier\n\t\t\t\t\t\t\t? options.appBundleIdentifier\n\t\t\t\t\t\t\t: options.clientId,\n\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t});\n\t\t\t[\"email_verified\", \"is_private_email\"].forEach((field) => {\n\t\t\t\tif (jwtClaims[field] !== undefined) {\n\t\t\t\t\tjwtClaims[field] = Boolean(jwtClaims[field]);\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn !!jwtClaims;\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions,\n\t\t\t\t\t\ttokenEndpoint: \"https://appleid.apple.com/auth/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst profile = decodeJwt<AppleProfile>(token.idToken);\n\t\t\tif (!profile) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// TODO: \"\" masking will be removed when the name field is made optional\n\t\t\tlet name: string;\n\t\t\tif (token.user?.name) {\n\t\t\t\tconst firstName = token.user.name.firstName || \"\";\n\t\t\t\tconst lastName = token.user.name.lastName || \"\";\n\t\t\t\tconst fullName = `${firstName} ${lastName}`.trim();\n\t\t\t\tname = fullName;\n\t\t\t} else {\n\t\t\t\tname = profile.name || \"\";\n\t\t\t}\n\n\t\t\tconst emailVerified =\n\t\t\t\ttypeof profile.email_verified === \"boolean\"\n\t\t\t\t\t? profile.email_verified\n\t\t\t\t\t: profile.email_verified === \"true\";\n\t\t\tconst enrichedProfile = {\n\t\t\t\t...profile,\n\t\t\t\tname,\n\t\t\t};\n\t\t\tconst userMap = await options.mapProfileToUser?.(enrichedProfile);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: profile.sub,\n\t\t\t\t\tname: enrichedProfile.name,\n\t\t\t\t\temailVerified: emailVerified,\n\t\t\t\t\temail: profile.email,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: enrichedProfile,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<AppleProfile>;\n};\n\nexport const getApplePublicKey = async (kid: string) => {\n\tconst APPLE_BASE_URL = \"https://appleid.apple.com\";\n\tconst JWKS_APPLE_URI = \"/auth/keys\";\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;AA6EA,MAAa,SAAS,YAA0B;CAC/C,MAAM,gBAAgB;AACtB,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAAE,OAAO,QAAQ,eAAe;GAC5D,MAAM,SAAS,QAAQ,sBAAsB,EAAE,GAAG,CAAC,SAAS,OAAO;AACnE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAChD,OAAI,OAAQ,QAAO,KAAK,GAAG,OAAO;AAWlC,UAVY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA,cAAc;IACd,cAAc;IACd,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA;IACA,CAAC;;EAEH,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;
|
|
1
|
+
{"version":3,"file":"apple.mjs","names":[],"sources":["../../src/social-providers/apple.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\n\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport { APIError } from \"../error\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\nexport interface AppleProfile {\n\t/**\n\t * The subject registered claim identifies the principal that’s the subject\n\t * of the identity token. Because this token is for your app, the value is\n\t * the unique identifier for the user.\n\t */\n\tsub: string;\n\t/**\n\t * A String value representing the user's email address.\n\t * The email address is either the user's real email address or the proxy\n\t * address, depending on their status private email relay service.\n\t */\n\temail: string;\n\t/**\n\t * A string or Boolean value that indicates whether the service verifies\n\t * the email. The value can either be a string (\"true\" or \"false\") or a\n\t * Boolean (true or false). The system may not verify email addresses for\n\t * Sign in with Apple at Work & School users, and this claim is \"false\" or\n\t * false for those users.\n\t */\n\temail_verified: true | \"true\";\n\t/**\n\t * A string or Boolean value that indicates whether the email that the user\n\t * shares is the proxy address. The value can either be a string (\"true\" or\n\t * \"false\") or a Boolean (true or false).\n\t */\n\tis_private_email: boolean;\n\t/**\n\t * An Integer value that indicates whether the user appears to be a real\n\t * person. Use the value of this claim to mitigate fraud. The possible\n\t * values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal). For\n\t * more information, see ASUserDetectionStatus. This claim is present only\n\t * in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14\n\t * and later. The claim isn’t present or supported for web-based apps.\n\t */\n\treal_user_status: number;\n\t/**\n\t * The user’s full name in the format provided during the authorization\n\t * process.\n\t */\n\tname: string;\n\t/**\n\t * The URL to the user's profile picture.\n\t */\n\tpicture: string;\n\tuser?: AppleNonConformUser | undefined;\n}\n\n/**\n * This is the shape of the `user` query parameter that Apple sends the first\n * time the user consents to the app.\n * @see https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server./\n */\nexport interface AppleNonConformUser {\n\tname: {\n\t\tfirstName: string;\n\t\tlastName: string;\n\t};\n\temail: string;\n}\n\nexport interface AppleOptions extends ProviderOptions<AppleProfile> {\n\tclientId: string;\n\tappBundleIdentifier?: string | undefined;\n\taudience?: (string | string[]) | undefined;\n}\n\nexport const apple = (options: AppleOptions) => {\n\tconst tokenEndpoint = \"https://appleid.apple.com/auth/token\";\n\treturn {\n\t\tid: \"apple\",\n\t\tname: \"Apple\",\n\t\tasync createAuthorizationURL({ state, scopes, redirectURI }) {\n\t\t\tconst _scope = options.disableDefaultScope ? [] : [\"email\", \"name\"];\n\t\t\tif (options.scope) _scope.push(...options.scope);\n\t\t\tif (scopes) _scope.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"apple\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://appleid.apple.com/auth/authorize\",\n\t\t\t\tscopes: _scope,\n\t\t\t\tstate,\n\t\t\t\tredirectURI,\n\t\t\t\tresponseMode: \"form_post\",\n\t\t\t\tresponseType: \"code id_token\",\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint,\n\t\t\t});\n\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst decodedHeader = decodeProtectedHeader(token);\n\t\t\t\tconst { kid, alg: jwtAlg } = decodedHeader;\n\t\t\t\tif (!kid || !jwtAlg) return false;\n\t\t\t\tconst publicKey = await getApplePublicKey(kid);\n\t\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\t\tissuer: \"https://appleid.apple.com\",\n\t\t\t\t\taudience:\n\t\t\t\t\t\toptions.audience && options.audience.length\n\t\t\t\t\t\t\t? options.audience\n\t\t\t\t\t\t\t: options.appBundleIdentifier\n\t\t\t\t\t\t\t\t? options.appBundleIdentifier\n\t\t\t\t\t\t\t\t: options.clientId,\n\t\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t\t});\n\t\t\t\t[\"email_verified\", \"is_private_email\"].forEach((field) => {\n\t\t\t\t\tif (jwtClaims[field] !== undefined) {\n\t\t\t\t\t\tjwtClaims[field] = Boolean(jwtClaims[field]);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn !!jwtClaims;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions,\n\t\t\t\t\t\ttokenEndpoint: \"https://appleid.apple.com/auth/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst profile = decodeJwt<AppleProfile>(token.idToken);\n\t\t\tif (!profile) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// TODO: \"\" masking will be removed when the name field is made optional\n\t\t\tlet name: string;\n\t\t\tif (token.user?.name) {\n\t\t\t\tconst firstName = token.user.name.firstName || \"\";\n\t\t\t\tconst lastName = token.user.name.lastName || \"\";\n\t\t\t\tconst fullName = `${firstName} ${lastName}`.trim();\n\t\t\t\tname = fullName;\n\t\t\t} else {\n\t\t\t\tname = profile.name || \"\";\n\t\t\t}\n\n\t\t\tconst emailVerified =\n\t\t\t\ttypeof profile.email_verified === \"boolean\"\n\t\t\t\t\t? profile.email_verified\n\t\t\t\t\t: profile.email_verified === \"true\";\n\t\t\tconst enrichedProfile = {\n\t\t\t\t...profile,\n\t\t\t\tname,\n\t\t\t};\n\t\t\tconst userMap = await options.mapProfileToUser?.(enrichedProfile);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: profile.sub,\n\t\t\t\t\tname: enrichedProfile.name,\n\t\t\t\t\temailVerified: emailVerified,\n\t\t\t\t\temail: profile.email,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: enrichedProfile,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<AppleProfile>;\n};\n\nexport const getApplePublicKey = async (kid: string) => {\n\tconst APPLE_BASE_URL = \"https://appleid.apple.com\";\n\tconst JWKS_APPLE_URI = \"/auth/keys\";\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;AA6EA,MAAa,SAAS,YAA0B;CAC/C,MAAM,gBAAgB;AACtB,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAAE,OAAO,QAAQ,eAAe;GAC5D,MAAM,SAAS,QAAQ,sBAAsB,EAAE,GAAG,CAAC,SAAS,OAAO;AACnE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAChD,OAAI,OAAQ,QAAO,KAAK,GAAG,OAAO;AAWlC,UAVY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA,cAAc;IACd,cAAc;IACd,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA;IACA,CAAC;;EAEH,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;AAE3C,OAAI;IAEH,MAAM,EAAE,KAAK,KAAK,WADI,sBAAsB,MAAM;AAElD,QAAI,CAAC,OAAO,CAAC,OAAQ,QAAO;IAE5B,MAAM,EAAE,SAAS,cAAc,MAAM,UAAU,OAD7B,MAAM,kBAAkB,IAAI,EACmB;KAChE,YAAY,CAAC,OAAO;KACpB,QAAQ;KACR,UACC,QAAQ,YAAY,QAAQ,SAAS,SAClC,QAAQ,WACR,QAAQ,sBACP,QAAQ,sBACR,QAAQ;KACb,aAAa;KACb,CAAC;AACF,KAAC,kBAAkB,mBAAmB,CAAC,SAAS,UAAU;AACzD,SAAI,UAAU,WAAW,OACxB,WAAU,SAAS,QAAQ,UAAU,OAAO;MAE5C;AACF,QAAI,SAAS,UAAU,UAAU,MAChC,QAAO;AAER,WAAO,CAAC,CAAC;WACF;AACP,WAAO;;;EAGT,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;AACxB,UAAO,mBAAmB;IACzB;IACA;IACA,eAAe;IACf,CAAC;;EAEL,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,UAAU,UAAwB,MAAM,QAAQ;AACtD,OAAI,CAAC,QACJ,QAAO;GAIR,IAAI;AACJ,OAAI,MAAM,MAAM,KAIf,QADiB,GAFC,MAAM,KAAK,KAAK,aAAa,GAEjB,GADb,MAAM,KAAK,KAAK,YAAY,KACD,MAAM;OAGlD,QAAO,QAAQ,QAAQ;GAGxB,MAAM,gBACL,OAAO,QAAQ,mBAAmB,YAC/B,QAAQ,iBACR,QAAQ,mBAAmB;GAC/B,MAAM,kBAAkB;IACvB,GAAG;IACH;IACA;GACD,MAAM,UAAU,MAAM,QAAQ,mBAAmB,gBAAgB;AACjE,UAAO;IACN,MAAM;KACL,IAAI,QAAQ;KACZ,MAAM,gBAAgB;KACP;KACf,OAAO,QAAQ;KACf,GAAG;KACH;IACD,MAAM;IACN;;EAEF;EACA;;AAGF,MAAa,oBAAoB,OAAO,QAAgB;CAGvD,MAAM,EAAE,SAAS,MAAM,YASpB,sCAAqC;AACxC,KAAI,CAAC,MAAM,KACV,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,kBACT,CAAC;CAEH,MAAM,MAAM,KAAK,KAAK,MAAM,QAAQ,IAAI,QAAQ,IAAI;AACpD,KAAI,CAAC,IACJ,OAAM,IAAI,MAAM,gBAAgB,IAAI,YAAY;AAEjD,QAAO,MAAM,UAAU,KAAK,IAAI,IAAI"}
|
|
@@ -65,16 +65,20 @@ const google = (options) => {
|
|
|
65
65
|
async verifyIdToken(token, nonce) {
|
|
66
66
|
if (options.disableIdTokenSignIn) return false;
|
|
67
67
|
if (options.verifyIdToken) return options.verifyIdToken(token, nonce);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
try {
|
|
69
|
+
const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
|
|
70
|
+
if (!kid || !jwtAlg) return false;
|
|
71
|
+
const { payload: jwtClaims } = await jwtVerify(token, await getGooglePublicKey(kid), {
|
|
72
|
+
algorithms: [jwtAlg],
|
|
73
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
74
|
+
audience: options.clientId,
|
|
75
|
+
maxTokenAge: "1h"
|
|
76
|
+
});
|
|
77
|
+
if (nonce && jwtClaims.nonce !== nonce) return false;
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
78
82
|
},
|
|
79
83
|
async getUserInfo(token) {
|
|
80
84
|
if (options.getUserInfo) return options.getUserInfo(token);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.mjs","names":[],"sources":["../../src/social-providers/google.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport { logger } from \"../env\";\nimport { APIError, BetterAuthError } from \"../error\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\n\nexport interface GoogleProfile {\n\taud: string;\n\tazp: string;\n\temail: string;\n\temail_verified: boolean;\n\texp: number;\n\t/**\n\t * The family name of the user, or last name in most\n\t * Western languages.\n\t */\n\tfamily_name: string;\n\t/**\n\t * The given name of the user, or first name in most\n\t * Western languages.\n\t */\n\tgiven_name: string;\n\thd?: string | undefined;\n\tiat: number;\n\tiss: string;\n\tjti?: string | undefined;\n\tlocale?: string | undefined;\n\tname: string;\n\tnbf?: number | undefined;\n\tpicture: string;\n\tsub: string;\n}\n\nexport interface GoogleOptions extends ProviderOptions<GoogleProfile> {\n\tclientId: string;\n\t/**\n\t * The access type to use for the authorization code request\n\t */\n\taccessType?: (\"offline\" | \"online\") | undefined;\n\t/**\n\t * The display mode to use for the authorization code request\n\t */\n\tdisplay?: (\"page\" | \"popup\" | \"touch\" | \"wap\") | undefined;\n\t/**\n\t * The hosted domain of the user\n\t */\n\thd?: string | undefined;\n}\n\nexport const google = (options: GoogleOptions) => {\n\treturn {\n\t\tid: \"google\",\n\t\tname: \"Google\",\n\t\tasync createAuthorizationURL({\n\t\t\tstate,\n\t\t\tscopes,\n\t\t\tcodeVerifier,\n\t\t\tredirectURI,\n\t\t\tloginHint,\n\t\t\tdisplay,\n\t\t}) {\n\t\t\tif (!options.clientId || !options.clientSecret) {\n\t\t\t\tlogger.error(\n\t\t\t\t\t\"Client Id and Client Secret is required for Google. Make sure to provide them in the options.\",\n\t\t\t\t);\n\t\t\t\tthrow new BetterAuthError(\"CLIENT_ID_AND_SECRET_REQUIRED\");\n\t\t\t}\n\t\t\tif (!codeVerifier) {\n\t\t\t\tthrow new BetterAuthError(\"codeVerifier is required for Google\");\n\t\t\t}\n\t\t\tconst _scopes = options.disableDefaultScope\n\t\t\t\t? []\n\t\t\t\t: [\"email\", \"profile\", \"openid\"];\n\t\t\tif (options.scope) _scopes.push(...options.scope);\n\t\t\tif (scopes) _scopes.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"google\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n\t\t\t\tscopes: _scopes,\n\t\t\t\tstate,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\tprompt: options.prompt,\n\t\t\t\taccessType: options.accessType,\n\t\t\t\tdisplay: display || options.display,\n\t\t\t\tloginHint,\n\t\t\t\thd: options.hd,\n\t\t\t\tadditionalParams: {\n\t\t\t\t\tinclude_granted_scopes: \"true\",\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t});\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientKey: options.clientKey,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\n\t\t\t// Verify JWT integrity\n\t\t\t// See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token\n\n\t\t\tconst { kid, alg: jwtAlg } = decodeProtectedHeader(token);\n\t\t\tif (!kid || !jwtAlg) return false;\n\n\t\t\tconst publicKey = await getGooglePublicKey(kid);\n\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\tissuer: [\"https://accounts.google.com\", \"accounts.google.com\"],\n\t\t\t\taudience: options.clientId,\n\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t});\n\n\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\treturn true;\n\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst user = decodeJwt(token.idToken) as GoogleProfile;\n\t\t\tconst userMap = await options.mapProfileToUser?.(user);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: user.sub,\n\t\t\t\t\tname: user.name,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\timage: user.picture,\n\t\t\t\t\temailVerified: user.email_verified,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: user,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<GoogleProfile>;\n};\n\nexport const getGooglePublicKey = async (kid: string) => {\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(\"https://www.googleapis.com/oauth2/v3/certs\");\n\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;;;AAsDA,MAAa,UAAU,YAA2B;AACjD,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAC5B,OACA,QACA,cACA,aACA,WACA,WACE;AACF,OAAI,CAAC,QAAQ,YAAY,CAAC,QAAQ,cAAc;AAC/C,WAAO,MACN,gGACA;AACD,UAAM,IAAI,gBAAgB,gCAAgC;;AAE3D,OAAI,CAAC,aACJ,OAAM,IAAI,gBAAgB,sCAAsC;GAEjE,MAAM,UAAU,QAAQ,sBACrB,EAAE,GACF;IAAC;IAAS;IAAW;IAAS;AACjC,OAAI,QAAQ,MAAO,SAAQ,KAAK,GAAG,QAAQ,MAAM;AACjD,OAAI,OAAQ,SAAQ,KAAK,GAAG,OAAO;AAkBnC,UAjBY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA;IACA,QAAQ,QAAQ;IAChB,YAAY,QAAQ;IACpB,SAAS,WAAW,QAAQ;IAC5B;IACA,IAAI,QAAQ;IACZ,kBAAkB,EACjB,wBAAwB,QACxB;IACD,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA,eAAe;IACf,CAAC;;EAEH,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;AACxB,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,WAAW,QAAQ;KACnB,cAAc,QAAQ;KACtB;IACD,eAAe;IACf,CAAC;;EAEL,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;
|
|
1
|
+
{"version":3,"file":"google.mjs","names":[],"sources":["../../src/social-providers/google.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport { logger } from \"../env\";\nimport { APIError, BetterAuthError } from \"../error\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\n\nexport interface GoogleProfile {\n\taud: string;\n\tazp: string;\n\temail: string;\n\temail_verified: boolean;\n\texp: number;\n\t/**\n\t * The family name of the user, or last name in most\n\t * Western languages.\n\t */\n\tfamily_name: string;\n\t/**\n\t * The given name of the user, or first name in most\n\t * Western languages.\n\t */\n\tgiven_name: string;\n\thd?: string | undefined;\n\tiat: number;\n\tiss: string;\n\tjti?: string | undefined;\n\tlocale?: string | undefined;\n\tname: string;\n\tnbf?: number | undefined;\n\tpicture: string;\n\tsub: string;\n}\n\nexport interface GoogleOptions extends ProviderOptions<GoogleProfile> {\n\tclientId: string;\n\t/**\n\t * The access type to use for the authorization code request\n\t */\n\taccessType?: (\"offline\" | \"online\") | undefined;\n\t/**\n\t * The display mode to use for the authorization code request\n\t */\n\tdisplay?: (\"page\" | \"popup\" | \"touch\" | \"wap\") | undefined;\n\t/**\n\t * The hosted domain of the user\n\t */\n\thd?: string | undefined;\n}\n\nexport const google = (options: GoogleOptions) => {\n\treturn {\n\t\tid: \"google\",\n\t\tname: \"Google\",\n\t\tasync createAuthorizationURL({\n\t\t\tstate,\n\t\t\tscopes,\n\t\t\tcodeVerifier,\n\t\t\tredirectURI,\n\t\t\tloginHint,\n\t\t\tdisplay,\n\t\t}) {\n\t\t\tif (!options.clientId || !options.clientSecret) {\n\t\t\t\tlogger.error(\n\t\t\t\t\t\"Client Id and Client Secret is required for Google. Make sure to provide them in the options.\",\n\t\t\t\t);\n\t\t\t\tthrow new BetterAuthError(\"CLIENT_ID_AND_SECRET_REQUIRED\");\n\t\t\t}\n\t\t\tif (!codeVerifier) {\n\t\t\t\tthrow new BetterAuthError(\"codeVerifier is required for Google\");\n\t\t\t}\n\t\t\tconst _scopes = options.disableDefaultScope\n\t\t\t\t? []\n\t\t\t\t: [\"email\", \"profile\", \"openid\"];\n\t\t\tif (options.scope) _scopes.push(...options.scope);\n\t\t\tif (scopes) _scopes.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"google\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n\t\t\t\tscopes: _scopes,\n\t\t\t\tstate,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\tprompt: options.prompt,\n\t\t\t\taccessType: options.accessType,\n\t\t\t\tdisplay: display || options.display,\n\t\t\t\tloginHint,\n\t\t\t\thd: options.hd,\n\t\t\t\tadditionalParams: {\n\t\t\t\t\tinclude_granted_scopes: \"true\",\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t});\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientKey: options.clientKey,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\n\t\t\t// Verify JWT integrity\n\t\t\t// See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token\n\n\t\t\ttry {\n\t\t\t\tconst { kid, alg: jwtAlg } = decodeProtectedHeader(token);\n\t\t\t\tif (!kid || !jwtAlg) return false;\n\n\t\t\t\tconst publicKey = await getGooglePublicKey(kid);\n\t\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\t\tissuer: [\"https://accounts.google.com\", \"accounts.google.com\"],\n\t\t\t\t\taudience: options.clientId,\n\t\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t\t});\n\n\t\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst user = decodeJwt(token.idToken) as GoogleProfile;\n\t\t\tconst userMap = await options.mapProfileToUser?.(user);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: user.sub,\n\t\t\t\t\tname: user.name,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\timage: user.picture,\n\t\t\t\t\temailVerified: user.email_verified,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: user,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<GoogleProfile>;\n};\n\nexport const getGooglePublicKey = async (kid: string) => {\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(\"https://www.googleapis.com/oauth2/v3/certs\");\n\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;;;AAsDA,MAAa,UAAU,YAA2B;AACjD,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAC5B,OACA,QACA,cACA,aACA,WACA,WACE;AACF,OAAI,CAAC,QAAQ,YAAY,CAAC,QAAQ,cAAc;AAC/C,WAAO,MACN,gGACA;AACD,UAAM,IAAI,gBAAgB,gCAAgC;;AAE3D,OAAI,CAAC,aACJ,OAAM,IAAI,gBAAgB,sCAAsC;GAEjE,MAAM,UAAU,QAAQ,sBACrB,EAAE,GACF;IAAC;IAAS;IAAW;IAAS;AACjC,OAAI,QAAQ,MAAO,SAAQ,KAAK,GAAG,QAAQ,MAAM;AACjD,OAAI,OAAQ,SAAQ,KAAK,GAAG,OAAO;AAkBnC,UAjBY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA;IACA,QAAQ,QAAQ;IAChB,YAAY,QAAQ;IACpB,SAAS,WAAW,QAAQ;IAC5B;IACA,IAAI,QAAQ;IACZ,kBAAkB,EACjB,wBAAwB,QACxB;IACD,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA,eAAe;IACf,CAAC;;EAEH,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;AACxB,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,WAAW,QAAQ;KACnB,cAAc,QAAQ;KACtB;IACD,eAAe;IACf,CAAC;;EAEL,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;AAM3C,OAAI;IACH,MAAM,EAAE,KAAK,KAAK,WAAW,sBAAsB,MAAM;AACzD,QAAI,CAAC,OAAO,CAAC,OAAQ,QAAO;IAG5B,MAAM,EAAE,SAAS,cAAc,MAAM,UAAU,OAD7B,MAAM,mBAAmB,IAAI,EACkB;KAChE,YAAY,CAAC,OAAO;KACpB,QAAQ,CAAC,+BAA+B,sBAAsB;KAC9D,UAAU,QAAQ;KAClB,aAAa;KACb,CAAC;AAEF,QAAI,SAAS,UAAU,UAAU,MAChC,QAAO;AAGR,WAAO;WACA;AACP,WAAO;;;EAGT,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,OAAO,UAAU,MAAM,QAAQ;GACrC,MAAM,UAAU,MAAM,QAAQ,mBAAmB,KAAK;AACtD,UAAO;IACN,MAAM;KACL,IAAI,KAAK;KACT,MAAM,KAAK;KACX,OAAO,KAAK;KACZ,OAAO,KAAK;KACZ,eAAe,KAAK;KACpB,GAAG;KACH;IACD,MAAM;IACN;;EAEF;EACA;;AAGF,MAAa,qBAAqB,OAAO,QAAgB;CACxD,MAAM,EAAE,SAAS,MAAM,YASpB,6CAA6C;AAEhD,KAAI,CAAC,MAAM,KACV,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,kBACT,CAAC;CAGH,MAAM,MAAM,KAAK,KAAK,MAAM,QAAQ,IAAI,QAAQ,IAAI;AACpD,KAAI,CAAC,IACJ,OAAM,IAAI,MAAM,gBAAgB,IAAI,YAAY;AAGjD,QAAO,MAAM,UAAU,KAAK,IAAI,IAAI"}
|
package/dist/types/context.d.mts
CHANGED
|
@@ -137,6 +137,11 @@ type InfoContext = {
|
|
|
137
137
|
type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> = PluginContext<Options> & InfoContext & {
|
|
138
138
|
options: Options;
|
|
139
139
|
trustedOrigins: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Resolved list of trusted providers for account linking.
|
|
142
|
+
* Populated from "account.accountLinking.trustedProviders" (supports static array or async function).
|
|
143
|
+
*/
|
|
144
|
+
trustedProviders: string[];
|
|
140
145
|
/**
|
|
141
146
|
* Verifies whether url is a trusted origin according to the "trustedOrigins" configuration
|
|
142
147
|
* @param url The url to verify against the "trustedOrigins" configuration
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Awaitable, AwaitableFunction, LiteralString, LiteralUnion, Prettify, Primitive, UnionToIntersection } from "./helper.mjs";
|
|
2
2
|
import { BetterAuthPlugin, BetterAuthPluginErrorCodePart, HookEndpointContext } from "./plugin.mjs";
|
|
3
|
-
import { BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, GenerateIdFn, StoreIdentifierOption } from "./init-options.mjs";
|
|
3
|
+
import { BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, DynamicBaseURLConfig, GenerateIdFn, StoreIdentifierOption } from "./init-options.mjs";
|
|
4
4
|
import { BetterAuthCookie, BetterAuthCookies } from "./cookie.mjs";
|
|
5
5
|
import { AuthContext, BetterAuthPluginRegistry, BetterAuthPluginRegistryIdentifier, GenericEndpointContext, InfoContext, InternalAdapter, PluginContext } from "./context.mjs";
|
|
6
6
|
import { BetterAuthClientOptions, BetterAuthClientPlugin, ClientAtomListener, ClientFetchOption, ClientStore } from "./plugin-client.mjs";
|
|
@@ -27,6 +27,49 @@ type GenerateIdFn = (options: {
|
|
|
27
27
|
model: ModelNames;
|
|
28
28
|
size?: number | undefined;
|
|
29
29
|
}) => string | false;
|
|
30
|
+
/**
|
|
31
|
+
* Configuration for dynamic base URL resolution.
|
|
32
|
+
* Allows Better Auth to work with multiple domains (e.g., Vercel preview deployments).
|
|
33
|
+
*/
|
|
34
|
+
type DynamicBaseURLConfig = {
|
|
35
|
+
/**
|
|
36
|
+
* List of allowed hostnames. Supports wildcard patterns.
|
|
37
|
+
*
|
|
38
|
+
* The derived host from the request will be validated against this list.
|
|
39
|
+
* Uses the same wildcard matching as `trustedOrigins`.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* allowedHosts: [
|
|
44
|
+
* "myapp.com", // Exact match
|
|
45
|
+
* "*.vercel.app", // Any Vercel preview
|
|
46
|
+
* "preview-*.myapp.com" // Pattern match
|
|
47
|
+
* ]
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
allowedHosts: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Fallback URL to use if the derived host doesn't match any allowed host.
|
|
53
|
+
* If not set, Better Auth will throw an error when the host doesn't match.
|
|
54
|
+
*
|
|
55
|
+
* @example "https://myapp.com"
|
|
56
|
+
*/
|
|
57
|
+
fallback?: string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Protocol to use when constructing the URL.
|
|
60
|
+
* - `"https"`: Always use HTTPS (recommended for production)
|
|
61
|
+
* - `"http"`: Always use HTTP (for local development)
|
|
62
|
+
* - `"auto"`: Derive from `x-forwarded-proto` header or default to HTTPS
|
|
63
|
+
*
|
|
64
|
+
* @default "auto"
|
|
65
|
+
*/
|
|
66
|
+
protocol?: "http" | "https" | "auto" | undefined;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Base URL configuration.
|
|
70
|
+
* Can be a static string or a dynamic config for multi-domain deployments.
|
|
71
|
+
*/
|
|
72
|
+
type BaseURLConfig = string | DynamicBaseURLConfig;
|
|
30
73
|
interface BetterAuthRateLimitStorage {
|
|
31
74
|
get: (key: string) => Promise<RateLimit | null | undefined>;
|
|
32
75
|
set: (key: string, value: RateLimit, update?: boolean | undefined) => Promise<void>;
|
|
@@ -281,12 +324,27 @@ type BetterAuthOptions = {
|
|
|
281
324
|
/**
|
|
282
325
|
* Base URL for the Better Auth. This is typically the
|
|
283
326
|
* root URL where your application server is hosted.
|
|
284
|
-
* If not explicitly set,
|
|
285
|
-
* the system will check the following environment variable:
|
|
286
327
|
*
|
|
287
|
-
*
|
|
328
|
+
* Can be configured as:
|
|
329
|
+
* - A static string: `"https://myapp.com"`
|
|
330
|
+
* - A dynamic config with allowed hosts for multi-domain deployments
|
|
331
|
+
*
|
|
332
|
+
* If not explicitly set, the system will check environment variables:
|
|
333
|
+
* `BETTER_AUTH_URL`, `NEXT_PUBLIC_BETTER_AUTH_URL`, etc.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* // Static URL
|
|
338
|
+
* baseURL: "https://myapp.com"
|
|
339
|
+
*
|
|
340
|
+
* // Dynamic with allowed hosts (for Vercel, multi-domain, etc.)
|
|
341
|
+
* baseURL: {
|
|
342
|
+
* allowedHosts: ["myapp.com", "*.vercel.app", "preview-*.myapp.com"],
|
|
343
|
+
* fallback: "https://myapp.com"
|
|
344
|
+
* }
|
|
345
|
+
* ```
|
|
288
346
|
*/
|
|
289
|
-
baseURL?:
|
|
347
|
+
baseURL?: BaseURLConfig | undefined;
|
|
290
348
|
/**
|
|
291
349
|
* Base path for the Better Auth. This is typically
|
|
292
350
|
* the path where the
|
|
@@ -398,7 +456,6 @@ type BetterAuthOptions = {
|
|
|
398
456
|
* it contains the token as well
|
|
399
457
|
* @param token the token to send the verification email to
|
|
400
458
|
*/
|
|
401
|
-
|
|
402
459
|
data: {
|
|
403
460
|
user: User;
|
|
404
461
|
url: string;
|
|
@@ -407,7 +464,6 @@ type BetterAuthOptions = {
|
|
|
407
464
|
/**
|
|
408
465
|
* The request object
|
|
409
466
|
*/
|
|
410
|
-
|
|
411
467
|
request?: Request) => Promise<void>;
|
|
412
468
|
/**
|
|
413
469
|
* Send a verification email automatically after sign up.
|
|
@@ -544,6 +600,20 @@ type BetterAuthOptions = {
|
|
|
544
600
|
* @default false
|
|
545
601
|
*/
|
|
546
602
|
revokeSessionsOnPasswordReset?: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* A callback function that is triggered when a user tries to sign up
|
|
605
|
+
* with an email that already exists. Useful for notifying the existing user
|
|
606
|
+
* that someone attempted to register with their email.
|
|
607
|
+
*
|
|
608
|
+
* This is only called when `requireEmailVerification: true` or `autoSignIn: false`.
|
|
609
|
+
*/
|
|
610
|
+
onExistingUserSignUp?: (
|
|
611
|
+
/**
|
|
612
|
+
* @param user the existing user from the database
|
|
613
|
+
*/
|
|
614
|
+
data: {
|
|
615
|
+
user: User;
|
|
616
|
+
}, request?: Request) => Promise<void>;
|
|
547
617
|
} | undefined;
|
|
548
618
|
/**
|
|
549
619
|
* list of social providers
|
|
@@ -781,9 +851,27 @@ type BetterAuthOptions = {
|
|
|
781
851
|
*/
|
|
782
852
|
disableImplicitLinking?: boolean;
|
|
783
853
|
/**
|
|
784
|
-
* List of trusted providers
|
|
854
|
+
* List of trusted providers. Can be a static array or a function
|
|
855
|
+
* that returns providers dynamically. The function is called
|
|
856
|
+
* during context init (with `request` undefined) and again
|
|
857
|
+
* on each request (with the incoming Request). It must be
|
|
858
|
+
* resilient to `request` being undefined.
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* ```ts
|
|
862
|
+
* trustedProviders: ["google", "github"]
|
|
863
|
+
* ```
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```ts
|
|
867
|
+
* trustedProviders: async (request) => {
|
|
868
|
+
* if (!request) return [];
|
|
869
|
+
* const providers = await getTrustedProvidersForTenant(request);
|
|
870
|
+
* return providers;
|
|
871
|
+
* }
|
|
872
|
+
* ```
|
|
785
873
|
*/
|
|
786
|
-
trustedProviders?: Array<LiteralUnion<SocialProviderList[number] | "email-password", string
|
|
874
|
+
trustedProviders?: Array<LiteralUnion<SocialProviderList[number] | "email-password", string>> | ((request?: Request | undefined) => Awaitable<Array<LiteralUnion<SocialProviderList[number] | "email-password", string>>>);
|
|
787
875
|
/**
|
|
788
876
|
* If enabled (true), this will allow users to manually linking accounts with different email addresses than the main user.
|
|
789
877
|
*
|
|
@@ -1206,5 +1294,5 @@ type BetterAuthOptions = {
|
|
|
1206
1294
|
};
|
|
1207
1295
|
};
|
|
1208
1296
|
//#endregion
|
|
1209
|
-
export { BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, GenerateIdFn, StoreIdentifierOption };
|
|
1297
|
+
export { BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, DynamicBaseURLConfig, GenerateIdFn, StoreIdentifierOption };
|
|
1210
1298
|
//# sourceMappingURL=init-options.d.mts.map
|
package/package.json
CHANGED
|
@@ -112,30 +112,34 @@ export const apple = (options: AppleOptions) => {
|
|
|
112
112
|
if (options.verifyIdToken) {
|
|
113
113
|
return options.verifyIdToken(token, nonce);
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
jwtClaims[field]
|
|
115
|
+
try {
|
|
116
|
+
const decodedHeader = decodeProtectedHeader(token);
|
|
117
|
+
const { kid, alg: jwtAlg } = decodedHeader;
|
|
118
|
+
if (!kid || !jwtAlg) return false;
|
|
119
|
+
const publicKey = await getApplePublicKey(kid);
|
|
120
|
+
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
121
|
+
algorithms: [jwtAlg],
|
|
122
|
+
issuer: "https://appleid.apple.com",
|
|
123
|
+
audience:
|
|
124
|
+
options.audience && options.audience.length
|
|
125
|
+
? options.audience
|
|
126
|
+
: options.appBundleIdentifier
|
|
127
|
+
? options.appBundleIdentifier
|
|
128
|
+
: options.clientId,
|
|
129
|
+
maxTokenAge: "1h",
|
|
130
|
+
});
|
|
131
|
+
["email_verified", "is_private_email"].forEach((field) => {
|
|
132
|
+
if (jwtClaims[field] !== undefined) {
|
|
133
|
+
jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
137
|
+
return false;
|
|
133
138
|
}
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
return !!jwtClaims;
|
|
140
|
+
} catch {
|
|
136
141
|
return false;
|
|
137
142
|
}
|
|
138
|
-
return !!jwtClaims;
|
|
139
143
|
},
|
|
140
144
|
refreshAccessToken: options.refreshAccessToken
|
|
141
145
|
? options.refreshAccessToken
|
|
@@ -130,22 +130,26 @@ export const google = (options: GoogleOptions) => {
|
|
|
130
130
|
// Verify JWT integrity
|
|
131
131
|
// See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
try {
|
|
134
|
+
const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
|
|
135
|
+
if (!kid || !jwtAlg) return false;
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
const publicKey = await getGooglePublicKey(kid);
|
|
138
|
+
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
139
|
+
algorithms: [jwtAlg],
|
|
140
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
141
|
+
audience: options.clientId,
|
|
142
|
+
maxTokenAge: "1h",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
143
148
|
|
|
144
|
-
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
145
151
|
return false;
|
|
146
152
|
}
|
|
147
|
-
|
|
148
|
-
return true;
|
|
149
153
|
},
|
|
150
154
|
async getUserInfo(token) {
|
|
151
155
|
if (options.getUserInfo) {
|
package/src/types/context.ts
CHANGED
|
@@ -265,6 +265,11 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
|
|
|
265
265
|
InfoContext & {
|
|
266
266
|
options: Options;
|
|
267
267
|
trustedOrigins: string[];
|
|
268
|
+
/**
|
|
269
|
+
* Resolved list of trusted providers for account linking.
|
|
270
|
+
* Populated from "account.accountLinking.trustedProviders" (supports static array or async function).
|
|
271
|
+
*/
|
|
272
|
+
trustedProviders: string[];
|
|
268
273
|
/**
|
|
269
274
|
* Verifies whether url is a trusted origin according to the "trustedOrigins" configuration
|
|
270
275
|
* @param url The url to verify against the "trustedOrigins" configuration
|
package/src/types/index.ts
CHANGED
|
@@ -14,12 +14,14 @@ export type {
|
|
|
14
14
|
} from "./cookie";
|
|
15
15
|
export type * from "./helper";
|
|
16
16
|
export type {
|
|
17
|
+
BaseURLConfig,
|
|
17
18
|
BetterAuthAdvancedOptions,
|
|
18
19
|
BetterAuthDBOptions,
|
|
19
20
|
BetterAuthOptions,
|
|
20
21
|
BetterAuthRateLimitOptions,
|
|
21
22
|
BetterAuthRateLimitRule,
|
|
22
23
|
BetterAuthRateLimitStorage,
|
|
24
|
+
DynamicBaseURLConfig,
|
|
23
25
|
GenerateIdFn,
|
|
24
26
|
StoreIdentifierOption,
|
|
25
27
|
} from "./init-options";
|
|
@@ -46,6 +46,53 @@ export type GenerateIdFn = (options: {
|
|
|
46
46
|
size?: number | undefined;
|
|
47
47
|
}) => string | false;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for dynamic base URL resolution.
|
|
51
|
+
* Allows Better Auth to work with multiple domains (e.g., Vercel preview deployments).
|
|
52
|
+
*/
|
|
53
|
+
export type DynamicBaseURLConfig = {
|
|
54
|
+
/**
|
|
55
|
+
* List of allowed hostnames. Supports wildcard patterns.
|
|
56
|
+
*
|
|
57
|
+
* The derived host from the request will be validated against this list.
|
|
58
|
+
* Uses the same wildcard matching as `trustedOrigins`.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* allowedHosts: [
|
|
63
|
+
* "myapp.com", // Exact match
|
|
64
|
+
* "*.vercel.app", // Any Vercel preview
|
|
65
|
+
* "preview-*.myapp.com" // Pattern match
|
|
66
|
+
* ]
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
allowedHosts: string[];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fallback URL to use if the derived host doesn't match any allowed host.
|
|
73
|
+
* If not set, Better Auth will throw an error when the host doesn't match.
|
|
74
|
+
*
|
|
75
|
+
* @example "https://myapp.com"
|
|
76
|
+
*/
|
|
77
|
+
fallback?: string | undefined;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Protocol to use when constructing the URL.
|
|
81
|
+
* - `"https"`: Always use HTTPS (recommended for production)
|
|
82
|
+
* - `"http"`: Always use HTTP (for local development)
|
|
83
|
+
* - `"auto"`: Derive from `x-forwarded-proto` header or default to HTTPS
|
|
84
|
+
*
|
|
85
|
+
* @default "auto"
|
|
86
|
+
*/
|
|
87
|
+
protocol?: "http" | "https" | "auto" | undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Base URL configuration.
|
|
92
|
+
* Can be a static string or a dynamic config for multi-domain deployments.
|
|
93
|
+
*/
|
|
94
|
+
export type BaseURLConfig = string | DynamicBaseURLConfig;
|
|
95
|
+
|
|
49
96
|
export interface BetterAuthRateLimitStorage {
|
|
50
97
|
get: (key: string) => Promise<RateLimit | null | undefined>;
|
|
51
98
|
set: (
|
|
@@ -346,12 +393,27 @@ export type BetterAuthOptions = {
|
|
|
346
393
|
/**
|
|
347
394
|
* Base URL for the Better Auth. This is typically the
|
|
348
395
|
* root URL where your application server is hosted.
|
|
349
|
-
* If not explicitly set,
|
|
350
|
-
* the system will check the following environment variable:
|
|
351
396
|
*
|
|
352
|
-
*
|
|
397
|
+
* Can be configured as:
|
|
398
|
+
* - A static string: `"https://myapp.com"`
|
|
399
|
+
* - A dynamic config with allowed hosts for multi-domain deployments
|
|
400
|
+
*
|
|
401
|
+
* If not explicitly set, the system will check environment variables:
|
|
402
|
+
* `BETTER_AUTH_URL`, `NEXT_PUBLIC_BETTER_AUTH_URL`, etc.
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* ```ts
|
|
406
|
+
* // Static URL
|
|
407
|
+
* baseURL: "https://myapp.com"
|
|
408
|
+
*
|
|
409
|
+
* // Dynamic with allowed hosts (for Vercel, multi-domain, etc.)
|
|
410
|
+
* baseURL: {
|
|
411
|
+
* allowedHosts: ["myapp.com", "*.vercel.app", "preview-*.myapp.com"],
|
|
412
|
+
* fallback: "https://myapp.com"
|
|
413
|
+
* }
|
|
414
|
+
* ```
|
|
353
415
|
*/
|
|
354
|
-
baseURL?:
|
|
416
|
+
baseURL?: BaseURLConfig | undefined;
|
|
355
417
|
/**
|
|
356
418
|
* Base path for the Better Auth. This is typically
|
|
357
419
|
* the path where the
|
|
@@ -627,6 +689,20 @@ export type BetterAuthOptions = {
|
|
|
627
689
|
* @default false
|
|
628
690
|
*/
|
|
629
691
|
revokeSessionsOnPasswordReset?: boolean;
|
|
692
|
+
/**
|
|
693
|
+
* A callback function that is triggered when a user tries to sign up
|
|
694
|
+
* with an email that already exists. Useful for notifying the existing user
|
|
695
|
+
* that someone attempted to register with their email.
|
|
696
|
+
*
|
|
697
|
+
* This is only called when `requireEmailVerification: true` or `autoSignIn: false`.
|
|
698
|
+
*/
|
|
699
|
+
onExistingUserSignUp?: (
|
|
700
|
+
/**
|
|
701
|
+
* @param user the existing user from the database
|
|
702
|
+
*/
|
|
703
|
+
data: { user: User },
|
|
704
|
+
request?: Request,
|
|
705
|
+
) => Promise<void>;
|
|
630
706
|
}
|
|
631
707
|
| undefined;
|
|
632
708
|
/**
|
|
@@ -887,11 +963,43 @@ export type BetterAuthOptions = {
|
|
|
887
963
|
*/
|
|
888
964
|
disableImplicitLinking?: boolean;
|
|
889
965
|
/**
|
|
890
|
-
* List of trusted providers
|
|
966
|
+
* List of trusted providers. Can be a static array or a function
|
|
967
|
+
* that returns providers dynamically. The function is called
|
|
968
|
+
* during context init (with `request` undefined) and again
|
|
969
|
+
* on each request (with the incoming Request). It must be
|
|
970
|
+
* resilient to `request` being undefined.
|
|
971
|
+
*
|
|
972
|
+
* @example
|
|
973
|
+
* ```ts
|
|
974
|
+
* trustedProviders: ["google", "github"]
|
|
975
|
+
* ```
|
|
976
|
+
*
|
|
977
|
+
* @example
|
|
978
|
+
* ```ts
|
|
979
|
+
* trustedProviders: async (request) => {
|
|
980
|
+
* if (!request) return [];
|
|
981
|
+
* const providers = await getTrustedProvidersForTenant(request);
|
|
982
|
+
* return providers;
|
|
983
|
+
* }
|
|
984
|
+
* ```
|
|
891
985
|
*/
|
|
892
|
-
trustedProviders?:
|
|
893
|
-
|
|
894
|
-
|
|
986
|
+
trustedProviders?:
|
|
987
|
+
| Array<
|
|
988
|
+
LiteralUnion<
|
|
989
|
+
SocialProviderList[number] | "email-password",
|
|
990
|
+
string
|
|
991
|
+
>
|
|
992
|
+
>
|
|
993
|
+
| ((
|
|
994
|
+
request?: Request | undefined,
|
|
995
|
+
) => Awaitable<
|
|
996
|
+
Array<
|
|
997
|
+
LiteralUnion<
|
|
998
|
+
SocialProviderList[number] | "email-password",
|
|
999
|
+
string
|
|
1000
|
+
>
|
|
1001
|
+
>
|
|
1002
|
+
>);
|
|
895
1003
|
/**
|
|
896
1004
|
* If enabled (true), this will allow users to manually linking accounts with different email addresses than the main user.
|
|
897
1005
|
*
|