better-auth 1.7.0-beta.7 → 1.7.0-beta.9
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 +8 -26
- package/dist/api/routes/account.d.mts +4 -11
- package/dist/api/routes/account.mjs +81 -64
- package/dist/api/routes/callback.mjs +35 -30
- package/dist/api/routes/sign-in.d.mts +0 -2
- package/dist/api/routes/sign-in.mjs +13 -21
- package/dist/cookies/session-store.d.mts +1 -1
- package/dist/db/get-migration.mjs +1 -34
- package/dist/db/internal-adapter.mjs +20 -0
- package/dist/db/schema.d.mts +1 -1
- package/dist/oauth2/index.d.mts +3 -5
- package/dist/oauth2/index.mjs +3 -5
- package/dist/oauth2/link-account.d.mts +74 -0
- package/dist/oauth2/link-account.mjs +222 -0
- package/dist/oauth2/state.d.mts +0 -11
- package/dist/oauth2/state.mjs +1 -2
- package/dist/oauth2/utils.d.mts +11 -0
- package/dist/oauth2/{token-encryption.mjs → utils.mjs} +6 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/generic-oauth/index.d.mts +2 -2
- package/dist/plugins/generic-oauth/index.mjs +0 -1
- package/dist/plugins/oauth-popup/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +6 -20
- package/dist/plugins/one-tap/index.mjs +6 -10
- package/dist/state.d.mts +1 -10
- package/dist/state.mjs +1 -2
- package/dist/test-utils/http-test-instance.d.mts +0 -20
- package/package.json +8 -8
- package/dist/oauth2/persist-account.d.mts +0 -80
- package/dist/oauth2/persist-account.mjs +0 -84
- package/dist/oauth2/resolve-account.d.mts +0 -126
- package/dist/oauth2/resolve-account.mjs +0 -128
- package/dist/oauth2/sign-in-with-oauth-identity.d.mts +0 -83
- package/dist/oauth2/sign-in-with-oauth-identity.mjs +0 -133
- package/dist/oauth2/token-encryption.d.mts +0 -7
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { formCsrfMiddleware } from "../middlewares/origin-check.mjs";
|
|
2
2
|
import { parseUserOutput } from "../../db/schema.mjs";
|
|
3
|
-
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
3
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
4
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
6
5
|
import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
6
|
+
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
7
|
+
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
7
8
|
import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
|
|
8
|
-
import { signInWithOAuthIdentity } from "../../oauth2/sign-in-with-oauth-identity.mjs";
|
|
9
9
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
10
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
11
11
|
import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
@@ -25,7 +25,6 @@ const socialSignInBodySchema = z.object({
|
|
|
25
25
|
accessToken: z.string().meta({ description: "Access token from the provider" }).optional(),
|
|
26
26
|
refreshToken: z.string().meta({ description: "Refresh token from the provider" }).optional(),
|
|
27
27
|
expiresAt: z.number().meta({ description: "Expiry date of the token" }).optional(),
|
|
28
|
-
scopes: z.array(z.string()).meta({ description: "The scopes granted alongside the id token" }).optional(),
|
|
29
28
|
user: z.object({
|
|
30
29
|
name: z.object({
|
|
31
30
|
firstName: z.string().optional(),
|
|
@@ -101,7 +100,7 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
101
100
|
c.context.logger.error(missingEmailLogMessage(c.body.provider, { source: "id_token" }), { provider: c.body.provider });
|
|
102
101
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND);
|
|
103
102
|
}
|
|
104
|
-
const data = await
|
|
103
|
+
const data = await handleOAuthUserInfo(c, {
|
|
105
104
|
userInfo: {
|
|
106
105
|
...userInfo.user,
|
|
107
106
|
email: userInfo.user.email,
|
|
@@ -110,12 +109,10 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
110
109
|
image: userInfo.user.image,
|
|
111
110
|
emailVerified: userInfo.user.emailVerified || false
|
|
112
111
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
accessToken: c.body.idToken.accessToken,
|
|
118
|
-
refreshToken: c.body.idToken.refreshToken
|
|
112
|
+
account: {
|
|
113
|
+
providerId: provider.id,
|
|
114
|
+
accountId: String(userInfo.user.id),
|
|
115
|
+
accessToken: c.body.idToken.accessToken
|
|
119
116
|
},
|
|
120
117
|
callbackURL: c.body.callbackURL,
|
|
121
118
|
disableSignUp: provider.disableImplicitSignUp && !c.body.requestSignUp || provider.disableSignUp,
|
|
@@ -142,25 +139,20 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
142
139
|
user: parseUserOutput(c.context.options, data.data.user)
|
|
143
140
|
});
|
|
144
141
|
}
|
|
145
|
-
const state = generateRandomString(32);
|
|
146
|
-
const codeVerifier = generateRandomString(128);
|
|
147
142
|
const idTokenNonce = generateIdTokenNonce(provider);
|
|
148
|
-
const {
|
|
143
|
+
const { codeVerifier, state } = await generateState(c, {
|
|
144
|
+
additionalData: c.body.additionalData,
|
|
145
|
+
idTokenNonce
|
|
146
|
+
});
|
|
147
|
+
const url = await provider.createAuthorizationURL({
|
|
149
148
|
state,
|
|
150
149
|
codeVerifier,
|
|
151
150
|
idTokenNonce,
|
|
152
|
-
redirectURI: `${c.context.baseURL}${provider
|
|
151
|
+
redirectURI: `${c.context.baseURL}${getOAuthCallbackPath(provider)}`,
|
|
153
152
|
scopes: c.body.scopes,
|
|
154
153
|
loginHint: c.body.loginHint,
|
|
155
154
|
additionalParams: c.body.additionalParams
|
|
156
155
|
});
|
|
157
|
-
await generateState(c, {
|
|
158
|
-
additionalData: c.body.additionalData,
|
|
159
|
-
requestedScopes,
|
|
160
|
-
state,
|
|
161
|
-
codeVerifier,
|
|
162
|
-
idTokenNonce
|
|
163
|
-
});
|
|
164
156
|
if (!c.body.disableRedirect) c.setHeader("Location", url.toString());
|
|
165
157
|
return c.json({
|
|
166
158
|
url: url.toString(),
|
|
@@ -24,7 +24,7 @@ declare function getAccountCookie(c: GenericEndpointContext): Promise<{
|
|
|
24
24
|
idToken?: string | null | undefined;
|
|
25
25
|
accessTokenExpiresAt?: Date | null | undefined;
|
|
26
26
|
refreshTokenExpiresAt?: Date | null | undefined;
|
|
27
|
-
|
|
27
|
+
scope?: string | null | undefined;
|
|
28
28
|
password?: string | null | undefined;
|
|
29
29
|
} | null>;
|
|
30
30
|
//#endregion
|
|
@@ -87,40 +87,7 @@ function matchType(columnDataType, fieldType, dbType) {
|
|
|
87
87
|
function normalize(type) {
|
|
88
88
|
return type.toLowerCase().split("(")[0].trim();
|
|
89
89
|
}
|
|
90
|
-
if (fieldType === "string[]" || fieldType === "number[]")
|
|
91
|
-
const normalized = normalize(columnDataType);
|
|
92
|
-
if (map[dbType].json.map((t) => t.toLowerCase()).includes(normalized)) return true;
|
|
93
|
-
if (dbType === "postgres") {
|
|
94
|
-
const element = columnDataType.toLowerCase().replace(/^_/, "").replace(/\[\]$/, "").trim();
|
|
95
|
-
return (fieldType === "string[]" ? new Set([
|
|
96
|
-
"text",
|
|
97
|
-
"varchar",
|
|
98
|
-
"character varying",
|
|
99
|
-
"char",
|
|
100
|
-
"character",
|
|
101
|
-
"bpchar",
|
|
102
|
-
"name",
|
|
103
|
-
"uuid",
|
|
104
|
-
"citext"
|
|
105
|
-
]) : new Set([
|
|
106
|
-
"int",
|
|
107
|
-
"int2",
|
|
108
|
-
"int4",
|
|
109
|
-
"int8",
|
|
110
|
-
"integer",
|
|
111
|
-
"smallint",
|
|
112
|
-
"bigint",
|
|
113
|
-
"numeric",
|
|
114
|
-
"decimal",
|
|
115
|
-
"real",
|
|
116
|
-
"float4",
|
|
117
|
-
"float8",
|
|
118
|
-
"double precision",
|
|
119
|
-
"money"
|
|
120
|
-
])).has(element);
|
|
121
|
-
}
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
90
|
+
if (fieldType === "string[]" || fieldType === "number[]") return columnDataType.toLowerCase().includes("json");
|
|
124
91
|
const types = map[dbType];
|
|
125
92
|
return (Array.isArray(fieldType) ? types["string"].map((t) => t.toLowerCase()) : types[fieldType].map((t) => t.toLowerCase())).includes(normalize(columnDataType));
|
|
126
93
|
}
|
|
@@ -89,6 +89,26 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
return {
|
|
92
|
+
createOAuthUser: async (user, account) => {
|
|
93
|
+
return runWithTransaction(adapter, async () => {
|
|
94
|
+
const createdUser = await createWithHooks({
|
|
95
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
96
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
97
|
+
...user,
|
|
98
|
+
email: user.email?.toLowerCase()
|
|
99
|
+
}, "user", void 0);
|
|
100
|
+
if (!createdUser) throw new APIError("BAD_REQUEST", { message: "Failed to create user" });
|
|
101
|
+
return {
|
|
102
|
+
user: createdUser,
|
|
103
|
+
account: await createWithHooks({
|
|
104
|
+
...account,
|
|
105
|
+
userId: createdUser.id,
|
|
106
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
107
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
108
|
+
}, "account", void 0)
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
},
|
|
92
112
|
createUser: async (user, source) => {
|
|
93
113
|
const data = {
|
|
94
114
|
createdAt: /* @__PURE__ */ new Date(),
|
package/dist/db/schema.d.mts
CHANGED
|
@@ -37,7 +37,7 @@ declare function parseAccountInput(options: BetterAuthOptions, account: Partial<
|
|
|
37
37
|
idToken?: string | null | undefined;
|
|
38
38
|
accessTokenExpiresAt?: Date | null | undefined;
|
|
39
39
|
refreshTokenExpiresAt?: Date | null | undefined;
|
|
40
|
-
|
|
40
|
+
scope?: string | null | undefined;
|
|
41
41
|
password?: string | null | undefined;
|
|
42
42
|
}>>;
|
|
43
43
|
declare function parseSessionInput(options: BetterAuthOptions, session: Partial<Session$1>, action?: "create" | "update"): Partial<Partial<{
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./state.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { signInWithOAuthIdentity } from "./sign-in-with-oauth-identity.mjs";
|
|
5
|
-
import { decryptOAuthToken, setTokenUtil } from "./token-encryption.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
3
|
+
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
|
|
6
4
|
export * from "@better-auth/core/oauth2";
|
|
7
|
-
export { GenerateStateOptions,
|
|
5
|
+
export { GenerateStateOptions, applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { decryptOAuthToken, setTokenUtil } from "./
|
|
2
|
-
import {
|
|
3
|
-
import { applyUpdateUserInfoOnLink, canLinkImplicitly, resolveOAuthUser } from "./resolve-account.mjs";
|
|
1
|
+
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
4
3
|
import { generateIdTokenNonce, generateState, parseState } from "./state.mjs";
|
|
5
|
-
import { signInWithOAuthIdentity } from "./sign-in-with-oauth-identity.mjs";
|
|
6
4
|
export * from "@better-auth/core/oauth2";
|
|
7
|
-
export { applyUpdateUserInfoOnLink,
|
|
5
|
+
export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Account, User } from "../types/models.mjs";
|
|
2
|
+
import { GenericEndpointContext, UserProvisioningSource } from "@better-auth/core";
|
|
3
|
+
|
|
4
|
+
//#region src/oauth2/link-account.d.ts
|
|
5
|
+
declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
6
|
+
userInfo: Omit<User, "createdAt" | "updatedAt">;
|
|
7
|
+
account: Omit<Account, "id" | "userId" | "createdAt" | "updatedAt">;
|
|
8
|
+
callbackURL?: string | undefined;
|
|
9
|
+
disableSignUp?: boolean | undefined;
|
|
10
|
+
overrideUserInfo?: boolean | undefined;
|
|
11
|
+
isTrustedProvider?: boolean | undefined;
|
|
12
|
+
trustProviderByName?: boolean | undefined;
|
|
13
|
+
source?: UserProvisioningSource | undefined;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
error: string;
|
|
16
|
+
data: null;
|
|
17
|
+
isRegister?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
error: string;
|
|
20
|
+
data: null;
|
|
21
|
+
isRegister: boolean;
|
|
22
|
+
} | {
|
|
23
|
+
data: {
|
|
24
|
+
session: {
|
|
25
|
+
id: string;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
userId: string;
|
|
29
|
+
expiresAt: Date;
|
|
30
|
+
token: string;
|
|
31
|
+
ipAddress?: string | null | undefined;
|
|
32
|
+
userAgent?: string | null | undefined;
|
|
33
|
+
};
|
|
34
|
+
user: {
|
|
35
|
+
id: string;
|
|
36
|
+
createdAt: Date;
|
|
37
|
+
updatedAt: Date;
|
|
38
|
+
email: string;
|
|
39
|
+
emailVerified: boolean;
|
|
40
|
+
name: string;
|
|
41
|
+
image?: string | null | undefined;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
error: null;
|
|
45
|
+
isRegister: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Provider profile a freshly linked account may copy onto the local user.
|
|
49
|
+
* `id` is the provider's account id (never the local user id), and `email`/
|
|
50
|
+
* `emailVerified` are identity anchors; all three are stripped before the
|
|
51
|
+
* remaining fields are written.
|
|
52
|
+
*/
|
|
53
|
+
type LinkedProviderProfile = {
|
|
54
|
+
id: string | number;
|
|
55
|
+
name?: string | undefined;
|
|
56
|
+
email?: string | null | undefined;
|
|
57
|
+
emailVerified?: boolean | undefined;
|
|
58
|
+
image?: string | null | undefined;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
|
|
62
|
+
* copy the freshly linked provider's profile onto the local user, matching the
|
|
63
|
+
* field set persisted on sign-up. The local `email` and `emailVerified` are
|
|
64
|
+
* never changed, so a link can't rebind the account's identity, and
|
|
65
|
+
* `updateUser` drops `undefined` fields, so a provider that omits one leaves
|
|
66
|
+
* the existing column intact.
|
|
67
|
+
*
|
|
68
|
+
* Returns the updated user so a caller that issues a session can seed the
|
|
69
|
+
* cookie cache with the fresh row. Returns `undefined` when the policy is
|
|
70
|
+
* disabled or the update fails: a failed profile sync must not abort the link.
|
|
71
|
+
*/
|
|
72
|
+
declare function applyUpdateUserInfoOnLink(c: GenericEndpointContext, userId: string, userInfo: LinkedProviderProfile): Promise<User | undefined>;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { isAPIError } from "../utils/is-api-error.mjs";
|
|
2
|
+
import { setAccountCookie } from "../cookies/session-store.mjs";
|
|
3
|
+
import { assertValidUserInfo } from "../utils/validate-user-info.mjs";
|
|
4
|
+
import { OAUTH_CALLBACK_ERROR_CODES, redirectOnError } from "./errors.mjs";
|
|
5
|
+
import { setTokenUtil } from "./utils.mjs";
|
|
6
|
+
import { createEmailVerificationToken } from "../api/routes/email-verification.mjs";
|
|
7
|
+
import { runWithTransaction } from "@better-auth/core/context";
|
|
8
|
+
import { isDevelopment, logger } from "@better-auth/core/env";
|
|
9
|
+
//#region src/oauth2/link-account.ts
|
|
10
|
+
async function handleOAuthUserInfo(c, opts) {
|
|
11
|
+
const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
|
|
12
|
+
const source = opts.source ?? {
|
|
13
|
+
method: "oauth",
|
|
14
|
+
oauth: { providerId: account.providerId }
|
|
15
|
+
};
|
|
16
|
+
const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => {
|
|
17
|
+
logger.error("Better auth was unable to query your database.\nError: ", e);
|
|
18
|
+
redirectOnError(c, c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`, "internal_server_error");
|
|
19
|
+
});
|
|
20
|
+
let user = dbUser?.user;
|
|
21
|
+
const isRegister = !user;
|
|
22
|
+
if (dbUser) {
|
|
23
|
+
const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
|
|
24
|
+
if (!linkedAccount) {
|
|
25
|
+
const accountLinking = c.context.options.account?.accountLinking;
|
|
26
|
+
const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
|
|
27
|
+
const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
|
|
28
|
+
if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
|
|
29
|
+
if (isDevelopment()) logger.warn(`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`);
|
|
30
|
+
return {
|
|
31
|
+
error: "account not linked",
|
|
32
|
+
data: null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const { id: _providerAccountId, ...providerUserInfo } = userInfo;
|
|
37
|
+
await assertValidUserInfo(c, {
|
|
38
|
+
user: {
|
|
39
|
+
...providerUserInfo,
|
|
40
|
+
id: dbUser.user.id,
|
|
41
|
+
email: userInfo.email.toLowerCase()
|
|
42
|
+
},
|
|
43
|
+
source: {
|
|
44
|
+
...source,
|
|
45
|
+
action: "link-account"
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
await c.context.internalAdapter.linkAccount({
|
|
49
|
+
providerId: account.providerId,
|
|
50
|
+
accountId: userInfo.id.toString(),
|
|
51
|
+
userId: dbUser.user.id,
|
|
52
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
53
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
54
|
+
idToken: account.idToken,
|
|
55
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
56
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
57
|
+
scope: account.scope
|
|
58
|
+
});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (isAPIError(e)) throw e;
|
|
61
|
+
logger.error("Unable to link account", e);
|
|
62
|
+
return {
|
|
63
|
+
error: "unable to link account",
|
|
64
|
+
data: null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
|
|
68
|
+
user = await applyUpdateUserInfoOnLink(c, dbUser.user.id, userInfo) ?? user;
|
|
69
|
+
} else {
|
|
70
|
+
const { id: _providerAccountId, ...providerUserInfo } = userInfo;
|
|
71
|
+
await assertValidUserInfo(c, {
|
|
72
|
+
user: {
|
|
73
|
+
...providerUserInfo,
|
|
74
|
+
id: dbUser.user.id,
|
|
75
|
+
email: userInfo.email.toLowerCase()
|
|
76
|
+
},
|
|
77
|
+
source: {
|
|
78
|
+
...source,
|
|
79
|
+
action: "sign-in"
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* `scope` intentionally omitted. Updated only via linkSocial.
|
|
84
|
+
*
|
|
85
|
+
* @see {@link Account.scope}
|
|
86
|
+
*/
|
|
87
|
+
const freshTokens = c.context.options.account?.updateAccountOnSignIn !== false ? Object.fromEntries(Object.entries({
|
|
88
|
+
idToken: account.idToken,
|
|
89
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
90
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
91
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
92
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt
|
|
93
|
+
}).filter(([_, value]) => value !== void 0)) : {};
|
|
94
|
+
if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, {
|
|
95
|
+
...linkedAccount,
|
|
96
|
+
...freshTokens
|
|
97
|
+
});
|
|
98
|
+
if (Object.keys(freshTokens).length > 0) await c.context.internalAdapter.updateAccount(linkedAccount.id, freshTokens);
|
|
99
|
+
if (userInfo.emailVerified && !dbUser.user.emailVerified && userInfo.email.toLowerCase() === dbUser.user.email) await c.context.internalAdapter.updateUser(dbUser.user.id, { emailVerified: true });
|
|
100
|
+
}
|
|
101
|
+
if (overrideUserInfo) {
|
|
102
|
+
const { id: _, ...restUserInfo } = userInfo;
|
|
103
|
+
const updatedUser = await c.context.internalAdapter.updateUser(dbUser.user.id, {
|
|
104
|
+
...restUserInfo,
|
|
105
|
+
email: userInfo.email.toLowerCase(),
|
|
106
|
+
emailVerified: userInfo.email.toLowerCase() === dbUser.user.email ? dbUser.user.emailVerified || userInfo.emailVerified : userInfo.emailVerified
|
|
107
|
+
});
|
|
108
|
+
if (updatedUser == null) logger.warn("Could not update user info during OAuth sign in; preserving existing user for session.");
|
|
109
|
+
user = updatedUser ?? user;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
if (disableSignUp) return {
|
|
113
|
+
error: "signup disabled",
|
|
114
|
+
data: null,
|
|
115
|
+
isRegister: false
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const { id: _, ...restUserInfo } = userInfo;
|
|
119
|
+
const accountData = {
|
|
120
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
121
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
122
|
+
idToken: account.idToken,
|
|
123
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
124
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
125
|
+
scope: account.scope,
|
|
126
|
+
providerId: account.providerId,
|
|
127
|
+
accountId: userInfo.id.toString()
|
|
128
|
+
};
|
|
129
|
+
const { createdUser, createdAccount } = await runWithTransaction(c.context.adapter, async () => {
|
|
130
|
+
const createdUser = await c.context.internalAdapter.createUser({
|
|
131
|
+
...restUserInfo,
|
|
132
|
+
email: userInfo.email.toLowerCase()
|
|
133
|
+
}, source);
|
|
134
|
+
return {
|
|
135
|
+
createdUser,
|
|
136
|
+
createdAccount: await c.context.internalAdapter.createAccount({
|
|
137
|
+
...accountData,
|
|
138
|
+
userId: createdUser.id
|
|
139
|
+
})
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
user = createdUser;
|
|
143
|
+
if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, createdAccount);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (isAPIError(e)) throw e;
|
|
146
|
+
logger.error("Unable to create OAuth user", e);
|
|
147
|
+
return {
|
|
148
|
+
error: "unable to create user",
|
|
149
|
+
data: null,
|
|
150
|
+
isRegister: false
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!user) return {
|
|
155
|
+
error: "unable to create user",
|
|
156
|
+
data: null,
|
|
157
|
+
isRegister: false
|
|
158
|
+
};
|
|
159
|
+
const requireEmailVerification = c.context.socialProviders.find((p) => p.id === account.providerId)?.options?.requireEmailVerification;
|
|
160
|
+
if (isRegister && !user.emailVerified && (c.context.options.emailVerification?.sendOnSignUp ?? requireEmailVerification)) await dispatchVerificationEmail(c, user, callbackURL);
|
|
161
|
+
if (requireEmailVerification && !user.emailVerified) {
|
|
162
|
+
if (!isRegister && c.context.options.emailVerification?.sendOnSignIn) await dispatchVerificationEmail(c, user, callbackURL);
|
|
163
|
+
return {
|
|
164
|
+
error: OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_VERIFIED,
|
|
165
|
+
data: null,
|
|
166
|
+
isRegister
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const session = await c.context.internalAdapter.createSession(user.id);
|
|
170
|
+
if (!session) return {
|
|
171
|
+
error: "unable to create session",
|
|
172
|
+
data: null,
|
|
173
|
+
isRegister: false
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
data: {
|
|
177
|
+
session,
|
|
178
|
+
user
|
|
179
|
+
},
|
|
180
|
+
error: null,
|
|
181
|
+
isRegister
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function dispatchVerificationEmail(c, user, callbackURL) {
|
|
185
|
+
const sendVerificationEmail = c.context.options.emailVerification?.sendVerificationEmail;
|
|
186
|
+
if (!sendVerificationEmail) return;
|
|
187
|
+
try {
|
|
188
|
+
const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
|
|
189
|
+
const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL || "/")}`;
|
|
190
|
+
await c.context.runInBackgroundOrAwait(sendVerificationEmail({
|
|
191
|
+
user,
|
|
192
|
+
url,
|
|
193
|
+
token
|
|
194
|
+
}, c.request));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
c.context.logger.error("Failed to send OAuth verification email", e);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
|
|
201
|
+
* copy the freshly linked provider's profile onto the local user, matching the
|
|
202
|
+
* field set persisted on sign-up. The local `email` and `emailVerified` are
|
|
203
|
+
* never changed, so a link can't rebind the account's identity, and
|
|
204
|
+
* `updateUser` drops `undefined` fields, so a provider that omits one leaves
|
|
205
|
+
* the existing column intact.
|
|
206
|
+
*
|
|
207
|
+
* Returns the updated user so a caller that issues a session can seed the
|
|
208
|
+
* cookie cache with the fresh row. Returns `undefined` when the policy is
|
|
209
|
+
* disabled or the update fails: a failed profile sync must not abort the link.
|
|
210
|
+
*/
|
|
211
|
+
async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
|
|
212
|
+
if (c.context.options.account?.accountLinking?.updateUserInfoOnLink !== true) return;
|
|
213
|
+
const { id: _id, email: _email, emailVerified: _emailVerified, ...profile } = userInfo;
|
|
214
|
+
try {
|
|
215
|
+
return await c.context.internalAdapter.updateUser(userId, profile);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
c.context.logger.warn("Could not update user info on account link", e);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|
package/dist/oauth2/state.d.mts
CHANGED
|
@@ -14,14 +14,6 @@ declare function generateIdTokenNonce(provider: {
|
|
|
14
14
|
/**
|
|
15
15
|
* Inputs for {@link generateState}. Grouped into one object so call sites read
|
|
16
16
|
* by name instead of by position.
|
|
17
|
-
*
|
|
18
|
-
* `requestedScopes` is the effective scope set encoded in the authorization
|
|
19
|
-
* URL, persisted into OAuth state so the callback can fall back to the request
|
|
20
|
-
* when the provider omits `scope` from its token response (RFC 6749 §5.1).
|
|
21
|
-
* Because that set is only known after `createAuthorizationURL` runs, the
|
|
22
|
-
* redirect-initiating routes build the URL first, then pass the URL's `state`
|
|
23
|
-
* and `codeVerifier` here so state is still written exactly once. Flows that do
|
|
24
|
-
* not need the URL first (e.g. SSO) omit both and let fresh values be minted.
|
|
25
17
|
*/
|
|
26
18
|
interface GenerateStateOptions {
|
|
27
19
|
/** Link target when this flow links a provider identity to an existing user. */
|
|
@@ -31,8 +23,6 @@ interface GenerateStateOptions {
|
|
|
31
23
|
} | undefined;
|
|
32
24
|
/** Extra data to round-trip through state; `false` writes none. */
|
|
33
25
|
additionalData?: Record<string, any> | false | undefined;
|
|
34
|
-
/** The effective scopes encoded in the authorization URL (the §5.1 fallback). */
|
|
35
|
-
requestedScopes?: string[] | undefined;
|
|
36
26
|
/** The `state` nonce already used to build the authorization URL. Minted when omitted. */
|
|
37
27
|
state?: string | undefined;
|
|
38
28
|
/** The PKCE `codeVerifier` already used to build the authorization URL. Minted when omitted. */
|
|
@@ -58,7 +48,6 @@ declare function parseState(c: GenericEndpointContext): Promise<{
|
|
|
58
48
|
} | undefined;
|
|
59
49
|
requestSignUp?: boolean | undefined;
|
|
60
50
|
idTokenNonce?: string | undefined;
|
|
61
|
-
requestedScopes?: string[] | undefined;
|
|
62
51
|
serverContext?: Record<string, unknown> | undefined;
|
|
63
52
|
}>;
|
|
64
53
|
//#endregion
|
package/dist/oauth2/state.mjs
CHANGED
|
@@ -30,12 +30,11 @@ async function generateState(c, options) {
|
|
|
30
30
|
serverContext,
|
|
31
31
|
expiresAt: Date.now() + 600 * 1e3,
|
|
32
32
|
requestSignUp: c.body?.requestSignUp,
|
|
33
|
-
requestedScopes: options?.requestedScopes,
|
|
34
33
|
idTokenNonce: options?.idTokenNonce
|
|
35
34
|
};
|
|
36
35
|
await setOAuthState(stateData);
|
|
37
36
|
try {
|
|
38
|
-
return generateGenericState(c, stateData
|
|
37
|
+
return generateGenericState(c, stateData);
|
|
39
38
|
} catch (error) {
|
|
40
39
|
c.context.logger.error("Failed to create verification", error);
|
|
41
40
|
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AuthContext, LiteralString } from "@better-auth/core";
|
|
2
|
+
|
|
3
|
+
//#region src/oauth2/utils.d.ts
|
|
4
|
+
declare function decryptOAuthToken(token: string, ctx: AuthContext): string | Promise<string>;
|
|
5
|
+
declare function setTokenUtil(token: string | null | undefined, ctx: AuthContext): string | Promise<string> | null | undefined;
|
|
6
|
+
declare function getOAuthCallbackPath(provider: {
|
|
7
|
+
id: LiteralString;
|
|
8
|
+
callbackPath?: string | undefined;
|
|
9
|
+
}): string;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { symmetricDecrypt, symmetricEncrypt } from "../crypto/index.mjs";
|
|
2
|
-
//#region src/oauth2/
|
|
2
|
+
//#region src/oauth2/utils.ts
|
|
3
3
|
/**
|
|
4
4
|
* Check if a string looks like encrypted data
|
|
5
5
|
*/
|
|
@@ -25,5 +25,9 @@ function setTokenUtil(token, ctx) {
|
|
|
25
25
|
});
|
|
26
26
|
return token;
|
|
27
27
|
}
|
|
28
|
+
function getOAuthCallbackPath(provider) {
|
|
29
|
+
if (!provider.callbackPath) return `/callback/${provider.id}`;
|
|
30
|
+
return provider.callbackPath.startsWith("/") ? provider.callbackPath : `/${provider.callbackPath}`;
|
|
31
|
+
}
|
|
28
32
|
//#endregion
|
|
29
|
-
export { decryptOAuthToken, setTokenUtil };
|
|
33
|
+
export { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil };
|
package/dist/package.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { PatreonOptions, patreon } from "./providers/patreon.mjs";
|
|
|
10
10
|
import { SlackOptions, slack } from "./providers/slack.mjs";
|
|
11
11
|
import { AuthContext } from "@better-auth/core";
|
|
12
12
|
import * as _better_auth_core_oauth20 from "@better-auth/core/oauth2";
|
|
13
|
-
import {
|
|
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
15
|
|
|
16
16
|
//#region src/plugins/generic-oauth/index.d.ts
|
|
@@ -38,7 +38,7 @@ declare const genericOAuth: <const ID extends string>(options: GenericOAuthOptio
|
|
|
38
38
|
version: string;
|
|
39
39
|
init: (ctx: AuthContext) => Promise<{
|
|
40
40
|
context: {
|
|
41
|
-
socialProviders:
|
|
41
|
+
socialProviders: OAuthProvider<Record<string, any>, Partial<_better_auth_core_oauth20.ProviderOptions<any>>>[];
|
|
42
42
|
};
|
|
43
43
|
}>;
|
|
44
44
|
options: GenericOAuthOptions<ID>;
|
|
@@ -122,7 +122,6 @@ const genericOAuth = (options) => {
|
|
|
122
122
|
const provider = {
|
|
123
123
|
id: c.providerId,
|
|
124
124
|
name: c.name ?? c.providerId,
|
|
125
|
-
callbackPath: `/callback/${c.providerId}`,
|
|
126
125
|
issuer,
|
|
127
126
|
idToken: idTokenConfig,
|
|
128
127
|
requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
|
|
@@ -152,13 +152,13 @@ const oauthPopupStart = createAuthEndpoint("/oauth-popup/start", {
|
|
|
152
152
|
popupOrigin,
|
|
153
153
|
popupNonce: c.query.popupNonce ?? ""
|
|
154
154
|
}), c.context.secret, marker.attributes);
|
|
155
|
-
|
|
155
|
+
url = await provider.createAuthorizationURL({
|
|
156
156
|
state,
|
|
157
157
|
codeVerifier,
|
|
158
158
|
idTokenNonce,
|
|
159
159
|
redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
|
|
160
160
|
scopes: c.query.scopes ? c.query.scopes.split(",") : void 0
|
|
161
|
-
})
|
|
161
|
+
});
|
|
162
162
|
} catch (error) {
|
|
163
163
|
c.context.logger.error("OAuth popup failed to start", error);
|
|
164
164
|
return fail("popup_sign_in_failed", "Failed to start the OAuth flow.");
|