better-auth 1.6.3 → 1.7.0-beta.1

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.
Files changed (45) hide show
  1. package/dist/api/index.d.mts +4 -0
  2. package/dist/api/routes/callback.d.mts +2 -0
  3. package/dist/api/routes/callback.mjs +22 -13
  4. package/dist/client/path-to-object.d.mts +35 -1
  5. package/dist/client/plugins/index.d.mts +3 -2
  6. package/dist/client/plugins/index.mjs +2 -1
  7. package/dist/oauth2/error-codes.d.mts +20 -0
  8. package/dist/oauth2/error-codes.mjs +20 -0
  9. package/dist/package.mjs +1 -1
  10. package/dist/plugins/admin/admin.mjs +1 -1
  11. package/dist/plugins/anonymous/index.mjs +1 -1
  12. package/dist/plugins/generic-oauth/client.d.mts +6 -6
  13. package/dist/plugins/generic-oauth/client.mjs +6 -0
  14. package/dist/plugins/generic-oauth/error-codes.d.mts +1 -6
  15. package/dist/plugins/generic-oauth/error-codes.mjs +2 -7
  16. package/dist/plugins/generic-oauth/index.d.mts +9 -156
  17. package/dist/plugins/generic-oauth/index.mjs +133 -73
  18. package/dist/plugins/generic-oauth/providers/auth0.d.mts +1 -1
  19. package/dist/plugins/generic-oauth/providers/gumroad.d.mts +1 -1
  20. package/dist/plugins/generic-oauth/providers/hubspot.d.mts +1 -1
  21. package/dist/plugins/generic-oauth/providers/keycloak.d.mts +1 -1
  22. package/dist/plugins/generic-oauth/providers/microsoft-entra-id.d.mts +1 -1
  23. package/dist/plugins/generic-oauth/providers/okta.d.mts +1 -1
  24. package/dist/plugins/generic-oauth/providers/patreon.d.mts +1 -1
  25. package/dist/plugins/generic-oauth/providers/slack.d.mts +1 -1
  26. package/dist/plugins/generic-oauth/types.d.mts +25 -27
  27. package/dist/plugins/index.d.mts +3 -3
  28. package/dist/plugins/index.mjs +2 -2
  29. package/dist/plugins/jwt/client.d.mts +1 -1
  30. package/dist/plugins/jwt/index.d.mts +3 -3
  31. package/dist/plugins/jwt/index.mjs +2 -2
  32. package/dist/plugins/jwt/sign.d.mts +15 -3
  33. package/dist/plugins/jwt/sign.mjs +31 -12
  34. package/dist/plugins/jwt/types.d.mts +13 -1
  35. package/dist/plugins/jwt/utils.d.mts +1 -1
  36. package/dist/plugins/last-login-method/index.mjs +1 -1
  37. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  38. package/dist/plugins/two-factor/client.d.mts +2 -0
  39. package/dist/plugins/two-factor/error-code.d.mts +2 -0
  40. package/dist/plugins/two-factor/error-code.mjs +2 -0
  41. package/dist/plugins/two-factor/index.d.mts +19 -0
  42. package/dist/plugins/two-factor/index.mjs +48 -25
  43. package/dist/test-utils/test-instance.d.mts +12 -0
  44. package/package.json +8 -8
  45. package/dist/plugins/generic-oauth/routes.mjs +0 -407
@@ -216,6 +216,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
216
216
  error_description: zod.ZodOptional<zod.ZodString>;
217
217
  state: zod.ZodOptional<zod.ZodString>;
218
218
  user: zod.ZodOptional<zod.ZodString>;
219
+ iss: zod.ZodOptional<zod.ZodString>;
219
220
  }, zod_v4_core0.$strip>>;
220
221
  query: zod.ZodOptional<zod.ZodObject<{
221
222
  code: zod.ZodOptional<zod.ZodString>;
@@ -224,6 +225,7 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
224
225
  error_description: zod.ZodOptional<zod.ZodString>;
225
226
  state: zod.ZodOptional<zod.ZodString>;
226
227
  user: zod.ZodOptional<zod.ZodString>;
228
+ iss: zod.ZodOptional<zod.ZodString>;
227
229
  }, zod_v4_core0.$strip>>;
228
230
  metadata: {
229
231
  allowedMediaTypes: string[];
@@ -2205,6 +2207,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2205
2207
  error_description: zod.ZodOptional<zod.ZodString>;
2206
2208
  state: zod.ZodOptional<zod.ZodString>;
2207
2209
  user: zod.ZodOptional<zod.ZodString>;
2210
+ iss: zod.ZodOptional<zod.ZodString>;
2208
2211
  }, zod_v4_core0.$strip>>;
