better-auth 1.7.1 → 1.7.3
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 +6 -22
- package/dist/api/index.mjs +2 -0
- package/dist/api/middlewares/origin-check.mjs +9 -5
- package/dist/api/routes/account.d.mts +1 -9
- package/dist/api/routes/account.mjs +0 -6
- package/dist/api/routes/callback.mjs +7 -7
- package/dist/api/routes/email-verification.mjs +4 -2
- package/dist/api/routes/error.mjs +12 -8
- package/dist/api/routes/password.mjs +0 -2
- package/dist/api/routes/session.mjs +12 -5
- package/dist/api/routes/sign-in.d.mts +2 -2
- package/dist/api/routes/sign-in.mjs +1 -4
- package/dist/api/routes/sign-up.mjs +0 -2
- package/dist/api/routes/update-user.mjs +0 -2
- package/dist/api/to-auth-endpoints.mjs +2 -0
- package/dist/auth/base.mjs +13 -1
- package/dist/auth/trusted-origins.mjs +24 -5
- package/dist/client/config.d.mts +2 -0
- package/dist/client/config.mjs +1 -0
- package/dist/client/lynx/index.d.mts +1 -1
- package/dist/client/react/index.d.mts +1 -1
- package/dist/client/solid/index.d.mts +1 -1
- package/dist/client/svelte/index.d.mts +1 -1
- package/dist/client/vanilla.d.mts +1 -1
- package/dist/client/vue/index.d.mts +32 -21
- package/dist/client/vue/index.mjs +23 -5
- package/dist/context/create-context.mjs +2 -0
- package/dist/cookies/cache.mjs +10 -2
- package/dist/cookies/session-store.d.mts +0 -1
- package/dist/cookies/session-store.mjs +16 -5
- package/dist/db/adapter-kysely.mjs +1 -2
- package/dist/db/get-migration.d.mts +1 -0
- package/dist/db/get-migration.mjs +49 -56
- package/dist/db/internal-adapter.mjs +17 -20
- package/dist/db/schema.d.mts +0 -1
- package/dist/db/with-hooks.mjs +7 -7
- package/dist/oauth2/account-key.mjs +1 -5
- package/dist/oauth2/errors.mjs +4 -3
- package/dist/oauth2/link-account.d.mts +0 -1
- package/dist/oauth2/link-account.mjs +5 -11
- package/dist/oauth2/state.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +1 -4
- package/dist/plugins/anonymous/index.mjs +5 -1
- package/dist/plugins/anonymous/types.d.mts +2 -3
- package/dist/plugins/device-authorization/index.d.mts +11 -11
- package/dist/plugins/email-otp/routes.mjs +0 -2
- package/dist/plugins/generic-oauth/index.mjs +14 -10
- package/dist/plugins/generic-oauth/providers/auth0.mjs +2 -4
- package/dist/plugins/generic-oauth/providers/keycloak.mjs +6 -9
- package/dist/plugins/generic-oauth/providers/line.mjs +0 -1
- package/dist/plugins/generic-oauth/providers/microsoft-entra-id.mjs +15 -5
- package/dist/plugins/generic-oauth/providers/okta.mjs +6 -9
- package/dist/plugins/generic-oauth/providers/slack.mjs +0 -1
- package/dist/plugins/generic-oauth/types.d.mts +0 -9
- package/dist/plugins/haveibeenpwned/index.d.mts +9 -1
- package/dist/plugins/haveibeenpwned/index.mjs +33 -13
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/jwt/verify.mjs +2 -2
- package/dist/plugins/last-login-method/index.mjs +1 -0
- package/dist/plugins/oauth-proxy/index.d.mts +17 -0
- package/dist/plugins/oauth-proxy/index.mjs +145 -99
- package/dist/plugins/one-tap/index.mjs +0 -1
- package/dist/plugins/open-api/generator.mjs +15 -1
- package/dist/plugins/organization/has-permission.mjs +4 -2
- package/dist/plugins/phone-number/routes.mjs +0 -2
- package/dist/plugins/siwe/index.mjs +5 -6
- package/dist/plugins/two-factor/client.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.mjs +1 -0
- package/dist/plugins/two-factor/index.d.mts +1 -0
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/state.d.mts +0 -13
- package/dist/test-utils/http-test-instance.d.mts +1 -1
- package/dist/test-utils/test-instance.mjs +27 -10
- package/package.json +10 -10
|
@@ -3,8 +3,7 @@ import { getDate } from "../utils/date.mjs";
|
|
|
3
3
|
import { assertValidUserInfo, assertValidUserInfoSource } from "../utils/validate-user-info.mjs";
|
|
4
4
|
import { getStorageOption, processIdentifier } from "./verification-token-storage.mjs";
|
|
5
5
|
import { getWithHooks } from "./with-hooks.mjs";
|
|
6
|
-
import { getCurrentAdapter,
|
|
7
|
-
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
6
|
+
import { getCurrentAdapter, getCurrentAuthEndpointContext, queueAfterTransactionHook, runWithTransaction, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
8
7
|
import { APIError, BetterAuthError } from "@better-auth/core/error";
|
|
9
8
|
import { generateId } from "@better-auth/core/utils/id";
|
|
10
9
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
@@ -153,7 +152,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
153
152
|
assertValidUserInfoSource(validationSource);
|
|
154
153
|
let endpointContext;
|
|
155
154
|
try {
|
|
156
|
-
endpointContext =
|
|
155
|
+
endpointContext = getCurrentAuthEndpointContext();
|
|
157
156
|
} catch (error) {
|
|
158
157
|
logger.error("Unable to run validateUserInfo: missing endpoint context", error);
|
|
159
158
|
throw new APIError("FORBIDDEN", {
|
|
@@ -247,7 +246,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
247
246
|
},
|
|
248
247
|
createSession: async (userId, dontRememberMe, override, overrideAll, storageOptions) => {
|
|
249
248
|
const headers = await (async () => {
|
|
250
|
-
const ctx =
|
|
249
|
+
const ctx = tryGetCurrentAuthEndpointContext();
|
|
251
250
|
return ctx?.headers || ctx?.request?.headers;
|
|
252
251
|
})();
|
|
253
252
|
const storeInDb = options.session?.storeSessionInDatabase;
|
|
@@ -540,18 +539,21 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
540
539
|
operator: "in"
|
|
541
540
|
}], "session", void 0);
|
|
542
541
|
},
|
|
543
|
-
findAccountOwnerByKey: async ({
|
|
544
|
-
const
|
|
542
|
+
findAccountOwnerByKey: async ({ providerId, accountId }) => {
|
|
543
|
+
const accountsWithUsers = await (await getCurrentAdapter(adapter)).findMany({
|
|
545
544
|
model: "account",
|
|
546
545
|
where: [{
|
|
547
|
-
field: "
|
|
548
|
-
value:
|
|
546
|
+
field: "providerId",
|
|
547
|
+
value: providerId
|
|
549
548
|
}, {
|
|
550
549
|
field: "accountId",
|
|
551
550
|
value: accountId
|
|
552
551
|
}],
|
|
552
|
+
limit: 2,
|
|
553
553
|
join: { user: true }
|
|
554
554
|
});
|
|
555
|
+
if (accountsWithUsers.length > 1) throw new BetterAuthError(`Multiple accounts match the same accountId for provider ${JSON.stringify(providerId)}. Resolve duplicate account identities before continuing.`);
|
|
556
|
+
const accountWithUser = accountsWithUsers[0];
|
|
555
557
|
if (!accountWithUser) return null;
|
|
556
558
|
const { user, ...account } = accountWithUser;
|
|
557
559
|
return user ? {
|
|
@@ -632,10 +634,6 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
632
634
|
field: "providerId",
|
|
633
635
|
value: "credential"
|
|
634
636
|
},
|
|
635
|
-
{
|
|
636
|
-
field: "issuer",
|
|
637
|
-
value: createLocalAccountIssuer("credential")
|
|
638
|
-
},
|
|
639
637
|
{
|
|
640
638
|
field: "accountId",
|
|
641
639
|
value: userId
|
|
@@ -663,10 +661,6 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
663
661
|
field: "providerId",
|
|
664
662
|
value: "credential"
|
|
665
663
|
},
|
|
666
|
-
{
|
|
667
|
-
field: "issuer",
|
|
668
|
-
value: createLocalAccountIssuer("credential")
|
|
669
|
-
},
|
|
670
664
|
{
|
|
671
665
|
field: "accountId",
|
|
672
666
|
value: userId
|
|
@@ -674,17 +668,20 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
674
668
|
]
|
|
675
669
|
});
|
|
676
670
|
},
|
|
677
|
-
findAccountByKey: async ({
|
|
678
|
-
|
|
671
|
+
findAccountByKey: async ({ providerId, accountId }) => {
|
|
672
|
+
const accounts = await (await getCurrentAdapter(adapter)).findMany({
|
|
679
673
|
model: "account",
|
|
674
|
+
limit: 2,
|
|
680
675
|
where: [{
|
|
681
|
-
field: "
|
|
682
|
-
value:
|
|
676
|
+
field: "providerId",
|
|
677
|
+
value: providerId
|
|
683
678
|
}, {
|
|
684
679
|
field: "accountId",
|
|
685
680
|
value: accountId
|
|
686
681
|
}]
|
|
687
682
|
});
|
|
683
|
+
if (accounts.length > 1) throw new BetterAuthError(`Multiple accounts match the same accountId for provider ${JSON.stringify(providerId)}. Resolve duplicate account identities before continuing.`);
|
|
684
|
+
return accounts[0] ?? null;
|
|
688
685
|
},
|
|
689
686
|
findAccountByUserId: async (userId) => {
|
|
690
687
|
return await (await getCurrentAdapter(adapter)).findMany({
|
package/dist/db/schema.d.mts
CHANGED
package/dist/db/with-hooks.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { getCurrentAdapter,
|
|
1
|
+
import { getCurrentAdapter, queueAfterTransactionHook, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
2
2
|
import { ATTR_CONTEXT, ATTR_DB_COLLECTION_NAME, ATTR_HOOK_TYPE, withSpan } from "@better-auth/core/instrumentation";
|
|
3
3
|
//#region src/db/with-hooks.ts
|
|
4
4
|
function getWithHooks(adapter, ctx) {
|
|
5
5
|
const hooksEntries = ctx.hooks;
|
|
6
6
|
async function createWithHooks(data, model, customCreateFn) {
|
|
7
|
-
const context =
|
|
7
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
8
8
|
let actualData = data;
|
|
9
9
|
for (const { source, hooks } of hooksEntries) {
|
|
10
10
|
const toRun = hooks[model]?.create?.before;
|
|
@@ -41,7 +41,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
41
41
|
return created;
|
|
42
42
|
}
|
|
43
43
|
async function updateWithHooks(data, where, model, customUpdateFn) {
|
|
44
|
-
const context =
|
|
44
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
45
45
|
let actualData = data;
|
|
46
46
|
for (const { source, hooks } of hooksEntries) {
|
|
47
47
|
const toRun = hooks[model]?.update?.before;
|
|
@@ -77,7 +77,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
77
77
|
return updated;
|
|
78
78
|
}
|
|
79
79
|
async function updateManyWithHooks(data, where, model, customUpdateFn) {
|
|
80
|
-
const context =
|
|
80
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
81
81
|
let actualData = data;
|
|
82
82
|
for (const { source, hooks } of hooksEntries) {
|
|
83
83
|
const toRun = hooks[model]?.update?.before;
|
|
@@ -113,7 +113,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
113
113
|
return updated;
|
|
114
114
|
}
|
|
115
115
|
async function deleteWithHooks(where, model, customDeleteFn) {
|
|
116
|
-
const context =
|
|
116
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
117
117
|
let entityToDelete = null;
|
|
118
118
|
try {
|
|
119
119
|
entityToDelete = (await (await getCurrentAdapter(adapter)).findMany({
|
|
@@ -150,7 +150,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
150
150
|
return deleted;
|
|
151
151
|
}
|
|
152
152
|
async function deleteManyWithHooks(where, model, customDeleteFn) {
|
|
153
|
-
const context =
|
|
153
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
154
154
|
let entitiesToDelete = [];
|
|
155
155
|
try {
|
|
156
156
|
entitiesToDelete = await (await getCurrentAdapter(adapter)).findMany({
|
|
@@ -202,7 +202,7 @@ function getWithHooks(adapter, ctx) {
|
|
|
202
202
|
* the helper resolves to `null` (no `consumeFn` call, no after hooks).
|
|
203
203
|
*/
|
|
204
204
|
async function consumeOneWithHooks(model, hookWhere, consumeFn, preSnapshot) {
|
|
205
|
-
const context =
|
|
205
|
+
const context = tryGetCurrentAuthEndpointContext();
|
|
206
206
|
const beforeHooks = hooksEntries.flatMap(({ source, hooks }) => {
|
|
207
207
|
const fn = hooks[model]?.delete?.before;
|
|
208
208
|
return fn ? [{
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createOAuthAccountIssuer } from "@better-auth/core/db";
|
|
2
1
|
import { APIError, BASE_ERROR_CODES, BetterAuthError } from "@better-auth/core/error";
|
|
3
2
|
//#region src/oauth2/account-key.ts
|
|
4
3
|
/**
|
|
@@ -21,11 +20,8 @@ async function resolveOAuthAccountKey(provider, tokens, profile) {
|
|
|
21
20
|
const resolvedSubject = await accountSubject(accountKeyContext);
|
|
22
21
|
const accountId = String(resolvedSubject);
|
|
23
22
|
if (typeof resolvedSubject === "number" && !Number.isFinite(resolvedSubject) || accountId.trim().length === 0 || accountId === "undefined" || accountId === "null") throw new BetterAuthError("OAUTH_ACCOUNT_SUBJECT_INVALID");
|
|
24
|
-
const accountIssuer = provider.accountIssuer;
|
|
25
|
-
const issuer = accountIssuer === void 0 ? createOAuthAccountIssuer(provider.id) : typeof accountIssuer === "function" ? await accountIssuer(accountKeyContext) : accountIssuer;
|
|
26
|
-
if (typeof issuer !== "string" || issuer.trim().length === 0 || issuer === "undefined" || issuer === "null") throw new BetterAuthError("OAUTH_ACCOUNT_ISSUER_INVALID");
|
|
27
23
|
return {
|
|
28
|
-
|
|
24
|
+
providerId: provider.id,
|
|
29
25
|
accountId
|
|
30
26
|
};
|
|
31
27
|
}
|
package/dist/oauth2/errors.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
1
2
|
//#region src/oauth2/errors.ts
|
|
2
3
|
/**
|
|
3
4
|
* Error codes used in OAuth callback redirects (`?error=<code>`). These are
|
|
@@ -33,8 +34,8 @@ const HANDLING_DOCS_URL = "https://www.better-auth.com/docs/concepts/oauth#handl
|
|
|
33
34
|
function redirectOnError(ctx, errorURL, error, description) {
|
|
34
35
|
const params = new URLSearchParams({ error });
|
|
35
36
|
if (description) params.set("error_description", description);
|
|
36
|
-
const
|
|
37
|
-
throw ctx.redirect(
|
|
37
|
+
const redirectURL = appendQueryParams(errorURL, params);
|
|
38
|
+
throw ctx.redirect(redirectURL);
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* Build the logger message shown when an OAuth provider does not return an
|
|
@@ -42,7 +43,7 @@ function redirectOnError(ctx, errorURL, error, description) {
|
|
|
42
43
|
* the same workaround docs.
|
|
43
44
|
*/
|
|
44
45
|
function missingEmailLogMessage(providerId, options) {
|
|
45
|
-
return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or
|
|
46
|
+
return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or create a placeholder via \`mapProfileToUser\`. See ${HANDLING_DOCS_URL}`;
|
|
46
47
|
}
|
|
47
48
|
//#endregion
|
|
48
49
|
export { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage, redirectOnError };
|
|
@@ -18,7 +18,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
18
18
|
const requireExactAccountBinding = !!opts.selectedUser || opts.requireExactAccountBinding === true;
|
|
19
19
|
let pendingAccountCookie = null;
|
|
20
20
|
const accountOwner = await c.context.internalAdapter.findAccountOwnerByKey({
|
|
21
|
-
|
|
21
|
+
providerId: account.providerId,
|
|
22
22
|
accountId: account.accountId
|
|
23
23
|
}).catch((e) => {
|
|
24
24
|
c.context.logger.error("Better auth was unable to query your database.\nError: ", e);
|
|
@@ -38,10 +38,6 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
38
38
|
code: "account_ownership_conflict",
|
|
39
39
|
message: "Account is already linked to another user"
|
|
40
40
|
});
|
|
41
|
-
if (requireExactAccountBinding && accountOwner.account.providerId !== account.providerId) throw new APIError("CONFLICT", {
|
|
42
|
-
code: "account_provider_conflict",
|
|
43
|
-
message: "Account is already linked through another provider"
|
|
44
|
-
});
|
|
45
41
|
return {
|
|
46
42
|
user: accountOwner.user,
|
|
47
43
|
linkedAccount: accountOwner.account,
|
|
@@ -75,7 +71,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
75
71
|
let user = dbUser?.user;
|
|
76
72
|
const isRegister = !user;
|
|
77
73
|
if (dbUser) {
|
|
78
|
-
const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.
|
|
74
|
+
const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
|
|
79
75
|
if (!linkedAccount) {
|
|
80
76
|
const accountLinking = c.context.options.account?.accountLinking;
|
|
81
77
|
const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
|
|
@@ -102,7 +98,6 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
102
98
|
});
|
|
103
99
|
const createdAccount = await c.context.internalAdapter.linkAccount({
|
|
104
100
|
providerId: account.providerId,
|
|
105
|
-
issuer: account.issuer,
|
|
106
101
|
accountId: account.accountId,
|
|
107
102
|
userId: dbUser.user.id,
|
|
108
103
|
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
@@ -116,7 +111,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
116
111
|
error: "unable to link account",
|
|
117
112
|
data: null
|
|
118
113
|
};
|
|
119
|
-
if (requireExactAccountBinding && (createdAccount.
|
|
114
|
+
if (requireExactAccountBinding && (createdAccount.accountId !== account.accountId || createdAccount.providerId !== account.providerId || createdAccount.userId !== dbUser.user.id)) throw new APIError("CONFLICT", {
|
|
120
115
|
code: "account_hook_binding_conflict",
|
|
121
116
|
message: "Account hook changed the selected authentication binding"
|
|
122
117
|
});
|
|
@@ -172,7 +167,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
172
167
|
error: "unable to update account",
|
|
173
168
|
data: null
|
|
174
169
|
};
|
|
175
|
-
if (requireExactAccountBinding && (updatedAccount.
|
|
170
|
+
if (requireExactAccountBinding && (updatedAccount.accountId !== account.accountId || updatedAccount.providerId !== account.providerId || updatedAccount.userId !== dbUser.user.id)) throw new APIError("CONFLICT", {
|
|
176
171
|
code: "account_hook_binding_conflict",
|
|
177
172
|
message: "Account hook changed the selected authentication binding"
|
|
178
173
|
});
|
|
@@ -214,7 +209,6 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
214
209
|
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
215
210
|
scope: account.scope,
|
|
216
211
|
providerId: account.providerId,
|
|
217
|
-
issuer: account.issuer,
|
|
218
212
|
accountId: account.accountId
|
|
219
213
|
};
|
|
220
214
|
const { createdUser, createdAccount } = await runWithTransaction(c.context.adapter, async () => {
|
|
@@ -233,7 +227,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
233
227
|
})
|
|
234
228
|
};
|
|
235
229
|
});
|
|
236
|
-
if (requireExactAccountBinding && (createdAccount.
|
|
230
|
+
if (requireExactAccountBinding && (createdAccount.accountId !== account.accountId || createdAccount.providerId !== account.providerId || createdAccount.userId !== createdUser.id)) throw new APIError("CONFLICT", {
|
|
237
231
|
code: "account_hook_binding_conflict",
|
|
238
232
|
message: "Account hook changed the selected authentication binding"
|
|
239
233
|
});
|
package/dist/oauth2/state.mjs
CHANGED
|
@@ -55,7 +55,7 @@ async function parseState(c) {
|
|
|
55
55
|
let redirectErrorURL = errorURL;
|
|
56
56
|
if (error instanceof StateError) {
|
|
57
57
|
code = error.code === "state_security_mismatch" ? "state_mismatch" : error.code;
|
|
58
|
-
redirectErrorURL = error.errorURL
|
|
58
|
+
redirectErrorURL = error.errorURL || errorURL;
|
|
59
59
|
}
|
|
60
60
|
redirectOnError(c, redirectErrorURL, code);
|
|
61
61
|
}
|
package/dist/package.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cooki
|
|
|
4
4
|
import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
5
5
|
import { ADMIN_ERROR_CODES } from "./error-codes.mjs";
|
|
6
6
|
import { hasPermission } from "./has-permission.mjs";
|
|
7
|
-
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
8
7
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
9
8
|
import { whereOperators } from "@better-auth/core/db/adapter";
|
|
10
9
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
@@ -203,7 +202,6 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
|
203
202
|
const hashedPassword = await ctx.context.password.hash(ctx.body.password);
|
|
204
203
|
await ctx.context.internalAdapter.linkAccount({
|
|
205
204
|
providerId: "credential",
|
|
206
|
-
issuer: createLocalAccountIssuer("credential"),
|
|
207
205
|
accountId: user.id,
|
|
208
206
|
password: hashedPassword,
|
|
209
207
|
userId: user.id
|
|
@@ -534,7 +532,7 @@ const banUser = (opts) => createAuthEndpoint("/admin/ban-user", {
|
|
|
534
532
|
const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
|
|
535
533
|
banned: true,
|
|
536
534
|
banReason: ctx.body.banReason || opts?.defaultBanReason || "No reason",
|
|
537
|
-
banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") :
|
|
535
|
+
banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") : null,
|
|
538
536
|
updatedAt: /* @__PURE__ */ new Date()
|
|
539
537
|
});
|
|
540
538
|
await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
|
|
@@ -842,7 +840,6 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
|
|
|
842
840
|
else await ctx.context.internalAdapter.createAccount({
|
|
843
841
|
userId,
|
|
844
842
|
providerId: "credential",
|
|
845
|
-
issuer: createLocalAccountIssuer("credential"),
|
|
846
843
|
accountId: user.id,
|
|
847
844
|
password: hashedPassword
|
|
848
845
|
});
|
|
@@ -10,6 +10,7 @@ import { schema } from "./schema.mjs";
|
|
|
10
10
|
import { generateId } from "@better-auth/core/utils/id";
|
|
11
11
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
12
12
|
import * as z from "zod";
|
|
13
|
+
import { createPlaceholderEmail } from "@better-auth/core/utils/email";
|
|
13
14
|
//#region src/plugins/anonymous/index.ts
|
|
14
15
|
/**
|
|
15
16
|
* Resolves the anonymous session being upgraded during an account-link callback.
|
|
@@ -51,7 +52,10 @@ async function getAnonUserEmail(options) {
|
|
|
51
52
|
}
|
|
52
53
|
const id = generateId();
|
|
53
54
|
if (options?.emailDomainName) return `temp-${id}@${options.emailDomainName}`;
|
|
54
|
-
return
|
|
55
|
+
return createPlaceholderEmail({
|
|
56
|
+
identifier: id,
|
|
57
|
+
namespace: "anonymous"
|
|
58
|
+
});
|
|
55
59
|
}
|
|
56
60
|
const anonymous = (options) => {
|
|
57
61
|
return {
|
|
@@ -18,9 +18,8 @@ interface UserWithAnonymous extends User {
|
|
|
18
18
|
}
|
|
19
19
|
interface AnonymousOptions {
|
|
20
20
|
/**
|
|
21
|
-
* Configure
|
|
22
|
-
*
|
|
23
|
-
* @default "baseURL"
|
|
21
|
+
* Configure a custom domain for anonymous user email addresses.
|
|
22
|
+
* @default "anonymous.placeholder.invalid"
|
|
24
23
|
*/
|
|
25
24
|
emailDomainName?: string | undefined;
|
|
26
25
|
/**
|
|
@@ -162,37 +162,37 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
|
|
|
162
162
|
client_id: z.ZodString;
|
|
163
163
|
user_id: z.ZodOptional<z.ZodString>;
|
|
164
164
|
scope: z.ZodOptional<z.ZodString>;
|
|
165
|
-
} & (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
165
|
+
} & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
166
166
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
167
|
-
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) : ({
|
|
167
|
+
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_1 ? { -readonly [P in keyof T_1]: T_1[P] } : never) : ({
|
|
168
168
|
client_id: z.ZodString;
|
|
169
169
|
user_id: z.ZodOptional<z.ZodString>;
|
|
170
170
|
scope: z.ZodOptional<z.ZodString>;
|
|
171
|
-
} extends infer
|
|
171
|
+
} extends infer T_2 extends z.core.util.SomeObject ? { [K in keyof T_2 as K extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
172
172
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
173
|
-
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K]:
|
|
173
|
+
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K]: T_2[K] } : never) & (((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
174
174
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
175
|
-
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer
|
|
175
|
+
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_4 ? { -readonly [P in keyof T_4]: T_4[P] } : never) extends infer T_3 extends z.core.util.SomeObject ? { [K_1 in keyof T_3]: T_3[K_1] } : never)) extends infer T ? { [k in keyof T]: T[k] } : never, z.core.$strip> | z.ZodObject<(("scope" | "user_id" | "client_id") & keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
176
176
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
177
177
|
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends never ? {
|
|
178
178
|
user_id: z.ZodOptional<z.ZodString>;
|
|
179
179
|
scope: z.ZodOptional<z.ZodString>;
|
|
180
180
|
client_id: z.ZodOptional<z.ZodString>;
|
|
181
|
-
} & (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
181
|
+
} & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
182
182
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
183
|
-
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) : ({
|
|
183
|
+
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_6 ? { -readonly [P in keyof T_6]: T_6[P] } : never) : ({
|
|
184
184
|
user_id: z.ZodOptional<z.ZodString>;
|
|
185
185
|
scope: z.ZodOptional<z.ZodString>;
|
|
186
186
|
client_id: z.ZodOptional<z.ZodString>;
|
|
187
|
-
} extends infer
|
|
187
|
+
} extends infer T_7 extends z.core.util.SomeObject ? { [K_2 in keyof T_7 as K_2 extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
188
188
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
189
|
-
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K_2]:
|
|
189
|
+
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) ? never : K_2]: T_7[K_2] } : never) & (((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
190
190
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
191
|
-
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer
|
|
191
|
+
}>, Record<string, unknown>> ? RequestFields : Record<never, never>) extends infer T_9 ? { -readonly [P in keyof T_9]: T_9[P] } : never) extends infer T_8 extends z.core.util.SomeObject ? { [K_1 in keyof T_8]: T_8[K_1] } : never)) extends infer T_5 ? { [k_1 in keyof T_5]: T_5[k_1] } : never, z.core.$strip>;
|
|
192
192
|
error: z.ZodObject<{
|
|
193
193
|
error: z.ZodEnum<{ [k_3 in (readonly ["invalid_request", "invalid_client", "unauthorized_client", "invalid_scope", ...Grant extends {
|
|
194
194
|
requestErrorCodes: infer ErrorCodes extends readonly string[];
|
|
195
|
-
} ? ErrorCodes : readonly [], "server_error"])[number]]: k_3 } extends infer
|
|
195
|
+
} ? ErrorCodes : readonly [], "server_error"])[number]]: k_3 } extends infer T_10 ? { [k_2 in keyof T_10]: T_10[k_2] } : never>;
|
|
196
196
|
error_description: z.ZodString;
|
|
197
197
|
}, z.core.$strip>;
|
|
198
198
|
onValidationError: ({
|
|
@@ -10,7 +10,6 @@ import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
|
10
10
|
import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
|
|
11
11
|
import { splitAtLastColon, toOTPIdentifier } from "./utils.mjs";
|
|
12
12
|
import { storeOTP, tryReuseOTP, verifyStoredOTP } from "./otp-token.mjs";
|
|
13
|
-
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
14
13
|
import { BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
15
14
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
16
15
|
import { deprecate } from "@better-auth/core/utils/deprecate";
|
|
@@ -593,7 +592,6 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
|
|
|
593
592
|
if (!await ctx.context.internalAdapter.findCredentialAccount(user.user.id)) await ctx.context.internalAdapter.createAccount({
|
|
594
593
|
userId: user.user.id,
|
|
595
594
|
providerId: "credential",
|
|
596
|
-
issuer: createLocalAccountIssuer("credential"),
|
|
597
595
|
accountId: user.user.id,
|
|
598
596
|
password: passwordHash
|
|
599
597
|
});
|
|
@@ -93,7 +93,6 @@ const genericOAuth = (options) => {
|
|
|
93
93
|
return null;
|
|
94
94
|
});
|
|
95
95
|
if (discovered) {
|
|
96
|
-
if (!discovered.issuer && !c.accountIssuer) throw new Error(`Provider "${c.providerId}": discovery did not return an issuer. Configure accountIssuer explicitly to establish a stable account namespace.`);
|
|
97
96
|
authorizationUrl ??= discovered.authorization_endpoint;
|
|
98
97
|
tokenUrl ??= discovered.token_endpoint;
|
|
99
98
|
userInfoUrl ??= discovered.userinfo_endpoint;
|
|
@@ -106,7 +105,8 @@ const genericOAuth = (options) => {
|
|
|
106
105
|
try {
|
|
107
106
|
jwksUrl = new URL(discovered.jwks_uri, c.discoveryUrl);
|
|
108
107
|
} catch {
|
|
109
|
-
|
|
108
|
+
ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
|
|
109
|
+
continue;
|
|
110
110
|
}
|
|
111
111
|
idTokenConfig = {
|
|
112
112
|
jwks: createRemoteJWKSet(jwksUrl),
|
|
@@ -115,16 +115,24 @@ const genericOAuth = (options) => {
|
|
|
115
115
|
algorithms: isOidc ? signingAlgs : void 0
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
-
}
|
|
119
|
-
|
|
118
|
+
}
|
|
119
|
+
if (!authorizationUrl || !tokenUrl && !c.getToken) {
|
|
120
|
+
ctx.logger.error(`Provider "${c.providerId}": discovery left no usable authorization endpoint or token exchange. Provider skipped.`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (c.requireIdTokenVerification && !idTokenConfig) {
|
|
125
|
+
if (c.discoveryUrl) {
|
|
126
|
+
ctx.logger.error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri. Provider skipped.`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
|
|
120
130
|
}
|
|
121
|
-
if (c.requireIdTokenVerification && !idTokenConfig) throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
|
|
122
131
|
const tokenEndpointAuth = c.tokenEndpointAuth;
|
|
123
132
|
if (c.clientSecret && isSecretlessTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" cannot be combined with clientSecret`);
|
|
124
133
|
if (!c.clientSecret && isClientSecretTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" requires clientSecret`);
|
|
125
134
|
if (!c.clientSecret && !tokenEndpointAuth && c.authentication === "basic") throw new Error(`Provider "${c.providerId}": authentication "basic" requires clientSecret`);
|
|
126
135
|
const accountSubject = c.accountSubject;
|
|
127
|
-
const accountIssuer = c.accountIssuer;
|
|
128
136
|
const provider = {
|
|
129
137
|
id: c.providerId,
|
|
130
138
|
name: c.name ?? c.providerId,
|
|
@@ -137,10 +145,6 @@ const genericOAuth = (options) => {
|
|
|
137
145
|
});
|
|
138
146
|
return isOidc ? genericProfile.sub ?? "" : genericProfile.id ?? "";
|
|
139
147
|
},
|
|
140
|
-
accountIssuer: typeof accountIssuer === "function" ? ({ tokens, profile }) => accountIssuer({
|
|
141
|
-
tokens,
|
|
142
|
-
profile
|
|
143
|
-
}) : accountIssuer ?? issuer,
|
|
144
148
|
idToken: idTokenConfig,
|
|
145
149
|
requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
|
|
146
150
|
allowIdpInitiated: c.allowIdpInitiated,
|
|
@@ -27,12 +27,10 @@ function auth0(options) {
|
|
|
27
27
|
"profile",
|
|
28
28
|
"email"
|
|
29
29
|
];
|
|
30
|
-
const
|
|
31
|
-
const discoveryUrl = `https://${domain}/.well-known/openid-configuration`;
|
|
30
|
+
const domainUrl = options.domain.startsWith("http://") || options.domain.startsWith("https://") ? options.domain : `https://${options.domain}`;
|
|
32
31
|
return {
|
|
33
32
|
providerId: "auth0",
|
|
34
|
-
|
|
35
|
-
discoveryUrl,
|
|
33
|
+
discoveryUrl: `https://${new URL(domainUrl).host}/.well-known/openid-configuration`,
|
|
36
34
|
clientId: options.clientId,
|
|
37
35
|
clientSecret: options.clientSecret,
|
|
38
36
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
@@ -22,20 +22,17 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
function keycloak(options) {
|
|
25
|
-
const defaultScopes = [
|
|
26
|
-
"openid",
|
|
27
|
-
"profile",
|
|
28
|
-
"email"
|
|
29
|
-
];
|
|
30
|
-
const issuer = options.issuer.replace(/\/$/, "");
|
|
31
25
|
return {
|
|
32
26
|
providerId: "keycloak",
|
|
33
|
-
|
|
34
|
-
discoveryUrl: `${issuer}/.well-known/openid-configuration`,
|
|
27
|
+
discoveryUrl: `${options.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
35
28
|
clientId: options.clientId,
|
|
36
29
|
clientSecret: options.clientSecret,
|
|
37
30
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
38
|
-
scopes: options.scopes ??
|
|
31
|
+
scopes: options.scopes ?? [
|
|
32
|
+
"openid",
|
|
33
|
+
"profile",
|
|
34
|
+
"email"
|
|
35
|
+
],
|
|
39
36
|
redirectURI: options.redirectURI,
|
|
40
37
|
endSessionEndpoint: options.endSessionEndpoint,
|
|
41
38
|
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { decodeJwt } from "jose";
|
|
2
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
+
import { createPlaceholderEmail } from "@better-auth/core/utils/email";
|
|
3
4
|
//#region src/plugins/generic-oauth/providers/microsoft-entra-id.ts
|
|
4
5
|
function getMicrosoftProfileName(profile) {
|
|
5
6
|
return profile.name ?? (`${profile.given_name ?? profile.givenname ?? ""} ${profile.family_name ?? profile.familyname ?? ""}`.trim() || void 0);
|
|
@@ -46,25 +47,34 @@ function microsoftEntraId(options) {
|
|
|
46
47
|
} catch {
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
const oid = typeof tokenProfile.oid === "string" && tokenProfile.oid.trim().length > 0 ? tokenProfile.oid : void 0;
|
|
51
|
+
if (!oid) return null;
|
|
52
|
+
const tokenEmail = tokenProfile.email;
|
|
50
53
|
const tokenUserInfo = {
|
|
51
54
|
...tokenProfile,
|
|
52
55
|
name: getMicrosoftProfileName(tokenProfile),
|
|
53
|
-
email:
|
|
56
|
+
email: tokenEmail ?? createPlaceholderEmail({
|
|
57
|
+
identifier: oid,
|
|
58
|
+
namespace: "microsoft-entra-id"
|
|
59
|
+
}),
|
|
54
60
|
image: tokenProfile.picture,
|
|
55
|
-
emailVerified: tokenProfile.email_verified ?? false
|
|
61
|
+
emailVerified: tokenEmail ? tokenProfile.email_verified ?? false : false
|
|
56
62
|
};
|
|
57
63
|
if (!tokens.accessToken) return tokenUserInfo;
|
|
58
64
|
const { data: profile, error } = await betterFetch(userInfoUrl, { headers: { Authorization: `Bearer ${tokens.accessToken}` } });
|
|
59
65
|
if (error || !profile) return tokenUserInfo;
|
|
60
66
|
if (typeof tokenProfile.sub !== "string" || profile.sub !== tokenProfile.sub) return tokenUserInfo;
|
|
67
|
+
const emailClaim = tokenProfile.email ?? profile.email;
|
|
61
68
|
return {
|
|
62
69
|
...profile,
|
|
63
70
|
...tokenProfile,
|
|
64
71
|
name: getMicrosoftProfileName(tokenProfile) ?? getMicrosoftProfileName(profile),
|
|
65
|
-
email:
|
|
72
|
+
email: emailClaim ?? createPlaceholderEmail({
|
|
73
|
+
identifier: oid,
|
|
74
|
+
namespace: "microsoft-entra-id"
|
|
75
|
+
}),
|
|
66
76
|
image: tokenProfile.picture ?? profile.picture,
|
|
67
|
-
emailVerified: tokenProfile.email_verified ?? profile.email_verified ?? false
|
|
77
|
+
emailVerified: emailClaim != null ? tokenProfile.email_verified ?? profile.email_verified ?? false : false
|
|
68
78
|
};
|
|
69
79
|
};
|
|
70
80
|
return {
|
|
@@ -22,20 +22,17 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
function okta(options) {
|
|
25
|
-
const defaultScopes = [
|
|
26
|
-
"openid",
|
|
27
|
-
"profile",
|
|
28
|
-
"email"
|
|
29
|
-
];
|
|
30
|
-
const issuer = options.issuer.replace(/\/$/, "");
|
|
31
25
|
return {
|
|
32
26
|
providerId: "okta",
|
|
33
|
-
|
|
34
|
-
discoveryUrl: `${issuer}/.well-known/openid-configuration`,
|
|
27
|
+
discoveryUrl: `${options.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
35
28
|
clientId: options.clientId,
|
|
36
29
|
clientSecret: options.clientSecret,
|
|
37
30
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
38
|
-
scopes: options.scopes ??
|
|
31
|
+
scopes: options.scopes ?? [
|
|
32
|
+
"openid",
|
|
33
|
+
"profile",
|
|
34
|
+
"email"
|
|
35
|
+
],
|
|
39
36
|
redirectURI: options.redirectURI,
|
|
40
37
|
endSessionEndpoint: options.endSessionEndpoint,
|
|
41
38
|
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
@@ -41,7 +41,6 @@ function slack(options) {
|
|
|
41
41
|
return {
|
|
42
42
|
providerId: "slack",
|
|
43
43
|
accountSubject: ({ profile }) => profile.sub ?? "",
|
|
44
|
-
accountIssuer: "https://slack.com",
|
|
45
44
|
authorizationUrl: "https://slack.com/openid/connect/authorize",
|
|
46
45
|
tokenUrl: "https://slack.com/api/openid.connect.token",
|
|
47
46
|
userInfoUrl: "https://slack.com/api/openid.connect.userInfo",
|