better-auth 1.7.2 → 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/routes/account.d.mts +1 -9
- package/dist/api/routes/account.mjs +0 -6
- package/dist/api/routes/callback.mjs +0 -2
- 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 -5
- 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 +6 -4
- package/dist/client/config.d.mts +2 -0
- package/dist/client/config.mjs +1 -0
- package/dist/client/vue/index.d.mts +31 -20
- package/dist/client/vue/index.mjs +23 -5
- package/dist/context/create-context.mjs +2 -0
- 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 +10 -32
- package/dist/db/internal-adapter.mjs +14 -17
- package/dist/db/schema.d.mts +0 -1
- package/dist/index.mjs +0 -1
- package/dist/oauth2/account-key.mjs +1 -5
- package/dist/oauth2/link-account.d.mts +0 -1
- package/dist/oauth2/link-account.mjs +5 -11
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +0 -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/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 +31 -11
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.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/open-api/index.mjs +0 -1
- package/dist/plugins/organization/has-permission.mjs +4 -2
- package/dist/plugins/phone-number/routes.mjs +0 -3
- package/dist/plugins/siwe/index.mjs +0 -3
- 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/test-instance.mjs +10 -9
- package/package.json +9 -9
- package/dist/utils/index.mjs +0 -5
|
@@ -6,18 +6,36 @@ import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
|
|
|
6
6
|
function getAtomKey(str) {
|
|
7
7
|
return `use${capitalizeFirstLetter(str)}`;
|
|
8
8
|
}
|
|
9
|
+
/** Preserves standard `HeadersInit` values and removes undefined record entries. */
|
|
10
|
+
function toHeadersInit(headers) {
|
|
11
|
+
if (!headers) return void 0;
|
|
12
|
+
if (headers instanceof Headers || Array.isArray(headers)) return headers;
|
|
13
|
+
const normalizedHeaders = {};
|
|
14
|
+
for (const [name, value] of Object.entries(headers)) if (value !== void 0) normalizedHeaders[name] = value;
|
|
15
|
+
return normalizedHeaders;
|
|
16
|
+
}
|
|
9
17
|
function createAuthClient(options) {
|
|
10
|
-
const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $fetch, $store, atomListeners } = getClientConfig(options, false);
|
|
18
|
+
const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $sessionSignal, $fetch, $store, atomListeners } = getClientConfig(options, false);
|
|
19
|
+
const sessionCacheKey = [
|
|
20
|
+
"better-auth",
|
|
21
|
+
"session",
|
|
22
|
+
options?.baseURL || "inferred",
|
|
23
|
+
options?.basePath ?? "/api/auth"
|
|
24
|
+
].join(":");
|
|
11
25
|
const resolvedHooks = {};
|
|
12
26
|
for (const [key, value] of Object.entries(pluginsAtoms)) resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
|
13
27
|
function useSession(useFetch) {
|
|
14
28
|
if (useFetch) {
|
|
15
|
-
const
|
|
16
|
-
return useFetch(`${baseURL}/get-session`, {
|
|
29
|
+
const sessionSignal = useStore($sessionSignal);
|
|
30
|
+
return useFetch(`${baseURL}/get-session`, {
|
|
31
|
+
headers: toHeadersInit(options?.fetchOptions?.headers),
|
|
32
|
+
key: sessionCacheKey,
|
|
33
|
+
watch: [sessionSignal]
|
|
34
|
+
}).then((result) => {
|
|
17
35
|
return {
|
|
18
|
-
data:
|
|
36
|
+
data: result.data,
|
|
19
37
|
isPending: false,
|
|
20
|
-
error:
|
|
38
|
+
error: result.error
|
|
21
39
|
};
|
|
22
40
|
});
|
|
23
41
|
}
|
|
@@ -15,6 +15,7 @@ import { getAuthTables } from "@better-auth/core/db";
|
|
|
15
15
|
import { createLogger, env, isProduction, isTest } from "@better-auth/core/env";
|
|
16
16
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
17
17
|
import { generateId } from "@better-auth/core/utils/id";
|
|
18
|
+
import { schemaCheckFor } from "@better-auth/core/db/internal";
|
|
18
19
|
import { socialProviders } from "@better-auth/core/social-providers";
|
|
19
20
|
import { findInvalidTrustedProxies } from "@better-auth/core/utils/ip";
|
|
20
21
|
import { createTelemetry } from "@better-auth/telemetry";
|
|
@@ -227,6 +228,7 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
227
228
|
};
|
|
228
229
|
const initOrPromise = runPluginInit(ctx);
|
|
229
230
|
if (isPromise(initOrPromise)) await initOrPromise;
|
|
231
|
+
ctx.checkSchema = schemaCheckFor(ctx.adapter);
|
|
230
232
|
return ctx;
|
|
231
233
|
}
|
|
232
234
|
//#endregion
|
|
@@ -26,13 +26,24 @@ const MAX_COOKIE_CHUNKS = 100;
|
|
|
26
26
|
function getMaxCookieValueSize(name, options) {
|
|
27
27
|
return MAX_COOKIE_SIZE - serializeCookie(name, "", { ...options }).length;
|
|
28
28
|
}
|
|
29
|
+
function parseCookieChunkIndex(cookieName, name) {
|
|
30
|
+
const prefix = `${cookieName}.`;
|
|
31
|
+
if (!name.startsWith(prefix)) return null;
|
|
32
|
+
const suffix = name.slice(prefix.length);
|
|
33
|
+
const index = Number(suffix);
|
|
34
|
+
if (!Number.isSafeInteger(index) || index < 0 || String(index) !== suffix) return null;
|
|
35
|
+
return index;
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Read all existing chunks from cookies
|
|
31
39
|
*/
|
|
32
40
|
function readExistingChunks(cookieName, ctx) {
|
|
33
41
|
const chunks = {};
|
|
34
42
|
const cookies = parseCookies(ctx.headers?.get("cookie") || "");
|
|
35
|
-
for (const [name, value] of cookies)
|
|
43
|
+
for (const [name, value] of cookies) {
|
|
44
|
+
if (name !== cookieName && parseCookieChunkIndex(cookieName, name) === null) continue;
|
|
45
|
+
chunks[name] = value;
|
|
46
|
+
}
|
|
36
47
|
return chunks;
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
@@ -128,10 +139,10 @@ function getChunkedCookie(ctx, cookieName) {
|
|
|
128
139
|
const chunks = [];
|
|
129
140
|
const cookieHeader = ctx.headers?.get("cookie");
|
|
130
141
|
if (!cookieHeader) return null;
|
|
131
|
-
for (const [name, val] of parseCookies(cookieHeader))
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
for (const [name, val] of parseCookies(cookieHeader)) {
|
|
143
|
+
const index = parseCookieChunkIndex(cookieName, name);
|
|
144
|
+
if (index === null) continue;
|
|
145
|
+
chunks.push({
|
|
135
146
|
index,
|
|
136
147
|
value: val
|
|
137
148
|
});
|
|
@@ -3,10 +3,9 @@ import { BetterAuthError } from "@better-auth/core/error";
|
|
|
3
3
|
//#region src/db/adapter-kysely.ts
|
|
4
4
|
async function getAdapter(options) {
|
|
5
5
|
return getBaseAdapter(options, async (opts) => {
|
|
6
|
-
const { createKyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
|
|
6
|
+
const { createKyselyAdapter, kyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
|
|
7
7
|
const { kysely, databaseType, transaction } = await createKyselyAdapter(opts);
|
|
8
8
|
if (!kysely) throw new BetterAuthError("Failed to initialize database adapter");
|
|
9
|
-
const { kyselyAdapter } = await import("../adapters/kysely-adapter/index.mjs");
|
|
10
9
|
return kyselyAdapter(kysely, {
|
|
11
10
|
type: databaseType || "sqlite",
|
|
12
11
|
debugLogs: opts.database && "debugLogs" in opts.database ? opts.database.debugLogs : false,
|
|
@@ -2,9 +2,9 @@ import { getSchema } from "./get-schema.mjs";
|
|
|
2
2
|
import { getAuthTables } from "@better-auth/core/db";
|
|
3
3
|
import { createLogger } from "@better-auth/core/env";
|
|
4
4
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
|
-
import { createKyselyAdapter } from "@better-auth/kysely-adapter";
|
|
5
|
+
import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
|
|
6
6
|
import { initGetFieldName, initGetModelName } from "@better-auth/core/db/adapter";
|
|
7
|
-
import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey } from "@better-auth/core/db/internal";
|
|
7
|
+
import { diffSchema, formatSchemaFinding, getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, invalidateSchemaChecks } from "@better-auth/core/db/internal";
|
|
8
8
|
import { sql } from "kysely";
|
|
9
9
|
//#region src/db/get-migration.ts
|
|
10
10
|
const map = {
|
|
@@ -293,7 +293,6 @@ function assertExistingTableIndexFits({ columnBounds, dbType, existingColumns, f
|
|
|
293
293
|
}
|
|
294
294
|
if (requiredBytes > byteBudget) throw new BetterAuthError(`Cannot create database index "${index.name}" on existing table "${table}" because its columns can exceed ${dbType === "mysql" ? "MySQL" : "SQL Server"}'s ${byteBudget}-byte index-key limit. Bound the indexed string columns to the generated schema lengths, resolve oversized values, then run the migration again.`);
|
|
295
295
|
}
|
|
296
|
-
const columnBackfillGuideUrl = "https://better-auth.com/docs/guides/1-7-upgrade-guide#account-identity-is-scoped-by-issuer";
|
|
297
296
|
/**
|
|
298
297
|
* Thrown when {@link getMigrations} refuses to add a required column with no
|
|
299
298
|
* default value to a populated table. Distinct from the plain
|
|
@@ -320,27 +319,6 @@ function matchType(columnDataType, fieldType, dbType) {
|
|
|
320
319
|
return (Array.isArray(fieldType) ? types["string"].map((t) => t.toLowerCase()) : types[fieldType].map((t) => t.toLowerCase())).includes(normalize(columnDataType));
|
|
321
320
|
}
|
|
322
321
|
/**
|
|
323
|
-
* Get the current PostgreSQL schema (search_path) for the database connection
|
|
324
|
-
* Returns the first schema in the search_path, defaulting to 'public' if not found
|
|
325
|
-
*/
|
|
326
|
-
async function getPostgresSchema(db) {
|
|
327
|
-
try {
|
|
328
|
-
const result = await sql`SHOW search_path`.execute(db);
|
|
329
|
-
const searchPath = result.rows[0]?.search_path ?? result.rows[0]?.searchPath;
|
|
330
|
-
if (searchPath) return searchPath.split(",").map((s) => s.trim()).map((s) => s.replace(/^["']|["']$/g, "")).filter((s) => !s.startsWith("$") && !s.startsWith("\\$"))[0] || "public";
|
|
331
|
-
} catch {}
|
|
332
|
-
return "public";
|
|
333
|
-
}
|
|
334
|
-
async function getMssqlSchema(db) {
|
|
335
|
-
try {
|
|
336
|
-
return (await sql`
|
|
337
|
-
SELECT SCHEMA_NAME() AS "schemaName"
|
|
338
|
-
`.execute(db)).rows[0]?.schemaName || "dbo";
|
|
339
|
-
} catch {
|
|
340
|
-
return "dbo";
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
322
|
* Build the migration plan that `auth migrate` executes and `auth generate`
|
|
345
323
|
* prints for the Kysely adapter.
|
|
346
324
|
*
|
|
@@ -357,11 +335,6 @@ async function getMssqlSchema(db) {
|
|
|
357
335
|
async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
358
336
|
const betterAuthSchema = getSchema(config);
|
|
359
337
|
const authTables = getAuthTables(config);
|
|
360
|
-
const accountIssuer = authTables.account && {
|
|
361
|
-
table: authTables.account.modelName,
|
|
362
|
-
column: authTables.account.fields.issuer?.fieldName || "issuer"
|
|
363
|
-
};
|
|
364
|
-
const isAccountIssuerColumn = (table, column) => table === accountIssuer?.table && column === accountIssuer.column;
|
|
365
338
|
const logger = createLogger(config.logger);
|
|
366
339
|
const unsafeChanges = [];
|
|
367
340
|
const reportUnsafeChange = (message) => {
|
|
@@ -410,6 +383,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
410
383
|
logger.warn(`Could not filter tables by schema. Using all discovered tables. Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
411
384
|
}
|
|
412
385
|
else if (dbType === "mssql") tableMetadata = allTableMetadata.filter((table) => table.schema === currentSchema);
|
|
386
|
+
const schemaProblems = diffSchema(toPhysicalSchema(db, betterAuthSchema), toIntrospectedTables(tableMetadata)).filter((finding) => finding.kind === "unexpected-required-column").map((finding) => formatSchemaFinding(finding, "database"));
|
|
413
387
|
const toBeCreated = [];
|
|
414
388
|
const toBeAdded = [];
|
|
415
389
|
const toBeAddedIndexes = [];
|
|
@@ -602,8 +576,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
602
576
|
}
|
|
603
577
|
if (populated) {
|
|
604
578
|
const textDetail = field.type === "string" ? " For a text column, every existing row ends up with the same empty string." : "";
|
|
605
|
-
|
|
606
|
-
reportUnsafeChange(`Cannot add required column "${fieldName}" to populated table "${table.table}": the schema declares no default value, so existing rows have no value to backfill. MySQL accepts this statement instead of rejecting it and fills every existing row with an implicit default for the column type, reporting a successful migration over corrupted data.${textDetail} Add the column as nullable, backfill a correct value for every row, then make it NOT NULL.${guideLink}`);
|
|
579
|
+
reportUnsafeChange(`Cannot add required column "${fieldName}" to populated table "${table.table}": the schema declares no default value, so existing rows have no value to backfill. MySQL accepts this statement instead of rejecting it and fills every existing row with an implicit default for the column type, reporting a successful migration over corrupted data.${textDetail} Add the column as nullable, backfill a correct value for every row, then make it NOT NULL.`);
|
|
607
580
|
}
|
|
608
581
|
}
|
|
609
582
|
const type = getType(field, fieldName, getTableIndexStringLength(table.table, fieldName));
|
|
@@ -668,7 +641,11 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
668
641
|
}
|
|
669
642
|
for (const index of deferredIndexes) migrations.push(index);
|
|
670
643
|
async function runMigrations() {
|
|
671
|
-
|
|
644
|
+
try {
|
|
645
|
+
for (const migration of migrations) await migration.execute();
|
|
646
|
+
} finally {
|
|
647
|
+
if (migrations.length && config.database) invalidateSchemaChecks(config.database);
|
|
648
|
+
}
|
|
672
649
|
}
|
|
673
650
|
async function compileMigrations() {
|
|
674
651
|
return migrations.map((m) => m.compile().sql).join(";\n\n") + ";";
|
|
@@ -678,6 +655,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
678
655
|
toBeAdded,
|
|
679
656
|
toBeAddedIndexes,
|
|
680
657
|
unsafeChanges,
|
|
658
|
+
schemaProblems,
|
|
681
659
|
runMigrations,
|
|
682
660
|
compileMigrations
|
|
683
661
|
};
|
|
@@ -4,7 +4,6 @@ import { assertValidUserInfo, assertValidUserInfoSource } from "../utils/validat
|
|
|
4
4
|
import { getStorageOption, processIdentifier } from "./verification-token-storage.mjs";
|
|
5
5
|
import { getWithHooks } from "./with-hooks.mjs";
|
|
6
6
|
import { getCurrentAdapter, getCurrentAuthEndpointContext, queueAfterTransactionHook, runWithTransaction, tryGetCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
7
|
-
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
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";
|
|
@@ -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/index.mjs
CHANGED
|
@@ -2,7 +2,6 @@ import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProt
|
|
|
2
2
|
import { generateGenericState, parseGenericState } from "./state.mjs";
|
|
3
3
|
import { generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
|
|
4
4
|
import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
|
|
5
|
-
import "./utils/index.mjs";
|
|
6
5
|
import { APIError } from "./api/index.mjs";
|
|
7
6
|
import { betterAuth } from "./auth/full.mjs";
|
|
8
7
|
import { getCurrentAdapter } from "@better-auth/core/context";
|
|
@@ -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
|
}
|
|
@@ -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/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
|
|
@@ -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
|
});
|
|
@@ -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,
|