2209
2212
  query: zod.ZodOptional<zod.ZodObject<{
2210
2213
  code: zod.ZodOptional<zod.ZodString>;
@@ -2213,6 +2216,7 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
2213
2216
  error_description: zod.ZodOptional<zod.ZodString>;
2214
2217
  state: zod.ZodOptional<zod.ZodString>;
2215
2218
  user: zod.ZodOptional<zod.ZodString>;
2219
+ iss: zod.ZodOptional<zod.ZodString>;
2216
2220
  }, zod_v4_core0.$strip>>;
2217
2221
  metadata: {
2218
2222
  allowedMediaTypes: string[];
@@ -12,6 +12,7 @@ declare const callbackOAuth: better_call0.StrictEndpoint<"/callback/:id", {
12
12
  error_description: z.ZodOptional<z.ZodString>;
13
13
  state: z.ZodOptional<z.ZodString>;
14
14
  user: z.ZodOptional<z.ZodString>;
15
+ iss: z.ZodOptional<z.ZodString>;
15
16
  }, z.core.$strip>>;
16
17
  query: z.ZodOptional<z.ZodObject<{
17
18
  code: z.ZodOptional<z.ZodString>;
@@ -20,6 +21,7 @@ declare const callbackOAuth: better_call0.StrictEndpoint<"/callback/:id", {
20
21
  error_description: z.ZodOptional<z.ZodString>;
21
22
  state: z.ZodOptional<z.ZodString>;
22
23
  user: z.ZodOptional<z.ZodString>;
24
+ iss: z.ZodOptional<z.ZodString>;
23
25
  }, z.core.$strip>>;
24
26
  metadata: {
25
27
  allowedMediaTypes: string[];
@@ -2,6 +2,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
2
2
  import { getAwaitableValue } from "../../context/helpers.mjs";
3
3
  import { parseState } from "../../oauth2/state.mjs";
4
4
  import { setTokenUtil } from "../../oauth2/utils.mjs";
5
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
5
6
  import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
6
7
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
7
8
  import { safeJSONParse } from "@better-auth/core/utils/json";
@@ -14,7 +15,8 @@ const schema = z.object({
14
15
  device_id: z.string().optional(),
15
16
  error_description: z.string().optional(),
16
17
  state: z.string().optional(),
17
- user: z.string().optional()
18
+ user: z.string().optional(),
19
+ iss: z.string().optional()
18
20
  });
19
21
  const callbackOAuth = createAuthEndpoint("/callback/:id", {
20
22
  method: ["GET", "POST"],
@@ -48,7 +50,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
48
50
  c.context.logger.error("INVALID_CALLBACK_REQUEST", e);
49
51
  throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`);
50
52
  }
51
- const { code, error, state, error_description, device_id, user: userData } = queryOrBody;
53
+ const { code, error, state, error_description, device_id, user: userData, iss } = queryOrBody;
52
54
  if (!state) {
53
55
  c.context.logger.error("State not found", error);
54
56
  const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}state=state_not_found`;
@@ -65,12 +67,19 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
65
67
  if (error) redirectOnError(error, error_description);
66
68
  if (!code) {
67
69
  c.context.logger.error("Code not found");
68
- throw redirectOnError("no_code");
70
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NO_CODE);
69
71
  }
70
72
  const provider = await getAwaitableValue(c.context.socialProviders, { value: c.params.id });
71
73
  if (!provider) {
72
74
  c.context.logger.error("Oauth provider with id", c.params.id, "not found");
73
- throw redirectOnError("oauth_provider_not_found");
75
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.PROVIDER_NOT_FOUND);
76
+ }
77
+ if (iss && provider.issuer && iss !== provider.issuer) {
78
+ c.context.logger.error("OAuth issuer mismatch", {
79
+ expected: provider.issuer,
80
+ received: iss
81
+ });
82
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ISSUER_MISMATCH);
74
83
  }
75
84
  let tokens;
76
85
  try {
@@ -82,9 +91,9 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
82
91
  });
83
92
  } catch (e) {
84
93
  c.context.logger.error("", e);
85
- throw redirectOnError("invalid_code");
94
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE);
86
95
  }
87
- if (!tokens) throw redirectOnError("invalid_code");
96
+ if (!tokens) throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE);
88
97
  const parsedUserData = userData ? safeJSONParse(userData) : null;
89
98
  const userInfo = await provider.getUserInfo({
90
99
  ...tokens,
@@ -92,21 +101,21 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
92
101
  }).then((res) => res?.user);
