better-auth 1.6.15 → 1.6.16
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/middlewares/origin-check.mjs +1 -0
- package/dist/api/routes/account.mjs +11 -6
- package/dist/api/routes/callback.mjs +1 -1
- package/dist/api/routes/session.mjs +3 -1
- package/dist/api/routes/update-session.mjs +9 -3
- package/dist/client/lynx/index.d.mts +6 -5
- package/dist/client/react/index.d.mts +6 -5
- package/dist/client/solid/index.d.mts +6 -5
- package/dist/client/svelte/index.d.mts +6 -5
- package/dist/client/vanilla.d.mts +6 -5
- package/dist/client/vue/index.d.mts +6 -5
- package/dist/context/create-context.mjs +1 -1
- package/dist/cookies/index.mjs +6 -1
- package/dist/db/internal-adapter.mjs +5 -0
- package/dist/oauth2/link-account.d.mts +13 -0
- package/dist/oauth2/link-account.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -3
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +63 -2
- package/dist/plugins/admin/schema.d.mts +1 -0
- package/dist/plugins/admin/schema.mjs +2 -1
- package/dist/plugins/email-otp/routes.mjs +1 -1
- package/dist/plugins/generic-oauth/routes.mjs +9 -2
- package/dist/plugins/organization/organization.mjs +2 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +10 -1
- package/dist/plugins/organization/routes/crud-team.mjs +15 -2
- package/dist/plugins/organization/schema.d.mts +2 -0
- package/dist/plugins/siwe/index.mjs +28 -0
- package/dist/plugins/siwe/parse-message.mjs +60 -0
- package/dist/plugins/two-factor/index.mjs +9 -1
- package/dist/test-utils/test-instance.d.mts +6 -5
- package/package.json +10 -10
|
@@ -141,6 +141,7 @@ async function validateFormCsrf(ctx) {
|
|
|
141
141
|
}
|
|
142
142
|
return await validateOrigin(ctx, true);
|
|
143
143
|
}
|
|
144
|
+
if (headers.get("origin") || headers.get("referer")) return await validateOrigin(ctx, true);
|
|
144
145
|
}
|
|
145
146
|
//#endregion
|
|
146
147
|
export { formCsrfMiddleware, originCheck, originCheckMiddleware };
|
|
@@ -225,9 +225,15 @@ const unlinkAccount = createAuthEndpoint("/unlink-account", {
|
|
|
225
225
|
* `userId` directly. Throws `UNAUTHORIZED` when an HTTP caller is
|
|
226
226
|
* unauthenticated, and `USER_ID_OR_SESSION_REQUIRED` when neither a session
|
|
227
227
|
* nor a `userId` is available.
|
|
228
|
+
*
|
|
229
|
+
* When a durable store is authoritative, bypasses the cookie cache: these
|
|
230
|
+
* routes mint or refresh provider access tokens, so a server-side session
|
|
231
|
+
* revocation must take effect immediately rather than waiting for the cached
|
|
232
|
+
* cookie to expire. DB-less deployments keep the session in the cookie itself,
|
|
233
|
+
* so the cache is left in place for them.
|
|
228
234
|
*/
|
|
229
235
|
async function resolveUserId(ctx, userId) {
|
|
230
|
-
const session = await getSessionFromCtx(ctx);
|
|
236
|
+
const session = await getSessionFromCtx(ctx, { disableCookieCache: !!ctx.context.options.database || !!ctx.context.options.secondaryStorage });
|
|
231
237
|
if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
|
|
232
238
|
const resolvedUserId = session?.user?.id || userId;
|
|
233
239
|
if (!resolvedUserId) throw APIError.from("BAD_REQUEST", {
|
|
@@ -382,12 +388,11 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
|
382
388
|
});
|
|
383
389
|
let account = void 0;
|
|
384
390
|
const accountData = await getAccountCookie(ctx);
|
|
385
|
-
|
|
391
|
+
const usedAccountCookie = !!accountData && accountData.userId === resolvedUserId && providerId === accountData.providerId && (!accountId || accountData.accountId === accountId);
|
|
392
|
+
if (usedAccountCookie) account = accountData;
|
|
386
393
|
else account = (await ctx.context.internalAdapter.findAccounts(resolvedUserId)).find((acc) => accountId ? acc.accountId === accountId && acc.providerId === providerId : acc.providerId === providerId);
|
|
387
394
|
if (!account) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
388
|
-
|
|
389
|
-
if (accountData && providerId === accountData.providerId) refreshToken = accountData.refreshToken ?? void 0;
|
|
390
|
-
else refreshToken = account.refreshToken ?? void 0;
|
|
395
|
+
const refreshToken = account.refreshToken ?? void 0;
|
|
391
396
|
if (!refreshToken) throw APIError.from("BAD_REQUEST", {
|
|
392
397
|
message: "Refresh token not found",
|
|
393
398
|
code: "REFRESH_TOKEN_NOT_FOUND"
|
|
@@ -409,7 +414,7 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
|
|
|
409
414
|
};
|
|
410
415
|
await ctx.context.internalAdapter.updateAccount(account.id, updateData);
|
|
411
416
|
}
|
|
412
|
-
if (
|
|
417
|
+
if (usedAccountCookie && ctx.context.options.account?.storeAccountCookie) await setAccountCookie(ctx, {
|
|
413
418
|
...accountData,
|
|
414
419
|
accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
|
|
415
420
|
refreshToken: resolvedRefreshToken,
|
|
@@ -81,7 +81,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
81
81
|
...tokens,
|
|
82
82
|
user: parsedUserData ?? void 0
|
|
83
83
|
}).then((res) => res?.user);
|
|
84
|
-
if (!userInfo || userInfo.id === void 0 || userInfo.id === null) {
|
|
84
|
+
if (!userInfo || userInfo.id === void 0 || userInfo.id === null || userInfo.id === "") {
|
|
85
85
|
c.context.logger.error("Unable to get user info");
|
|
86
86
|
redirectOnError(c, resolvedErrorURL, "unable_to_get_user_info");
|
|
87
87
|
}
|
|
@@ -276,7 +276,9 @@ const getSessionFromCtx = async (ctx, config) => {
|
|
|
276
276
|
returnStatus: false,
|
|
277
277
|
query: {
|
|
278
278
|
...config,
|
|
279
|
-
...ctx.query
|
|
279
|
+
...ctx.query,
|
|
280
|
+
disableCookieCache: config?.disableCookieCache || ctx.query?.disableCookieCache,
|
|
281
|
+
disableRefresh: config?.disableRefresh || ctx.query?.disableRefresh
|
|
280
282
|
}
|
|
281
283
|
}).catch(() => {
|
|
282
284
|
return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseSessionInput, parseSessionOutput } from "../../db/schema.mjs";
|
|
2
|
-
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
2
|
+
import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
3
3
|
import { sessionMiddleware } from "./session.mjs";
|
|
4
4
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
5
5
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -34,10 +34,16 @@ const updateSession = () => createAuthEndpoint("/update-session", {
|
|
|
34
34
|
const session = ctx.context.session;
|
|
35
35
|
const additionalFields = parseSessionInput(ctx.context.options, body, "update");
|
|
36
36
|
if (Object.keys(additionalFields).length === 0) throw APIError.fromStatus("BAD_REQUEST", { message: "No fields to update" });
|
|
37
|
-
const
|
|
37
|
+
const updatedSession = await ctx.context.internalAdapter.updateSession(session.session.token, {
|
|
38
38
|
...additionalFields,
|
|
39
39
|
updatedAt: /* @__PURE__ */ new Date()
|
|
40
|
-
})
|
|
40
|
+
});
|
|
41
|
+
const isStateful = !!ctx.context.options.database || !!ctx.context.options.secondaryStorage;
|
|
42
|
+
if (!updatedSession && isStateful) {
|
|
43
|
+
deleteSessionCookie(ctx);
|
|
44
|
+
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
|
|
45
|
+
}
|
|
46
|
+
const newSession = updatedSession ?? {
|
|
41
47
|
...session.session,
|
|
42
48
|
...additionalFields,
|
|
43
49
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -69,11 +69,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
69
69
|
priority?: RequestPriority | undefined;
|
|
70
70
|
cache?: RequestCache | undefined;
|
|
71
71
|
credentials?: RequestCredentials;
|
|
72
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
73
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
74
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
75
|
-
authorization: "Bearer" | "Basic";
|
|
76
|
-
})) | undefined;
|
|
77
72
|
integrity?: string | undefined;
|
|
78
73
|
keepalive?: boolean | undefined;
|
|
79
74
|
method: string;
|
|
@@ -103,6 +98,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
103
98
|
prefix: string | (() => string | undefined) | undefined;
|
|
104
99
|
value: string | (() => string | undefined) | undefined;
|
|
105
100
|
}) | undefined;
|
|
101
|
+
headers?: {} | {
|
|
102
|
+
[x: string]: string | undefined;
|
|
103
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
104
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
105
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
106
|
+
} | undefined;
|
|
106
107
|
body?: any;
|
|
107
108
|
query?: any;
|
|
108
109
|
params?: any;
|
|
@@ -70,11 +70,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
70
70
|
priority?: RequestPriority | undefined;
|
|
71
71
|
cache?: RequestCache | undefined;
|
|
72
72
|
credentials?: RequestCredentials;
|
|
73
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
74
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
75
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
76
|
-
authorization: "Bearer" | "Basic";
|
|
77
|
-
})) | undefined;
|
|
78
73
|
integrity?: string | undefined;
|
|
79
74
|
keepalive?: boolean | undefined;
|
|
80
75
|
method: string;
|
|
@@ -104,6 +99,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
104
99
|
prefix: string | (() => string | undefined) | undefined;
|
|
105
100
|
value: string | (() => string | undefined) | undefined;
|
|
106
101
|
}) | undefined;
|
|
102
|
+
headers?: {} | {
|
|
103
|
+
[x: string]: string | undefined;
|
|
104
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
105
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
106
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
107
|
+
} | undefined;
|
|
107
108
|
body?: any;
|
|
108
109
|
query?: any;
|
|
109
110
|
params?: any;
|
|
@@ -69,11 +69,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
69
69
|
priority?: RequestPriority | undefined;
|
|
70
70
|
cache?: RequestCache | undefined;
|
|
71
71
|
credentials?: RequestCredentials;
|
|
72
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
73
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
74
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
75
|
-
authorization: "Bearer" | "Basic";
|
|
76
|
-
})) | undefined;
|
|
77
72
|
integrity?: string | undefined;
|
|
78
73
|
keepalive?: boolean | undefined;
|
|
79
74
|
method: string;
|
|
@@ -103,6 +98,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
103
98
|
prefix: string | (() => string | undefined) | undefined;
|
|
104
99
|
value: string | (() => string | undefined) | undefined;
|
|
105
100
|
}) | undefined;
|
|
101
|
+
headers?: {} | {
|
|
102
|
+
[x: string]: string | undefined;
|
|
103
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
104
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
105
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
106
|
+
} | undefined;
|
|
106
107
|
body?: any;
|
|
107
108
|
query?: any;
|
|
108
109
|
params?: any;
|
|
@@ -55,11 +55,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
55
55
|
priority?: RequestPriority | undefined;
|
|
56
56
|
cache?: RequestCache | undefined;
|
|
57
57
|
credentials?: RequestCredentials;
|
|
58
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
59
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
60
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
61
|
-
authorization: "Bearer" | "Basic";
|
|
62
|
-
})) | undefined;
|
|
63
58
|
integrity?: string | undefined;
|
|
64
59
|
keepalive?: boolean | undefined;
|
|
65
60
|
method: string;
|
|
@@ -89,6 +84,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
89
84
|
prefix: string | (() => string | undefined) | undefined;
|
|
90
85
|
value: string | (() => string | undefined) | undefined;
|
|
91
86
|
}) | undefined;
|
|
87
|
+
headers?: {} | {
|
|
88
|
+
[x: string]: string | undefined;
|
|
89
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
90
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
91
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
92
|
+
} | undefined;
|
|
92
93
|
body?: any;
|
|
93
94
|
query?: any;
|
|
94
95
|
params?: any;
|
|
@@ -53,11 +53,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
53
53
|
priority?: RequestPriority | undefined;
|
|
54
54
|
cache?: RequestCache | undefined;
|
|
55
55
|
credentials?: RequestCredentials;
|
|
56
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
57
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
58
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
59
|
-
authorization: "Bearer" | "Basic";
|
|
60
|
-
})) | undefined;
|
|
61
56
|
integrity?: string | undefined;
|
|
62
57
|
keepalive?: boolean | undefined;
|
|
63
58
|
method: string;
|
|
@@ -87,6 +82,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
87
82
|
prefix: string | (() => string | undefined) | undefined;
|
|
88
83
|
value: string | (() => string | undefined) | undefined;
|
|
89
84
|
}) | undefined;
|
|
85
|
+
headers?: {} | {
|
|
86
|
+
[x: string]: string | undefined;
|
|
87
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
88
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
89
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
90
|
+
} | undefined;
|
|
90
91
|
body?: any;
|
|
91
92
|
query?: any;
|
|
92
93
|
params?: any;
|
|
@@ -93,11 +93,6 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
93
93
|
priority?: RequestPriority | undefined;
|
|
94
94
|
cache?: RequestCache | undefined;
|
|
95
95
|
credentials?: RequestCredentials;
|
|
96
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
97
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
98
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
99
|
-
authorization: "Bearer" | "Basic";
|
|
100
|
-
})) | undefined;
|
|
101
96
|
integrity?: string | undefined;
|
|
102
97
|
keepalive?: boolean | undefined;
|
|
103
98
|
method: string;
|
|
@@ -127,6 +122,12 @@ declare function createAuthClient<Option extends BetterAuthClientOptions>(option
|
|
|
127
122
|
prefix: string | (() => string | undefined) | undefined;
|
|
128
123
|
value: string | (() => string | undefined) | undefined;
|
|
129
124
|
}) | undefined;
|
|
125
|
+
headers?: {} | {
|
|
126
|
+
[x: string]: string | undefined;
|
|
127
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
128
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
129
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
130
|
+
} | undefined;
|
|
130
131
|
body?: any;
|
|
131
132
|
query?: any;
|
|
132
133
|
params?: any;
|
|
@@ -59,7 +59,7 @@ async function createAuthContext(adapter, options, getDatabaseType) {
|
|
|
59
59
|
if (!allowedHosts || allowedHosts.length === 0) throw new BetterAuthError("baseURL.allowedHosts cannot be empty. Provide at least one allowed host pattern (e.g., [\"myapp.com\", \"*.vercel.app\"]).");
|
|
60
60
|
}
|
|
61
61
|
const baseURL = isDynamicConfig ? void 0 : getBaseURL(typeof options.baseURL === "string" ? options.baseURL : void 0, options.basePath);
|
|
62
|
-
if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL
|
|
62
|
+
if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL is not set. Set the baseURL option or BETTER_AUTH_URL env, or use a dynamic baseURL with allowedHosts for multi-host setups. Without it the origin is derived from the incoming request, and callbacks and redirects may not work correctly.`);
|
|
63
63
|
if (adapter.id === "memory" && options.advanced?.database?.generateId === false) logger.error(`[better-auth] Misconfiguration detected.
|
|
64
64
|
You are using the memory DB with generateId: false.
|
|
65
65
|
This will cause no id to be generated for any model.
|
package/dist/cookies/index.mjs
CHANGED
|
@@ -116,7 +116,12 @@ async function setCookieCache(ctx, session, dontRememberMe) {
|
|
|
116
116
|
}
|
|
117
117
|
if (ctx.context.options.account?.storeAccountCookie) {
|
|
118
118
|
const accountData = await getAccountCookie(ctx);
|
|
119
|
-
if (accountData) await setAccountCookie(ctx, accountData);
|
|
119
|
+
if (accountData) if (accountData.userId === session.user.id) await setAccountCookie(ctx, accountData);
|
|
120
|
+
else {
|
|
121
|
+
expireCookie(ctx, ctx.context.authCookies.accountData);
|
|
122
|
+
const accountStore = createAccountStore(ctx.context.authCookies.accountData.name, ctx.context.authCookies.accountData.attributes, ctx);
|
|
123
|
+
accountStore.setCookies(accountStore.clean());
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
|
|
@@ -16,6 +16,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
16
16
|
const options = ctx.options;
|
|
17
17
|
const secondaryStorage = options.secondaryStorage;
|
|
18
18
|
const verificationConsumeLocks = /* @__PURE__ */ new Map();
|
|
19
|
+
let warnedNonAtomicConsume = false;
|
|
19
20
|
const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7;
|
|
20
21
|
const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks } = getWithHooks(adapter, ctx);
|
|
21
22
|
async function refreshUserSessions(user) {
|
|
@@ -653,6 +654,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
653
654
|
if (secondaryStorage && !options.verification?.storeInDatabase) {
|
|
654
655
|
const consumeCacheKey = async (key) => {
|
|
655
656
|
if (secondaryStorage.getAndDelete) return hydrateCachedVerification(await secondaryStorage.getAndDelete(key));
|
|
657
|
+
if (!warnedNonAtomicConsume) {
|
|
658
|
+
warnedNonAtomicConsume = true;
|
|
659
|
+
logger.warn("Secondary storage does not implement `getAndDelete`, so single-use verification values cannot be consumed atomically across processes. Implement `getAndDelete` or use database-backed verification storage to guarantee single use.");
|
|
660
|
+
}
|
|
656
661
|
return withVerificationConsumeLock(key, async () => {
|
|
657
662
|
const parsed = hydrateCachedVerification(await secondaryStorage.get(key));
|
|
658
663
|
if (!parsed) return null;
|
|
@@ -9,6 +9,19 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
|
9
9
|
disableSignUp?: boolean | undefined;
|
|
10
10
|
overrideUserInfo?: boolean | undefined;
|
|
11
11
|
isTrustedProvider?: boolean | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Whether `account.providerId` may be matched against the globally
|
|
14
|
+
* configured `accountLinking.trustedProviders` list to infer trust.
|
|
15
|
+
*
|
|
16
|
+
* Defaults to `true` for built-in social/OAuth providers, whose
|
|
17
|
+
* `providerId` namespace is controlled by the developer's config. Callers
|
|
18
|
+
* whose `providerId` is user-controlled (e.g. the SSO plugin, where any
|
|
19
|
+
* authenticated user can register a provider with an arbitrary id) must
|
|
20
|
+
* pass `false` so a provider named after a trusted social provider can't
|
|
21
|
+
* launder that trust. Such callers should supply their own
|
|
22
|
+
* `isTrustedProvider` signal instead.
|
|
23
|
+
*/
|
|
24
|
+
trustProviderByName?: boolean | undefined;
|
|
12
25
|
}): Promise<{
|
|
13
26
|
error: string;
|
|
14
27
|
data: null;
|
|
@@ -17,7 +17,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
17
17
|
const linkedAccount = dbUser.linkedAccount ?? dbUser.accounts.find((acc) => acc.providerId === account.providerId && acc.accountId === account.accountId);
|
|
18
18
|
if (!linkedAccount) {
|
|
19
19
|
const accountLinking = c.context.options.account?.accountLinking;
|
|
20
|
-
const isTrustedProvider = opts.isTrustedProvider || c.context.trustedProviders.includes(account.providerId);
|
|
20
|
+
const isTrustedProvider = opts.isTrustedProvider || opts.trustProviderByName !== false && c.context.trustedProviders.includes(account.providerId);
|
|
21
21
|
const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
|
|
22
22
|
if (!isTrustedProvider && !userInfo.emailVerified || requireLocalEmailVerified && !dbUser.user.emailVerified || accountLinking?.enabled === false || accountLinking?.disableImplicitLinking === true) {
|
|
23
23
|
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.`);
|
package/dist/package.mjs
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
import { ExactRoleStatements, Role, RoleInput, Statements } from "../../access/types.mjs";
|
|
2
2
|
//#region src/plugins/admin/access/statement.d.ts
|
|
3
3
|
declare const defaultStatements: {
|
|
4
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
4
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
5
5
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
6
6
|
};
|
|
7
7
|
declare const defaultAc: {
|
|
8
8
|
newRole<const TRoleStatements extends Statements>(statements: RoleInput<{
|
|
9
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
9
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
10
10
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
11
11
|
}, TRoleStatements>): Role<ExactRoleStatements<TRoleStatements>, {
|
|
12
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
12
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
13
13
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
14
14
|
}>;
|
|
15
15
|
statements: {
|
|
16
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
16
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
17
17
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
declare const adminAc: Role<ExactRoleStatements<{
|
|
21
|
-
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
|
|
21
|
+
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
|
|
22
22
|
readonly session: ["list", "revoke", "delete"];
|
|
23
23
|
}>, {
|
|
24
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
24
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
25
25
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
26
26
|
}>;
|
|
27
27
|
declare const userAc: Role<ExactRoleStatements<{
|
|
28
28
|
readonly user: [];
|
|
29
29
|
readonly session: [];
|
|
30
30
|
}>, {
|
|
31
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
31
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
32
32
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
33
33
|
}>;
|
|
34
34
|
declare const defaultRoles: {
|
|
35
35
|
admin: Role<ExactRoleStatements<{
|
|
36
|
-
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"];
|
|
36
|
+
readonly user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"];
|
|
37
37
|
readonly session: ["list", "revoke", "delete"];
|
|
38
38
|
}>, {
|
|
39
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
39
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
40
40
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
41
41
|
}>;
|
|
42
42
|
user: Role<ExactRoleStatements<{
|
|
43
43
|
readonly user: [];
|
|
44
44
|
readonly session: [];
|
|
45
45
|
}>, {
|
|
46
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
46
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
47
47
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
48
48
|
}>;
|
|
49
49
|
};
|
|
@@ -10,6 +10,7 @@ const defaultStatements = {
|
|
|
10
10
|
"impersonate-admins",
|
|
11
11
|
"delete",
|
|
12
12
|
"set-password",
|
|
13
|
+
"set-email",
|
|
13
14
|
"get",
|
|
14
15
|
"update"
|
|
15
16
|
],
|
|
@@ -29,6 +30,7 @@ const adminAc = defaultAc.newRole({
|
|
|
29
30
|
"impersonate",
|
|
30
31
|
"delete",
|
|
31
32
|
"set-password",
|
|
33
|
+
"set-email",
|
|
32
34
|
"get",
|
|
33
35
|
"update"
|
|
34
36
|
],
|
|
@@ -824,13 +824,13 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
824
824
|
$Infer: {
|
|
825
825
|
body: {
|
|
826
826
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
827
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
827
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
828
828
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
829
829
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
830
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
830
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
831
831
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
832
832
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
833
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
833
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
834
834
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
835
835
|
})[key]> : never)[] | undefined };
|
|
836
836
|
} & {
|
|
@@ -866,6 +866,8 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
866
866
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
867
867
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
868
868
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
869
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
870
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
869
871
|
};
|
|
870
872
|
schema: {
|
|
871
873
|
user: {
|
|
@@ -898,6 +900,7 @@ declare const admin: <O extends AdminOptions>(options?: O | undefined) => {
|
|
|
898
900
|
impersonatedBy: {
|
|
899
901
|
type: "string";
|
|
900
902
|
required: false;
|
|
903
|
+
input: false;
|
|
901
904
|
};
|
|
902
905
|
};
|
|
903
906
|
};
|
|
@@ -14,7 +14,7 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
14
14
|
version: string;
|
|
15
15
|
$InferServerPlugin: ReturnType<typeof admin<{
|
|
16
16
|
ac: O["ac"] extends AccessControl ? O["ac"] : AccessControl<{
|
|
17
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
17
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
18
18
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
19
19
|
}>;
|
|
20
20
|
roles: O["roles"] extends Record<string, Role> ? O["roles"] : {
|
|
@@ -28,13 +28,13 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
28
28
|
roles: any;
|
|
29
29
|
} ? keyof O["roles"] : "admin" | "user")>(data: {
|
|
30
30
|
permissions: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
31
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
31
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
32
32
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
33
33
|
})]?: ((O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
34
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
34
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
35
35
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
36
36
|
})[key] extends readonly unknown[] ? ArrayElement<(O["ac"] extends AccessControl<infer S extends Statements> ? S : {
|
|
37
|
-
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"];
|
|
37
|
+
readonly user: readonly ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"];
|
|
38
38
|
readonly session: readonly ["list", "revoke", "delete"];
|
|
39
39
|
})[key]> : never)[] | undefined };
|
|
40
40
|
} & {
|
|
@@ -73,6 +73,8 @@ declare const adminClient: <O extends AdminClientOptions>(options?: O | undefine
|
|
|
73
73
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
74
74
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
75
75
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
76
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
77
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
76
78
|
};
|
|
77
79
|
};
|
|
78
80
|
//#endregion
|
|
@@ -23,6 +23,8 @@ declare const ADMIN_ERROR_CODES: {
|
|
|
23
23
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE">;
|
|
24
24
|
YOU_CANNOT_IMPERSONATE_ADMINS: _better_auth_core_utils_error_codes0.RawError<"YOU_CANNOT_IMPERSONATE_ADMINS">;
|
|
25
25
|
INVALID_ROLE_TYPE: _better_auth_core_utils_error_codes0.RawError<"INVALID_ROLE_TYPE">;
|
|
26
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: _better_auth_core_utils_error_codes0.RawError<"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL">;
|
|
27
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: _better_auth_core_utils_error_codes0.RawError<"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER">;
|
|
26
28
|
};
|
|
27
29
|
//#endregion
|
|
28
30
|
export { ADMIN_ERROR_CODES };
|
|
@@ -21,7 +21,9 @@ const ADMIN_ERROR_CODES = defineErrorCodes({
|
|
|
21
21
|
YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself",
|
|
22
22
|
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: "You are not allowed to set a non-existent role value",
|
|
23
23
|
YOU_CANNOT_IMPERSONATE_ADMINS: "You cannot impersonate admins",
|
|
24
|
-
INVALID_ROLE_TYPE: "Invalid role type"
|
|
24
|
+
INVALID_ROLE_TYPE: "Invalid role type",
|
|
25
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL: "You are not allowed to update users email",
|
|
26
|
+
PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER: "Password cannot be updated through update-user. Use the set-user-password endpoint instead"
|
|
25
27
|
});
|
|
26
28
|
//#endregion
|
|
27
29
|
export { ADMIN_ERROR_CODES };
|
|
@@ -156,14 +156,43 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
|
156
156
|
permissions: { user: ["create"] }
|
|
157
157
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS);
|
|
158
158
|
}
|
|
159
|
+
const { role: dataRole, ...userData } = ctx.body.data ?? {};
|
|
160
|
+
const requestedRole = ctx.body.role ?? dataRole;
|
|
161
|
+
if (requestedRole !== void 0) {
|
|
162
|
+
if (session) {
|
|
163
|
+
if (!hasPermission({
|
|
164
|
+
userId: session.user.id,
|
|
165
|
+
role: session.user.role,
|
|
166
|
+
options: opts,
|
|
167
|
+
permissions: { user: ["set-role"] }
|
|
168
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE);
|
|
169
|
+
}
|
|
170
|
+
const inputRoles = Array.isArray(requestedRole) ? requestedRole : [requestedRole];
|
|
171
|
+
for (const role of inputRoles) {
|
|
172
|
+
if (typeof role !== "string") throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.INVALID_ROLE_TYPE);
|
|
173
|
+
if (opts.roles && !opts.roles[role]) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (session && [
|
|
177
|
+
"banned",
|
|
178
|
+
"banReason",
|
|
179
|
+
"banExpires"
|
|
180
|
+
].some((key) => Object.prototype.hasOwnProperty.call(userData, key))) {
|
|
181
|
+
if (!hasPermission({
|
|
182
|
+
userId: session.user.id,
|
|
183
|
+
role: session.user.role,
|
|
184
|
+
options: opts,
|
|
185
|
+
permissions: { user: ["ban"] }
|
|
186
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
187
|
+
}
|
|
159
188
|
const email = ctx.body.email.toLowerCase();
|
|
160
189
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
161
190
|
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
162
191
|
const user = await ctx.context.internalAdapter.createUser({
|
|
192
|
+
...userData,
|
|
163
193
|
email,
|
|
164
194
|
name: ctx.body.name,
|
|
165
|
-
role:
|
|
166
|
-
...ctx.body.data
|
|
195
|
+
role: requestedRole !== void 0 ? parseRoles(requestedRole) : opts?.defaultRole ?? "user"
|
|
167
196
|
});
|
|
168
197
|
if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER);
|
|
169
198
|
if (ctx.body.password) {
|
|
@@ -220,6 +249,9 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
220
249
|
permissions: { user: ["update"] }
|
|
221
250
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS);
|
|
222
251
|
if (Object.keys(ctx.body.data).length === 0) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE);
|
|
252
|
+
const updateData = ctx.body.data;
|
|
253
|
+
const hasDataKey = (key) => Object.prototype.hasOwnProperty.call(updateData, key);
|
|
254
|
+
if (hasDataKey("password")) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER);
|
|
223
255
|
if (Object.prototype.hasOwnProperty.call(ctx.body.data, "role")) {
|
|
224
256
|
if (!hasPermission({
|
|
225
257
|
userId: ctx.context.session.user.id,
|
|
@@ -235,8 +267,37 @@ const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
|
235
267
|
}
|
|
236
268
|
ctx.body.data.role = parseRoles(inputRoles);
|
|
237
269
|
}
|
|
270
|
+
if ([
|
|
271
|
+
"banned",
|
|
272
|
+
"banReason",
|
|
273
|
+
"banExpires"
|
|
274
|
+
].some(hasDataKey)) {
|
|
275
|
+
if (!hasPermission({
|
|
276
|
+
userId: ctx.context.session.user.id,
|
|
277
|
+
role: ctx.context.session.user.role,
|
|
278
|
+
options: opts,
|
|
279
|
+
permissions: { user: ["ban"] }
|
|
280
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS);
|
|
281
|
+
if (updateData.banned === true && ctx.body.userId === ctx.context.session.user.id) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF);
|
|
282
|
+
}
|
|
283
|
+
if (hasDataKey("email") || hasDataKey("emailVerified")) {
|
|
284
|
+
if (!hasPermission({
|
|
285
|
+
userId: ctx.context.session.user.id,
|
|
286
|
+
role: ctx.context.session.user.role,
|
|
287
|
+
options: opts,
|
|
288
|
+
permissions: { user: ["set-email"] }
|
|
289
|
+
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL);
|
|
290
|
+
if (hasDataKey("email")) {
|
|
291
|
+
const email = String(updateData.email).toLowerCase();
|
|
292
|
+
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
293
|
+
const existUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
294
|
+
if (existUser && existUser.user.id !== ctx.body.userId) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
295
|
+
updateData.email = email;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
238
298
|
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
239
299
|
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
|
|
300
|
+
if (updateData.banned === true) await ctx.context.internalAdapter.deleteUserSessions(ctx.body.userId);
|
|
240
301
|
return ctx.json(parseUserOutput(ctx.context.options, updatedUser));
|
|
241
302
|
});
|
|
242
303
|
const listUsersQuerySchema = z.object({
|
|
@@ -334,7 +334,7 @@ const verifyEmailOTP = (opts) => createAuthEndpoint("/email-otp/verify-email", {
|
|
|
334
334
|
});
|
|
335
335
|
}
|
|
336
336
|
const currentSession = await getSessionFromCtx(ctx);
|
|
337
|
-
if (currentSession && updatedUser.emailVerified) {
|
|
337
|
+
if (currentSession && updatedUser.emailVerified && currentSession.user.id === updatedUser.id) {
|
|
338
338
|
const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
|
|
339
339
|
await setCookieCache(ctx, {
|
|
340
340
|
session: currentSession.session,
|
|
@@ -209,7 +209,12 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
209
209
|
ctx.context.logger.error(missingEmailLogMessage(providerConfig.providerId, { source: "generic" }), userInfo);
|
|
210
210
|
redirectOnError(ctx, resolvedErrorURL, "email_is_missing");
|
|
211
211
|
}
|
|
212
|
-
const
|
|
212
|
+
const rawId = mapUser.id !== void 0 && mapUser.id !== null && mapUser.id !== "" ? mapUser.id : userInfo.id;
|
|
213
|
+
const id = rawId !== void 0 && rawId !== null ? String(rawId) : "";
|
|
214
|
+
if (!id) {
|
|
215
|
+
ctx.context.logger.error("Provider did not return an account id (e.g. `sub`). Unable to sign in.", userInfo);
|
|
216
|
+
redirectOnError(ctx, resolvedErrorURL, "id_is_missing");
|
|
217
|
+
}
|
|
213
218
|
const name = mapUser.name ? mapUser.name : userInfo.name;
|
|
214
219
|
if (!name) {
|
|
215
220
|
ctx.context.logger.error("Unable to get user info", userInfo);
|
|
@@ -398,8 +403,10 @@ async function getUserInfo(tokens, finalUserInfoUrl) {
|
|
|
398
403
|
method: "GET",
|
|
399
404
|
headers: { Authorization: `Bearer ${tokens.accessToken}` }
|
|
400
405
|
});
|
|
406
|
+
const subjectId = userInfo.data?.sub ?? userInfo.data?.id;
|
|
407
|
+
if (subjectId === void 0 || subjectId === null || subjectId === "") return null;
|
|
401
408
|
return {
|
|
402
|
-
id:
|
|
409
|
+
id: subjectId,
|
|
403
410
|
emailVerified: userInfo.data?.email_verified ?? false,
|
|
404
411
|
email: userInfo.data?.email,
|
|
405
412
|
image: userInfo.data?.picture,
|
|
@@ -394,11 +394,13 @@ function organization(options) {
|
|
|
394
394
|
activeOrganizationId: {
|
|
395
395
|
type: "string",
|
|
396
396
|
required: false,
|
|
397
|
+
input: false,
|
|
397
398
|
fieldName: opts.schema?.session?.fields?.activeOrganizationId
|
|
398
399
|
},
|
|
399
400
|
...teamSupport ? { activeTeamId: {
|
|
400
401
|
type: "string",
|
|
401
402
|
required: false,
|
|
403
|
+
input: false,
|
|
402
404
|
fieldName: opts.schema?.session?.fields?.activeTeamId
|
|
403
405
|
} } : {}
|
|
404
406
|
} }
|
|
@@ -170,7 +170,12 @@ const createInvitation = (option) => {
|
|
|
170
170
|
}, ctx.context) : ctx.context.orgOptions.invitationLimit ?? 100;
|
|
171
171
|
if ((await adapter.findPendingInvitations({ organizationId })).length >= invitationLimit) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED);
|
|
172
172
|
if (ctx.context.orgOptions.teams?.enabled && "teamId" in ctx.body && ctx.body.teamId) {
|
|
173
|
-
|
|
173
|
+
const requestedTeamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
|
|
174
|
+
if (requestedTeamIds.some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID);
|
|
175
|
+
for (const teamId of requestedTeamIds) if (!await adapter.findTeamById({
|
|
176
|
+
teamId,
|
|
177
|
+
organizationId
|
|
178
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
174
179
|
}
|
|
175
180
|
if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined" && "teamId" in ctx.body && ctx.body.teamId) {
|
|
176
181
|
const teamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
|
|
@@ -285,6 +290,10 @@ const acceptInvitation = (options) => createAuthEndpoint("/organization/accept-i
|
|
|
285
290
|
const teamIds = acceptedI.teamId.split(",");
|
|
286
291
|
const onlyOne = teamIds.length === 1;
|
|
287
292
|
for (const teamId of teamIds) {
|
|
293
|
+
if (!await adapter.findTeamById({
|
|
294
|
+
teamId,
|
|
295
|
+
organizationId: invitation.organizationId
|
|
296
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
288
297
|
await adapter.findOrCreateTeamMember({
|
|
289
298
|
teamId,
|
|
290
299
|
userId: session.user.id
|
|
@@ -441,8 +441,15 @@ const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-t
|
|
|
441
441
|
use: [orgMiddleware, orgSessionMiddleware]
|
|
442
442
|
}, async (ctx) => {
|
|
443
443
|
const session = ctx.context.session;
|
|
444
|
-
const
|
|
445
|
-
|
|
444
|
+
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
445
|
+
const teams = await adapter.listTeamsByUser({ userId: session.user.id });
|
|
446
|
+
const orgIds = [...new Set(teams.map((team) => team.organizationId))];
|
|
447
|
+
const memberships = await Promise.all(orgIds.map((organizationId) => adapter.checkMembership({
|
|
448
|
+
userId: session.user.id,
|
|
449
|
+
organizationId
|
|
450
|
+
})));
|
|
451
|
+
const memberOrgIds = new Set(orgIds.filter((_, index) => memberships[index]));
|
|
452
|
+
return ctx.json(teams.filter((team) => memberOrgIds.has(team.organizationId)));
|
|
446
453
|
});
|
|
447
454
|
const listTeamMembersQuerySchema = z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) }));
|
|
448
455
|
const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team-members", {
|
|
@@ -494,6 +501,12 @@ const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team
|
|
|
494
501
|
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
495
502
|
const teamId = ctx.query?.teamId || session?.session.activeTeamId;
|
|
496
503
|
if (!teamId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM);
|
|
504
|
+
const team = await adapter.findTeamById({ teamId });
|
|
505
|
+
if (!team) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
506
|
+
if (!await adapter.checkMembership({
|
|
507
|
+
userId: session.user.id,
|
|
508
|
+
organizationId: team.organizationId
|
|
509
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM);
|
|
497
510
|
if (!await adapter.findTeamMember({
|
|
498
511
|
userId: session.user.id,
|
|
499
512
|
teamId
|
|
@@ -183,6 +183,7 @@ interface SessionDefaultFields {
|
|
|
183
183
|
activeOrganizationId: {
|
|
184
184
|
type: "string";
|
|
185
185
|
required: false;
|
|
186
|
+
input: false;
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessControl"] extends {
|
|
@@ -222,6 +223,7 @@ type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessContro
|
|
|
222
223
|
activeTeamId: {
|
|
223
224
|
type: "string";
|
|
224
225
|
required: false;
|
|
226
|
+
input: false;
|
|
225
227
|
};
|
|
226
228
|
} : {});
|
|
227
229
|
};
|
|
@@ -5,6 +5,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
|
|
|
5
5
|
import { APIError } from "../../api/index.mjs";
|
|
6
6
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
7
7
|
import { toChecksumAddress } from "../../utils/hashing.mjs";
|
|
8
|
+
import { normalizeSiweDomain, parseSiweMessage } from "./parse-message.mjs";
|
|
8
9
|
import { schema } from "./schema.mjs";
|
|
9
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
10
11
|
import * as z from "zod";
|
|
@@ -74,6 +75,33 @@ const siwe = (options) => {
|
|
|
74
75
|
code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE"
|
|
75
76
|
});
|
|
76
77
|
const { value: nonce } = verification;
|
|
78
|
+
const parsedMessage = parseSiweMessage(message);
|
|
79
|
+
const nonceMatches = parsedMessage.nonce === nonce;
|
|
80
|
+
const addressMatches = !!parsedMessage.address && parsedMessage.address.toLowerCase() === walletAddress.toLowerCase();
|
|
81
|
+
const chainMatches = parsedMessage.chainId === chainId;
|
|
82
|
+
const domainMatches = !!parsedMessage.domain && normalizeSiweDomain(parsedMessage.domain) === normalizeSiweDomain(options.domain);
|
|
83
|
+
if (!nonceMatches || !addressMatches || !chainMatches || !domainMatches) throw APIError.fromStatus("UNAUTHORIZED", {
|
|
84
|
+
message: "Unauthorized: SIWE message does not match the expected nonce, domain, address, or chain ID",
|
|
85
|
+
status: 401,
|
|
86
|
+
code: "UNAUTHORIZED_SIWE_MESSAGE_MISMATCH"
|
|
87
|
+
});
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (parsedMessage.expirationTime) {
|
|
90
|
+
const expiresAt = Date.parse(parsedMessage.expirationTime);
|
|
91
|
+
if (!Number.isNaN(expiresAt) && now >= expiresAt) throw APIError.fromStatus("UNAUTHORIZED", {
|
|
92
|
+
message: "Unauthorized: SIWE message has expired",
|
|
93
|
+
status: 401,
|
|
94
|
+
code: "UNAUTHORIZED_SIWE_MESSAGE_EXPIRED"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (parsedMessage.notBefore) {
|
|
98
|
+
const notBefore = Date.parse(parsedMessage.notBefore);
|
|
99
|
+
if (!Number.isNaN(notBefore) && now < notBefore) throw APIError.fromStatus("UNAUTHORIZED", {
|
|
100
|
+
message: "Unauthorized: SIWE message is not yet valid",
|
|
101
|
+
status: 401,
|
|
102
|
+
code: "UNAUTHORIZED_SIWE_MESSAGE_NOT_YET_VALID"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
77
105
|
if (!await options.verifyMessage({
|
|
78
106
|
message,
|
|
79
107
|
signature,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/plugins/siwe/parse-message.ts
|
|
2
|
+
const HEADER_REGEX = /^(?:([a-zA-Z][a-zA-Z0-9+.-]*):\/\/)?(\S+) wants you to sign in with your Ethereum account:$/;
|
|
3
|
+
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
|
|
4
|
+
const FIELD_REGEX = /^([A-Za-z ]+): (.*)$/;
|
|
5
|
+
function parseSiweMessage(message) {
|
|
6
|
+
const result = {};
|
|
7
|
+
const lines = message.split(/\r?\n/);
|
|
8
|
+
const headerMatch = lines[0]?.match(HEADER_REGEX);
|
|
9
|
+
if (headerMatch) {
|
|
10
|
+
if (headerMatch[1]) result.scheme = headerMatch[1];
|
|
11
|
+
result.domain = headerMatch[2];
|
|
12
|
+
}
|
|
13
|
+
const addressLine = lines[1]?.trim();
|
|
14
|
+
if (addressLine && ADDRESS_REGEX.test(addressLine)) result.address = addressLine;
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const match = line.match(FIELD_REGEX);
|
|
17
|
+
if (!match) continue;
|
|
18
|
+
const [, key, value] = match;
|
|
19
|
+
switch (key) {
|
|
20
|
+
case "URI":
|
|
21
|
+
result.uri = value;
|
|
22
|
+
break;
|
|
23
|
+
case "Version":
|
|
24
|
+
result.version = value;
|
|
25
|
+
break;
|
|
26
|
+
case "Chain ID": {
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (Number.isInteger(parsed)) result.chainId = parsed;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case "Nonce":
|
|
32
|
+
result.nonce = value;
|
|
33
|
+
break;
|
|
34
|
+
case "Issued At":
|
|
35
|
+
result.issuedAt = value;
|
|
36
|
+
break;
|
|
37
|
+
case "Expiration Time":
|
|
38
|
+
result.expirationTime = value;
|
|
39
|
+
break;
|
|
40
|
+
case "Not Before":
|
|
41
|
+
result.notBefore = value;
|
|
42
|
+
break;
|
|
43
|
+
case "Request ID":
|
|
44
|
+
result.requestId = value;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Normalizes a SIWE `domain` (RFC 3986 authority) for comparison: strips any
|
|
52
|
+
* scheme and path, lowercases, leaving `host[:port]`.
|
|
53
|
+
*/
|
|
54
|
+
function normalizeSiweDomain(domain) {
|
|
55
|
+
const withoutScheme = domain.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
56
|
+
const pathStart = withoutScheme.indexOf("/");
|
|
57
|
+
return pathStart === -1 ? withoutScheme : withoutScheme.slice(0, pathStart);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { normalizeSiweDomain, parseSiweMessage };
|
|
@@ -220,7 +220,15 @@ const twoFactor = (options) => {
|
|
|
220
220
|
expireCookie(ctx, trustDeviceCookieAttrs);
|
|
221
221
|
}
|
|
222
222
|
/**
|
|
223
|
-
*
|
|
223
|
+
* Remove the session cookie set by the credential sign-in.
|
|
224
|
+
*
|
|
225
|
+
* The credential handler already created a session and set
|
|
226
|
+
* `ctx.context.newSession`. Since 2FA is still pending, that
|
|
227
|
+
* session is deleted here and `newSession` is reset to `null`
|
|
228
|
+
* so downstream hooks don't observe a session that no longer
|
|
229
|
+
* exists. Hooks that read `ctx.context.newSession` after a
|
|
230
|
+
* sign-in must therefore null-check it: it is `null` while a
|
|
231
|
+
* 2FA challenge is in flight (no authenticated session yet).
|
|
224
232
|
*/
|
|
225
233
|
deleteSessionCookie(ctx, true);
|
|
226
234
|
await ctx.context.internalAdapter.deleteSession(data.session.token);
|
|
@@ -7999,11 +7999,6 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
7999
7999
|
priority?: RequestPriority | undefined;
|
|
8000
8000
|
cache?: RequestCache | undefined;
|
|
8001
8001
|
credentials?: RequestCredentials;
|
|
8002
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
8003
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
8004
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
8005
|
-
authorization: "Bearer" | "Basic";
|
|
8006
|
-
})) | undefined;
|
|
8007
8002
|
integrity?: string | undefined;
|
|
8008
8003
|
keepalive?: boolean | undefined;
|
|
8009
8004
|
method: string;
|
|
@@ -8033,6 +8028,12 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
8033
8028
|
prefix: string | (() => string | undefined) | undefined;
|
|
8034
8029
|
value: string | (() => string | undefined) | undefined;
|
|
8035
8030
|
}) | undefined;
|
|
8031
|
+
headers?: {} | {
|
|
8032
|
+
[x: string]: string | undefined;
|
|
8033
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
8034
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
8035
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
8036
|
+
} | undefined;
|
|
8036
8037
|
body?: any;
|
|
8037
8038
|
query?: any;
|
|
8038
8039
|
params?: any;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.16",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -480,22 +480,22 @@
|
|
|
480
480
|
},
|
|
481
481
|
"dependencies": {
|
|
482
482
|
"@better-auth/utils": "0.4.1",
|
|
483
|
-
"@better-fetch/fetch": "1.
|
|
483
|
+
"@better-fetch/fetch": "1.2.2",
|
|
484
484
|
"@noble/ciphers": "^2.1.1",
|
|
485
485
|
"@noble/hashes": "^2.0.1",
|
|
486
|
-
"better-call": "1.3.
|
|
486
|
+
"better-call": "1.3.6",
|
|
487
487
|
"defu": "^6.1.4",
|
|
488
488
|
"jose": "^6.1.3",
|
|
489
489
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
490
490
|
"nanostores": "^1.1.1",
|
|
491
491
|
"zod": "^4.3.6",
|
|
492
|
-
"@better-auth/core": "1.6.
|
|
493
|
-
"@better-auth/drizzle-adapter": "1.6.
|
|
494
|
-
"@better-auth/kysely-adapter": "1.6.
|
|
495
|
-
"@better-auth/memory-adapter": "1.6.
|
|
496
|
-
"@better-auth/mongo-adapter": "1.6.
|
|
497
|
-
"@better-auth/prisma-adapter": "1.6.
|
|
498
|
-
"@better-auth/telemetry": "1.6.
|
|
492
|
+
"@better-auth/core": "1.6.16",
|
|
493
|
+
"@better-auth/drizzle-adapter": "1.6.16",
|
|
494
|
+
"@better-auth/kysely-adapter": "1.6.16",
|
|
495
|
+
"@better-auth/memory-adapter": "1.6.16",
|
|
496
|
+
"@better-auth/mongo-adapter": "1.6.16",
|
|
497
|
+
"@better-auth/prisma-adapter": "1.6.16",
|
|
498
|
+
"@better-auth/telemetry": "1.6.16"
|
|
499
499
|
},
|
|
500
500
|
"devDependencies": {
|
|
501
501
|
"@lynx-js/react": "^0.116.3",
|