93
102
  if (!userInfo) {
94
103
  c.context.logger.error("Unable to get user info");
95
- return redirectOnError("unable_to_get_user_info");
104
+ return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_GET_USER_INFO);
96
105
  }
97
106
  if (!callbackURL) {
98
107
  c.context.logger.error("No callback URL found");
99
- throw redirectOnError("no_callback_url");
108
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NO_CALLBACK_URL);
100
109
  }
101
110
  if (link) {
102
111
  if (!c.context.trustedProviders.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
103
112
  c.context.logger.error("Unable to link account - untrusted provider");
104
- return redirectOnError("unable_to_link_account");
113
+ return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
105
114
  }
106
- if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError("email_doesn't_match");
115
+ if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_DOES_NOT_MATCH);
107
116
  const existingAccount = await c.context.internalAdapter.findAccountByProviderId(String(userInfo.id), provider.id);
108
117
  if (existingAccount) {
109
- if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError("account_already_linked_to_different_user");
118
+ if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
110
119
  const updateData = Object.fromEntries(Object.entries({
111
120
  accessToken: await setTokenUtil(tokens.accessToken, c.context),
112
121
  refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
@@ -124,7 +133,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
124
133
  accessToken: await setTokenUtil(tokens.accessToken, c.context),
125
134
  refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
126
135
  scope: tokens.scopes?.join(",")
127
- })) return redirectOnError("unable_to_link_account");
136
+ })) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
128
137
  let toRedirectTo;
129
138
  try {
130
139
  toRedirectTo = callbackURL.toString();
@@ -135,7 +144,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
135
144
  }
136
145
  if (!userInfo.email) {
137
146
  c.context.logger.error("Provider did not return email. This could be due to misconfiguration in the provider settings.");
138
- return redirectOnError("email_not_found");
147
+ return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_FOUND);
139
148
  }
140
149
  const accountData = {
141
150
  providerId: provider.id,
@@ -1,10 +1,41 @@
1
1
  import { HasRequiredKeys, IsAny, Prettify as Prettify$1, UnionToIntersection } from "../types/helper.mjs";
2
2
  import { InferAdditionalFromClient, InferSessionFromClient, InferUserFromClient } from "./types.mjs";
3
3
  import { BetterAuthClientOptions, ClientFetchOption } from "@better-auth/core";
4
+ import { SocialProviderList } from "@better-auth/core/social-providers";
4
5
  import { Endpoint, InputContext, StandardSchemaV1 } from "better-call";
5
6
  import { BetterFetchResponse } from "@better-fetch/fetch";
6
7
 
7
8
  //#region src/client/path-to-object.d.ts
9
+ /**
10
+ * Extract generic OAuth provider IDs from the client options.
11
+ * Supports both `$InferAuth` (server type bridge) and client plugins
12
+ * with `$InferServerPlugin`.
13
+ */
14
+ type InferGenericOAuthProviderIds<O extends BetterAuthClientOptions> = (O extends {
15
+ $InferAuth: {
16
+ options: {
17
+ plugins: Array<infer P>;
18
+ };
19
+ };
20
+ } ? P extends {
21
+ id: "generic-oauth";
22
+ options: {
23
+ config: Array<{
24
+ providerId: infer ID;
25
+ }>;
26
+ };
27
+ } ? ID & string : never : never) | (O extends {
28
+ plugins: Array<infer P>;
29
+ } ? P extends {
30
+ $InferServerPlugin: {
31
+ id: "generic-oauth";
32
+ options: {
33
+ config: Array<{
34
+ providerId: infer ID;
35
+ }>;
36
+ };
37
+ };
38
+ } ? ID & string : never : never);
8
39
  type KeepNullishFromOriginal<Original, Replaced> = Replaced | (undefined extends Original ? undefined : never) | (null extends Original ? null : never);
9
40
  type ReplaceTopLevelField<Data, Field extends "user" | "session", Replaced> = Data extends object ? Field extends keyof Data ? Omit<Data, Field> & { [K in Field]: KeepNullishFromOriginal<Data[K], Replaced> } : Data : Data;
10
41
  type ReplaceAuthUserAndSession<Data, ClientOpts extends BetterAuthClientOptions> = ReplaceTopLevelField<ReplaceTopLevelField<Data, "user", InferUserFromClient<ClientOpts>>, "session", InferSessionFromClient<ClientOpts>>;
@@ -51,7 +82,10 @@ type InferRoute<API, COpts extends BetterAuthClientOptions> = API extends Record
51
82
  scope: "http";
52
83
  } | {
53
84
  scope: "server";
54
- } ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data: HasRequiredKeys<InferCtx<C, FetchOptions>> extends true ? [Prettify$1<T["path"] extends `/sign-up/email` ? InferSignUpEmailCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>, FetchOptions?] : [Prettify$1<T["path"] extends `/update-user` ? InferUserUpdateCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>?, FetchOptions?]) => Promise<BetterFetchResponse<T["options"]["metadata"] extends {
85
+ } ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data: HasRequiredKeys<InferCtx<C, FetchOptions>> extends true ? [Prettify$1<T["path"] extends `/sign-up/email` ? InferSignUpEmailCtx<COpts, FetchOptions> : T["path"] extends `/sign-in/social` ? Omit<InferCtx<C, FetchOptions>, "provider"> & {
86
+ provider: SocialProviderList[number] | InferGenericOAuthProviderIds<COpts> | (string & {});
87
+ fetchOptions?: FetchOptions | undefined;
88
+ } : InferCtx<C, FetchOptions>>, FetchOptions?] : [Prettify$1<T["path"] extends `/update-user` ? InferUserUpdateCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>?, FetchOptions?]) => Promise<BetterFetchResponse<T["options"]["metadata"] extends {
55
89
  CUSTOM_SESSION: boolean;
56
90
  } ? MergeCustomSessionWithInferred<NonNullable<Awaited<R>>, COpts> : T["path"] extends "/get-session" ? {
57
91
  user: InferUserFromClient<COpts>;
@@ -14,7 +14,7 @@ import { OktaOptions, okta } from "../../plugins/generic-oauth/providers/okta.mj
14
14
  import { PatreonOptions, patreon } from "../../plugins/generic-oauth/providers/patreon.mjs";
15
15
  import { SlackOptions, slack } from "../../plugins/generic-oauth/providers/slack.mjs";
16
16
  import { BaseOAuthProviderOptions } from "../../plugins/generic-oauth/index.mjs";
17
- import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions } from "../../plugins/jwt/types.mjs";
17
+ import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey } from "../../plugins/jwt/types.mjs";
18
18
  import { AuthorizationQuery, Client, CodeVerificationValue, OAuthAccessToken, OIDCMetadata, OIDCOptions, TokenBody } from "../../plugins/oidc-provider/types.mjs";
19
19
  import { MULTI_SESSION_ERROR_CODES } from "../../plugins/multi-session/error-codes.mjs";
20
20
  import { MultiSessionConfig } from "../../plugins/multi-session/index.mjs";
@@ -37,6 +37,7 @@ import { customSessionClient } from "../../plugins/custom-session/client.mjs";
37
37
  import { deviceAuthorizationClient } from "../../plugins/device-authorization/client.mjs";
38
38
  import { EMAIL_OTP_ERROR_CODES } from "../../plugins/email-otp/error-codes.mjs";
39
39
  import { emailOTPClient } from "../../plugins/email-otp/client.mjs";
40
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
40
41
  import { GENERIC_OAUTH_ERROR_CODES } from "../../plugins/generic-oauth/error-codes.mjs";
41
42
  import { genericOAuthClient } from "../../plugins/generic-oauth/client.mjs";
42
43
  import { jwtClient } from "../../plugins/jwt/client.mjs";
@@ -52,4 +53,4 @@ import { phoneNumberClient } from "../../plugins/phone-number/client.mjs";
52
53
  import { siweClient } from "../../plugins/siwe/client.mjs";
53
54
  import { usernameClient } from "../../plugins/username/client.mjs";
54
55
  import { InferServerPlugin } from "./infer-plugin.mjs";
55
- export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, RequiredKeysOf, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
56
+ export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_CALLBACK_ERROR_CODES, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, RequiredKeysOf, ResolvedSigningKey, SessionWithImpersonatedBy, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
@@ -1,3 +1,4 @@
1
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
1
2
  import { inferAdditionalFields } from "../../plugins/additional-fields/client.mjs";
2
3
  import { ADMIN_ERROR_CODES } from "../../plugins/admin/error-codes.mjs";
3
4
  import { adminClient } from "../../plugins/admin/client.mjs";
@@ -27,4 +28,4 @@ import { twoFactorClient } from "../../plugins/two-factor/client.mjs";
27
28
  import { USERNAME_ERROR_CODES } from "../../plugins/username/error-codes.mjs";
28
29
  import { usernameClient } from "../../plugins/username/client.mjs";
29
30
  import { InferServerPlugin } from "./infer-plugin.mjs";
30
- export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, EMAIL_OTP_ERROR_CODES, GENERIC_OAUTH_ERROR_CODES, InferServerPlugin, MULTI_SESSION_ERROR_CODES, ORGANIZATION_ERROR_CODES, PHONE_NUMBER_ERROR_CODES, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, adminClient, anonymousClient, clientSideHasPermission, customSessionClient, deviceAuthorizationClient, emailOTPClient, genericOAuthClient, inferAdditionalFields, inferOrgAdditionalFields, jwtClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, oidcClient, oneTapClient, oneTimeTokenClient, organizationClient, phoneNumberClient, siweClient, twoFactorClient, usernameClient };
31
+ export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, EMAIL_OTP_ERROR_CODES, GENERIC_OAUTH_ERROR_CODES, InferServerPlugin, MULTI_SESSION_ERROR_CODES, OAUTH_CALLBACK_ERROR_CODES, ORGANIZATION_ERROR_CODES, PHONE_NUMBER_ERROR_CODES, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, adminClient, anonymousClient, clientSideHasPermission, customSessionClient, deviceAuthorizationClient, emailOTPClient, genericOAuthClient, inferAdditionalFields, inferOrgAdditionalFields, jwtClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, oidcClient, oneTapClient, oneTimeTokenClient, organizationClient, phoneNumberClient, siweClient, twoFactorClient, usernameClient };
@@ -0,0 +1,20 @@
1
+ //#region src/oauth2/error-codes.d.ts
2
+ /**
3
+ * Error codes used in OAuth callback redirects (`?error=<code>`).
4
+ * These are URL-safe strings, not API error objects.
5
+ */
6
+ declare const OAUTH_CALLBACK_ERROR_CODES: {
7
+ readonly NO_CODE: "no_code";
8
+ readonly PROVIDER_NOT_FOUND: "oauth_provider_not_found";
9
+ readonly ISSUER_MISSING: "issuer_missing";
10
+ readonly ISSUER_MISMATCH: "issuer_mismatch";
11
+ readonly INVALID_CODE: "invalid_code";
12
+ readonly UNABLE_TO_GET_USER_INFO: "unable_to_get_user_info";
13
+ readonly NO_CALLBACK_URL: "no_callback_url";
14
+ readonly UNABLE_TO_LINK_ACCOUNT: "unable_to_link_account";
15
+ readonly EMAIL_DOES_NOT_MATCH: "email_does_not_match";
16
+ readonly ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER: "account_already_linked_to_different_user";
17
+ readonly EMAIL_NOT_FOUND: "email_not_found";
18
+ };
19
+ //#endregion
20
+ export { OAUTH_CALLBACK_ERROR_CODES };
@@ -0,0 +1,20 @@
1
+ //#region src/oauth2/error-codes.ts
2
+ /**
3
+ * Error codes used in OAuth callback redirects (`?error=<code>`).
4
+ * These are URL-safe strings, not API error objects.
5
+ */
6
+ const OAUTH_CALLBACK_ERROR_CODES = {
7
+ NO_CODE: "no_code",
8
+ PROVIDER_NOT_FOUND: "oauth_provider_not_found",
9
+ ISSUER_MISSING: "issuer_missing",
10
+ ISSUER_MISMATCH: "issuer_mismatch",
11
+ INVALID_CODE: "invalid_code",
12
+ UNABLE_TO_GET_USER_INFO: "unable_to_get_user_info",
13
+ NO_CALLBACK_URL: "no_callback_url",
14
+ UNABLE_TO_LINK_ACCOUNT: "unable_to_link_account",
15
+ EMAIL_DOES_NOT_MATCH: "email_does_not_match",
16
+ ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER: "account_already_linked_to_different_user",
17
+ EMAIL_NOT_FOUND: "email_not_found"
18
+ };
19
+ //#endregion
20
+ export { OAUTH_CALLBACK_ERROR_CODES };
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.6.3";
2
+ var version = "1.7.0-beta.1";
3
3
  //#endregion
4
4
  export { version };
@@ -42,7 +42,7 @@ const admin = (options) => {
42
42
  });
43
43
  return;
44
44
  }
45
- if (ctx && (ctx.path.startsWith("/callback") || ctx.path.startsWith("/oauth2/callback"))) {
45
+ if (ctx.path.startsWith("/callback")) {
46
46
  const redirectURI = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
47
47
  throw ctx.redirect(`${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}`);
48
48
  }
@@ -113,7 +113,7 @@ const anonymous = (options) => {
113
113
  },
114
114
  hooks: { after: [{
115
115
  matcher(ctx) {
116
- return ctx.path?.startsWith("/sign-in") || ctx.path?.startsWith("/sign-up") || ctx.path?.startsWith("/callback") || ctx.path?.startsWith("/oauth2/callback") || ctx.path?.startsWith("/magic-link/verify") || ctx.path?.startsWith("/email-otp/verify-email") || ctx.path?.startsWith("/one-tap/callback") || ctx.path?.startsWith("/passkey/verify-authentication") || ctx.path?.startsWith("/phone-number/verify") || false;
116
+ return ctx.path?.startsWith("/sign-in") || ctx.path?.startsWith("/sign-up") || ctx.path?.startsWith("/callback") || ctx.path?.startsWith("/magic-link/verify") || ctx.path?.startsWith("/email-otp/verify-email") || ctx.path?.startsWith("/one-tap/callback") || ctx.path?.startsWith("/passkey/verify-authentication") || ctx.path?.startsWith("/phone-number/verify") || false;
117
117
  },
118
118
  handler: createAuthMiddleware(async (ctx) => {
119
119
  const setCookie = ctx.context.responseHeaders?.get("set-cookie");
@@ -9,10 +9,16 @@ import { OktaOptions, okta } from "./providers/okta.mjs";
9
9
  import { PatreonOptions, patreon } from "./providers/patreon.mjs";
10
10
  import { SlackOptions, slack } from "./providers/slack.mjs";
11
11
  import { BaseOAuthProviderOptions, genericOAuth } from "./index.mjs";
12
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
12
13
  import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes.mjs";
13
14
  import * as _better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
14
15
 
15
16
  //#region src/plugins/generic-oauth/client.d.ts
17
+ /**
18
+ * @deprecated No longer needed. Generic OAuth providers now use the standard
19
+ * `signIn.social` flow and require no client plugin. Remove this from your
20
+ * client plugin list.
21
+ */
16
22
  declare const genericOAuthClient: () => {
17
23
  id: "generic-oauth-client";
18
24
  version: string;
@@ -20,12 +26,6 @@ declare const genericOAuthClient: () => {
20
26
  $ERROR_CODES: {
21
27
  INVALID_OAUTH_CONFIGURATION: _better_auth_core_utils_error_codes0.RawError<"INVALID_OAUTH_CONFIGURATION">;
22
28
  TOKEN_URL_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"TOKEN_URL_NOT_FOUND">;
23
- PROVIDER_CONFIG_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"PROVIDER_CONFIG_NOT_FOUND">;
24
- PROVIDER_ID_REQUIRED: _better_auth_core_utils_error_codes0.RawError<"PROVIDER_ID_REQUIRED">;
25
- INVALID_OAUTH_CONFIG: _better_auth_core_utils_error_codes0.RawError<"INVALID_OAUTH_CONFIG">;
26
- SESSION_REQUIRED: _better_auth_core_utils_error_codes0.RawError<"SESSION_REQUIRED">;
27
- ISSUER_MISMATCH: _better_auth_core_utils_error_codes0.RawError<"ISSUER_MISMATCH">;
28
- ISSUER_MISSING: _better_auth_core_utils_error_codes0.RawError<"ISSUER_MISSING">;
29
29
  };
30
30
  };
31
31
  //#endregion
@@ -1,6 +1,12 @@
1
+ import "../../oauth2/error-codes.mjs";
1
2
  import { PACKAGE_VERSION } from "../../version.mjs";
2
3
  import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes.mjs";
3
4
  //#region src/plugins/generic-oauth/client.ts
5
+ /**
6
+ * @deprecated No longer needed. Generic OAuth providers now use the standard
7
+ * `signIn.social` flow and require no client plugin. Remove this from your
8
+ * client plugin list.
9
+ */
4
10
  const genericOAuthClient = () => {
5
11
  return {
6
12
  id: "generic-oauth-client",
@@ -1,15 +1,10 @@
1
+ import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes.mjs";
1
2
  import * as _better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
2
3
 
3
4
  //#region src/plugins/generic-oauth/error-codes.d.ts
4
5
  declare const GENERIC_OAUTH_ERROR_CODES: {
5
6
  INVALID_OAUTH_CONFIGURATION: _better_auth_core_utils_error_codes0.RawError<"INVALID_OAUTH_CONFIGURATION">;
6
7
  TOKEN_URL_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"TOKEN_URL_NOT_FOUND">;
7
- PROVIDER_CONFIG_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"PROVIDER_CONFIG_NOT_FOUND">;
8
- PROVIDER_ID_REQUIRED: _better_auth_core_utils_error_codes0.RawError<"PROVIDER_ID_REQUIRED">;
9
- INVALID_OAUTH_CONFIG: _better_auth_core_utils_error_codes0.RawError<"INVALID_OAUTH_CONFIG">;
10
- SESSION_REQUIRED: _better_auth_core_utils_error_codes0.RawError<"SESSION_REQUIRED">;
11
- ISSUER_MISMATCH: _better_auth_core_utils_error_codes0.RawError<"ISSUER_MISMATCH">;
12
- ISSUER_MISSING: _better_auth_core_utils_error_codes0.RawError<"ISSUER_MISSING">;
13
8
  };
14
9
  //#endregion
15
10
  export { GENERIC_OAUTH_ERROR_CODES };
@@ -1,14 +1,9 @@
1
+ import "../../oauth2/error-codes.mjs";
1
2
  import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
2
3
  //#region src/plugins/generic-oauth/error-codes.ts
3
4
  const GENERIC_OAUTH_ERROR_CODES = defineErrorCodes({
4
5
  INVALID_OAUTH_CONFIGURATION: "Invalid OAuth configuration",
5
- TOKEN_URL_NOT_FOUND: "Invalid OAuth configuration. Token URL not found.",
6
- PROVIDER_CONFIG_NOT_FOUND: "No config found for provider",
7
- PROVIDER_ID_REQUIRED: "Provider ID is required",
8
- INVALID_OAUTH_CONFIG: "Invalid OAuth configuration.",
9
- SESSION_REQUIRED: "Session is required",
10
- ISSUER_MISMATCH: "OAuth issuer mismatch. The authorization server issuer does not match the expected value (RFC 9207).",
11
- ISSUER_MISSING: "OAuth issuer parameter missing. The authorization server did not include the required iss parameter (RFC 9207)."
6
+ TOKEN_URL_NOT_FOUND: "Invalid OAuth configuration. Token URL not found."
12
7
  });
13
8
  //#endregion
14
9
  export { GENERIC_OAUTH_ERROR_CODES };
@@ -12,9 +12,6 @@ import { AuthContext } from "@better-auth/core";
12
12
  import * as _better_auth_core_oauth20 from "@better-auth/core/oauth2";
13
13
  import { OAuthProvider } from "@better-auth/core/oauth2";
14
14
  import * as _better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
15
- import * as better_call0 from "better-call";
16
- import * as zod from "zod";
17
- import * as zod_v4_core0 from "zod/v4/core";
18
15
 
19
16
  //#region src/plugins/generic-oauth/index.d.ts
20
17
  declare module "@better-auth/core" {
@@ -32,168 +29,24 @@ type BaseOAuthProviderOptions = Omit<Pick<GenericOAuthConfig, "clientId" | "clie
32
29
  /** OAuth client secret (required for provider options) */clientSecret: string;
33
30
  };
34
31
  /**
35
- * A generic OAuth plugin that can be used to add OAuth support to any provider
32
+ * A generic OAuth plugin that registers any OAuth/OIDC provider
33
+ * as a first-class social provider.
34
+ *
35
+ * Providers are used through the standard `signIn.social` and
36
+ * `callback/:id` core endpoints — no plugin-specific endpoints needed.
36
37
  */
37
- declare const genericOAuth: (options: GenericOAuthOptions) => {
38
+ declare const genericOAuth: <const ID extends string>(options: GenericOAuthOptions<ID>) => {
38
39
  id: "generic-oauth";
39
40
  version: string;
40
- init: (ctx: AuthContext) => {
41
+ init: (ctx: AuthContext) => Promise<{
41
42
  context: {
42
43
  socialProviders: OAuthProvider<Record<string, any>, Partial<_better_auth_core_oauth20.ProviderOptions<any>>>[];
43
44
  };
44
- };
45
- endpoints: {
46
- signInWithOAuth2: better_call0.StrictEndpoint<"/sign-in/oauth2", {
47
- method: "POST";
48
- body: zod.ZodObject<{
49
- providerId: zod.ZodString;
50
- callbackURL: zod.ZodOptional<zod.ZodString>;
51
- errorCallbackURL: zod.ZodOptional<zod.ZodString>;
52
- newUserCallbackURL: zod.ZodOptional<zod.ZodString>;
53
- disableRedirect: zod.ZodOptional<zod.ZodBoolean>;
54
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
55
- requestSignUp: zod.ZodOptional<zod.ZodBoolean>;
56
- additionalData: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
57
- }, zod_v4_core0.$strip>;
58
- metadata: {
59
- openapi: {
60
- description: string;
61
- responses: {
62
- 200: {
63
- description: string;
64
- content: {
65
- "application/json": {
66
- schema: {
67
- type: "object";
68
- properties: {
69
- url: {
70
- type: string;
71
- };
72
- redirect: {
73
- type: string;
74
- };
75
- };
76
- };
77
- };
78
- };
79
- };
80
- };
81
- };
82
- };
83
- }, {
84
- url: string;
85
- redirect: boolean;
86
- }>;
87
- oAuth2Callback: better_call0.StrictEndpoint<"/oauth2/callback/:providerId", {
88
- method: "GET";
89
- query: zod.ZodObject<{
90
- code: zod.ZodOptional<zod.ZodString>;
91
- error: zod.ZodOptional<zod.ZodString>;
92
- error_description: zod.ZodOptional<zod.ZodString>;
93
- state: zod.ZodOptional<zod.ZodString>;
94
- iss: zod.ZodOptional<zod.ZodString>;
95
- }, zod_v4_core0.$strip>;
96
- metadata: {
97
- allowedMediaTypes: string[];
98
- openapi: {
99
- description: string;
100
- responses: {
101
- 200: {
102
- description: string;
103
- content: {
104
- "application/json": {
105
- schema: {
106
- type: "object";
107
- properties: {
108
- url: {
109
- type: string;
110
- };
111
- };
112
- };
113
- };
114
- };
115
- };
116
- };
117
- };
118
- scope: "server";
119
- };
120
- }, void>;
121
- oAuth2LinkAccount: better_call0.StrictEndpoint<"/oauth2/link", {
122
- method: "POST";
123
- body: zod.ZodObject<{
124
- providerId: zod.ZodString;
125
- callbackURL: zod.ZodString;
126
- scopes: zod.ZodOptional<zod.ZodArray<zod.ZodString>>;
127
- errorCallbackURL: zod.ZodOptional<zod.ZodString>;
128
- }, zod_v4_core0.$strip>;
129
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
130
- session: {
131
- session: Record<string, any> & {
132
- id: string;
133
- createdAt: Date;
134
- updatedAt: Date;
135
- userId: string;
136
- expiresAt: Date;
137
- token: string;
138
- ipAddress?: string | null | undefined;
139
- userAgent?: string | null | undefined;
140
- };
141
- user: Record<string, any> & {
142
- id: string;
143
- createdAt: Date;
144
- updatedAt: Date;
145
- email: string;
146
- emailVerified: boolean;
147
- name: string;
148
- image?: string | null | undefined;
149
- };
150
- };
151
- }>)[];
152
- metadata: {
153
- openapi: {
154
- description: string;
155
- responses: {
156
- "200": {
157
- description: string;
158
- content: {
159
- "application/json": {
160
- schema: {
161
- type: "object";
162
- properties: {
163
- url: {
164
- type: string;
165
- format: string;
166
- description: string;
167
- };
168
- redirect: {
169
- type: string;
170
- description: string;
171
- enum: boolean[];
172
- };
173
- };
174
- required: string[];
175
- };
176
- };
177
- };
178
- };
179
- };
180
- };
181
- };
182
- }, {
183
- url: string;
184
- redirect: boolean;
185
- }>;
186
- };
187
- options: GenericOAuthOptions;
45
+ }>;
46
+ options: GenericOAuthOptions<ID>;
188
47
  $ERROR_CODES: {
189
48
  INVALID_OAUTH_CONFIGURATION: _better_auth_core_utils_error_codes0.RawError<"INVALID_OAUTH_CONFIGURATION">;
190
49
  TOKEN_URL_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"TOKEN_URL_NOT_FOUND">;
191
- PROVIDER_CONFIG_NOT_FOUND: _better_auth_core_utils_error_codes0.RawError<"PROVIDER_CONFIG_NOT_FOUND">;
192
- PROVIDER_ID_REQUIRED: _better_auth_core_utils_error_codes0.RawError<"PROVIDER_ID_REQUIRED">;
193
- INVALID_OAUTH_CONFIG: _better_auth_core_utils_error_codes0.RawError<"INVALID_OAUTH_CONFIG">;
194
- SESSION_REQUIRED: _better_auth_core_utils_error_codes0.RawError<"SESSION_REQUIRED">;
195
- ISSUER_MISMATCH: _better_auth_core_utils_error_codes0.RawError<"ISSUER_MISMATCH">;
196
- ISSUER_MISSING: _better_auth_core_utils_error_codes0.RawError<"ISSUER_MISSING">;
197
50
  };
198
51
  };
199
52
  //#endregion