better-auth 1.6.16 → 1.6.18
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 +2 -2
- package/dist/api/index.mjs +3 -4
- package/dist/api/middlewares/origin-check.mjs +5 -1
- package/dist/api/rate-limiter/index.mjs +259 -73
- package/dist/api/routes/account.mjs +22 -7
- package/dist/api/routes/callback.mjs +2 -2
- package/dist/api/routes/index.d.mts +1 -1
- package/dist/api/routes/password.mjs +3 -4
- package/dist/api/routes/session.d.mts +12 -1
- package/dist/api/routes/session.mjs +13 -1
- package/dist/api/routes/sign-in.mjs +5 -5
- package/dist/api/routes/sign-up.mjs +2 -2
- package/dist/api/routes/update-session.mjs +2 -3
- package/dist/api/routes/update-user.mjs +10 -12
- package/dist/auth/base.mjs +11 -7
- package/dist/client/equality.d.mts +19 -0
- package/dist/client/equality.mjs +42 -0
- package/dist/client/index.d.mts +5 -4
- package/dist/client/index.mjs +2 -1
- package/dist/client/lynx/index.d.mts +4 -2
- package/dist/client/path-to-object.d.mts +5 -2
- package/dist/client/plugins/index.d.mts +4 -1
- package/dist/client/plugins/index.mjs +4 -1
- package/dist/client/query.d.mts +4 -3
- package/dist/client/query.mjs +27 -17
- package/dist/client/react/index.d.mts +4 -2
- package/dist/client/session-atom.mjs +129 -4
- package/dist/client/session-refresh.d.mts +3 -18
- package/dist/client/session-refresh.mjs +38 -49
- package/dist/client/solid/index.d.mts +4 -2
- package/dist/client/svelte/index.d.mts +4 -2
- package/dist/client/types.d.mts +27 -16
- package/dist/client/vanilla.d.mts +4 -2
- package/dist/client/vue/index.d.mts +4 -2
- package/dist/context/create-context.mjs +2 -1
- package/dist/context/store-capabilities.mjs +12 -0
- package/dist/cookies/index.mjs +25 -2
- package/dist/db/internal-adapter.mjs +51 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +49 -19
- package/dist/plugins/admin/routes.mjs +10 -3
- package/dist/plugins/captcha/constants.mjs +8 -1
- package/dist/plugins/captcha/index.mjs +8 -2
- package/dist/plugins/captcha/types.d.mts +21 -0
- package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
- package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
- package/dist/plugins/device-authorization/routes.mjs +16 -9
- package/dist/plugins/email-otp/routes.mjs +22 -52
- package/dist/plugins/generic-oauth/index.mjs +7 -2
- package/dist/plugins/generic-oauth/routes.mjs +16 -12
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +5 -1
- package/dist/plugins/index.d.mts +6 -2
- package/dist/plugins/index.mjs +4 -1
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/mcp/client/index.mjs +1 -0
- package/dist/plugins/mcp/index.mjs +8 -0
- package/dist/plugins/multi-session/index.mjs +7 -5
- package/dist/plugins/oauth-popup/client.d.mts +82 -0
- package/dist/plugins/oauth-popup/client.mjs +203 -0
- package/dist/plugins/oauth-popup/constants.d.mts +11 -0
- package/dist/plugins/oauth-popup/constants.mjs +11 -0
- package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
- package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
- package/dist/plugins/oauth-popup/index.d.mts +67 -0
- package/dist/plugins/oauth-popup/index.mjs +227 -0
- package/dist/plugins/oauth-popup/types.d.mts +30 -0
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/utils.mjs +16 -2
- package/dist/plugins/oidc-provider/index.mjs +10 -0
- package/dist/plugins/one-tap/client.mjs +12 -6
- package/dist/plugins/one-tap/index.d.mts +1 -0
- package/dist/plugins/one-tap/index.mjs +9 -5
- package/dist/plugins/one-time-token/index.mjs +1 -3
- package/dist/plugins/open-api/generator.d.mts +66 -57
- package/dist/plugins/open-api/generator.mjs +185 -67
- package/dist/plugins/open-api/index.d.mts +2 -2
- package/dist/plugins/organization/adapter.d.mts +29 -1
- package/dist/plugins/organization/adapter.mjs +66 -6
- package/dist/plugins/organization/routes/crud-invites.mjs +49 -34
- package/dist/plugins/organization/routes/crud-members.mjs +42 -6
- package/dist/plugins/organization/routes/crud-team.mjs +36 -3
- package/dist/plugins/phone-number/routes.mjs +41 -36
- package/dist/plugins/siwe/index.mjs +2 -3
- package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
- package/dist/plugins/two-factor/otp/index.mjs +11 -13
- package/dist/plugins/two-factor/totp/index.mjs +1 -1
- package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
- package/dist/plugins/username/index.mjs +6 -6
- package/dist/test-utils/test-instance.d.mts +26 -23
- package/package.json +9 -9
|
@@ -1,33 +1,63 @@
|
|
|
1
1
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
2
2
|
//#region src/plugins/access/access.ts
|
|
3
|
+
function unknownResourceResponse(requestedResource) {
|
|
4
|
+
return {
|
|
5
|
+
success: false,
|
|
6
|
+
error: `You are not allowed to access resource: ${requestedResource}`
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function unauthorizedResourceResponse(requestedResource) {
|
|
10
|
+
return {
|
|
11
|
+
success: false,
|
|
12
|
+
error: `unauthorized to access resource "${requestedResource}"`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function normalizeConnector(connector) {
|
|
16
|
+
return connector === "OR" ? "OR" : "AND";
|
|
17
|
+
}
|
|
18
|
+
function isActionList(actions) {
|
|
19
|
+
return Array.isArray(actions);
|
|
20
|
+
}
|
|
21
|
+
function normalizeActionRequest(requestedActions) {
|
|
22
|
+
if (isActionList(requestedActions)) return {
|
|
23
|
+
actions: requestedActions,
|
|
24
|
+
connector: "AND"
|
|
25
|
+
};
|
|
26
|
+
if (!requestedActions || typeof requestedActions !== "object") throw new BetterAuthError("Invalid access control request");
|
|
27
|
+
const { actions, connector } = requestedActions;
|
|
28
|
+
if (!isActionList(actions)) return {
|
|
29
|
+
actions: [],
|
|
30
|
+
connector: normalizeConnector(connector)
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
actions,
|
|
34
|
+
connector: normalizeConnector(connector)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function hasAllowedAction(allowedActions, requestedAction) {
|
|
38
|
+
return typeof requestedAction === "string" && allowedActions.includes(requestedAction);
|
|
39
|
+
}
|
|
40
|
+
function isResourceAuthorized(allowedActions, { actions, connector }) {
|
|
41
|
+
if (actions.length === 0) return false;
|
|
42
|
+
if (connector === "OR") return actions.some((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
|
|
43
|
+
return actions.every((requestedAction) => hasAllowedAction(allowedActions, requestedAction));
|
|
44
|
+
}
|
|
3
45
|
function role(statements) {
|
|
4
46
|
return {
|
|
5
47
|
authorize(request, connector = "AND") {
|
|
6
|
-
let
|
|
48
|
+
let hasAuthorizedResource = false;
|
|
7
49
|
for (const [requestedResource, requestedActions] of Object.entries(request)) {
|
|
8
50
|
const allowedActions = statements[requestedResource];
|
|
9
51
|
if (!allowedActions) {
|
|
10
|
-
if (connector === "AND") return
|
|
11
|
-
success: false,
|
|
12
|
-
error: `You are not allowed to access resource: ${requestedResource}`
|
|
13
|
-
};
|
|
14
|
-
success = false;
|
|
52
|
+
if (connector === "AND") return unknownResourceResponse(requestedResource);
|
|
15
53
|
continue;
|
|
16
54
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
else if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
|
|
22
|
-
else success = actions.actions.every((requestedAction) => allowedActions.includes(requestedAction));
|
|
23
|
-
} else throw new BetterAuthError("Invalid access control request");
|
|
24
|
-
if (success && connector === "OR") return { success };
|
|
25
|
-
if (!success && connector === "AND") return {
|
|
26
|
-
success: false,
|
|
27
|
-
error: `unauthorized to access resource "${requestedResource}"`
|
|
28
|
-
};
|
|
55
|
+
const isAuthorized = isResourceAuthorized(allowedActions, normalizeActionRequest(requestedActions));
|
|
56
|
+
if (isAuthorized) hasAuthorizedResource = true;
|
|
57
|
+
if (isAuthorized && connector === "OR") return { success: true };
|
|
58
|
+
if (!isAuthorized && connector === "AND") return unauthorizedResourceResponse(requestedResource);
|
|
29
59
|
}
|
|
30
|
-
if (
|
|
60
|
+
if (hasAuthorizedResource) return { success: true };
|
|
31
61
|
return {
|
|
32
62
|
success: false,
|
|
33
63
|
error: "Not authorized"
|
|
@@ -816,16 +816,23 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
|
|
|
816
816
|
const { newPassword, userId } = ctx.body;
|
|
817
817
|
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
818
818
|
if (newPassword.length < minPasswordLength) {
|
|
819
|
-
ctx.context.logger.
|
|
819
|
+
ctx.context.logger.warn("Password is too short");
|
|
820
820
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
821
821
|
}
|
|
822
822
|
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
823
823
|
if (newPassword.length > maxPasswordLength) {
|
|
824
|
-
ctx.context.logger.
|
|
824
|
+
ctx.context.logger.warn("Password is too long");
|
|
825
825
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
826
826
|
}
|
|
827
|
+
if (!await ctx.context.internalAdapter.findUserById(userId)) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
827
828
|
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
828
|
-
await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
829
|
+
if ((await ctx.context.internalAdapter.findAccounts(userId)).find((account) => account.providerId === "credential")) await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
830
|
+
else await ctx.context.internalAdapter.createAccount({
|
|
831
|
+
userId,
|
|
832
|
+
providerId: "credential",
|
|
833
|
+
accountId: userId,
|
|
834
|
+
password: hashedPassword
|
|
835
|
+
});
|
|
829
836
|
return ctx.json({ status: true });
|
|
830
837
|
});
|
|
831
838
|
const userHasPermissionBodySchema = z.object({
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
//#region src/plugins/captcha/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound (in milliseconds) for a single provider verification request.
|
|
4
|
+
* Without it, a hanging provider would tie up the request indefinitely before
|
|
5
|
+
* any rate limiting applies, so every verify handler aborts at this deadline
|
|
6
|
+
* and fails closed.
|
|
7
|
+
*/
|
|
8
|
+
const CAPTCHA_VERIFY_TIMEOUT_MS = 1e4;
|
|
2
9
|
const defaultEndpoints = [
|
|
3
10
|
"/sign-up/email",
|
|
4
11
|
"/sign-in/email",
|
|
@@ -17,4 +24,4 @@ const siteVerifyMap = {
|
|
|
17
24
|
[Providers.CAPTCHAFOX]: "https://api.captchafox.com/siteverify"
|
|
18
25
|
};
|
|
19
26
|
//#endregion
|
|
20
|
-
export { Providers, defaultEndpoints, siteVerifyMap };
|
|
27
|
+
export { CAPTCHA_VERIFY_TIMEOUT_MS, Providers, defaultEndpoints, siteVerifyMap };
|
|
@@ -42,10 +42,16 @@ const captcha = (options) => ({
|
|
|
42
42
|
secretKey: options.secretKey,
|
|
43
43
|
remoteIP: remoteUserIP
|
|
44
44
|
};
|
|
45
|
-
if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile(
|
|
45
|
+
if (options.provider === Providers.CLOUDFLARE_TURNSTILE) return await cloudflareTurnstile({
|
|
46
|
+
...handlerParams,
|
|
47
|
+
expectedAction: options.expectedAction,
|
|
48
|
+
allowedHostnames: options.allowedHostnames
|
|
49
|
+
});
|
|
46
50
|
if (options.provider === Providers.GOOGLE_RECAPTCHA) return await googleRecaptcha({
|
|
47
51
|
...handlerParams,
|
|
48
|
-
minScore: options.minScore
|
|
52
|
+
minScore: options.minScore,
|
|
53
|
+
expectedAction: options.expectedAction,
|
|
54
|
+
allowedHostnames: options.allowedHostnames
|
|
49
55
|
});
|
|
50
56
|
if (options.provider === Providers.HCAPTCHA) return await hCaptcha({
|
|
51
57
|
...handlerParams,
|
|
@@ -10,9 +10,30 @@ interface BaseCaptchaOptions {
|
|
|
10
10
|
interface GoogleRecaptchaOptions extends BaseCaptchaOptions {
|
|
11
11
|
provider: typeof Providers.GOOGLE_RECAPTCHA;
|
|
12
12
|
minScore?: number | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Expected reCAPTCHA v3 `action`. When set, a verification whose action does
|
|
15
|
+
* not match is rejected, preventing a token minted for another action on the
|
|
16
|
+
* same site key from being replayed against this endpoint.
|
|
17
|
+
*/
|
|
18
|
+
expectedAction?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Allow-list of hostnames the token must have been issued for. When set, a
|
|
21
|
+
* verification reporting a different hostname is rejected.
|
|
22
|
+
*/
|
|
23
|
+
allowedHostnames?: string[] | undefined;
|
|
13
24
|
}
|
|
14
25
|
interface CloudflareTurnstileOptions extends BaseCaptchaOptions {
|
|
15
26
|
provider: typeof Providers.CLOUDFLARE_TURNSTILE;
|
|
27
|
+
/**
|
|
28
|
+
* Expected Turnstile `action`. When set, a verification whose action does
|
|
29
|
+
* not match is rejected, preventing cross-context token reuse.
|
|
30
|
+
*/
|
|
31
|
+
expectedAction?: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Allow-list of hostnames the token must have been issued for. When set, a
|
|
34
|
+
* verification reporting a different or missing hostname is rejected.
|
|
35
|
+
*/
|
|
36
|
+
allowedHostnames?: string[] | undefined;
|
|
16
37
|
}
|
|
17
38
|
interface HCaptchaOptions extends BaseCaptchaOptions {
|
|
18
39
|
provider: typeof Providers.HCAPTCHA;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { encodeToURLParams } from "../utils.mjs";
|
|
4
5
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
6
7
|
const captchaFox = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
|
|
7
8
|
const response = await betterFetch(siteVerifyURL, {
|
|
8
9
|
method: "POST",
|
|
10
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
9
11
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
10
12
|
body: encodeToURLParams({
|
|
11
13
|
secret: secretKey,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { betterFetch } from "@better-fetch/fetch";
|
|
4
5
|
//#region src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts
|
|
5
|
-
const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP }) => {
|
|
6
|
+
const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP, expectedAction, allowedHostnames }) => {
|
|
6
7
|
const response = await betterFetch(siteVerifyURL, {
|
|
7
8
|
method: "POST",
|
|
9
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
8
10
|
headers: { "Content-Type": "application/json" },
|
|
9
11
|
body: JSON.stringify({
|
|
10
12
|
secret: secretKey,
|
|
@@ -13,11 +15,14 @@ const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey,
|
|
|
13
15
|
})
|
|
14
16
|
});
|
|
15
17
|
if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
|
|
16
|
-
|
|
18
|
+
const verificationFailed = () => middlewareResponse({
|
|
17
19
|
message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
|
|
18
20
|
code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
|
|
19
21
|
status: 403
|
|
20
22
|
});
|
|
23
|
+
if (!response.data.success) return verificationFailed();
|
|
24
|
+
if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
|
|
25
|
+
if (allowedHostnames && allowedHostnames.length > 0 && !(response.data.hostname && allowedHostnames.includes(response.data.hostname))) return verificationFailed();
|
|
21
26
|
};
|
|
22
27
|
//#endregion
|
|
23
28
|
export { cloudflareTurnstile };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { encodeToURLParams } from "../utils.mjs";
|
|
4
5
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -6,9 +7,10 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
6
7
|
const isV3 = (response) => {
|
|
7
8
|
return "score" in response && typeof response.score === "number";
|
|
8
9
|
};
|
|
9
|
-
const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP }) => {
|
|
10
|
+
const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = .5, remoteIP, expectedAction, allowedHostnames }) => {
|
|
10
11
|
const response = await betterFetch(siteVerifyURL, {
|
|
11
12
|
method: "POST",
|
|
13
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
12
14
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
13
15
|
body: encodeToURLParams({
|
|
14
16
|
secret: secretKey,
|
|
@@ -17,11 +19,14 @@ const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minS
|
|
|
17
19
|
})
|
|
18
20
|
});
|
|
19
21
|
if (!response.data || response.error) throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message);
|
|
20
|
-
|
|
22
|
+
const verificationFailed = () => middlewareResponse({
|
|
21
23
|
message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
|
|
22
24
|
code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
|
|
23
25
|
status: 403
|
|
24
26
|
});
|
|
27
|
+
if (!response.data.success || isV3(response.data) && response.data.score < minScore) return verificationFailed();
|
|
28
|
+
if (expectedAction && response.data.action !== expectedAction) return verificationFailed();
|
|
29
|
+
if (allowedHostnames && allowedHostnames.length > 0 && !allowedHostnames.includes(response.data.hostname)) return verificationFailed();
|
|
25
30
|
};
|
|
26
31
|
//#endregion
|
|
27
32
|
export { googleRecaptcha };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
2
3
|
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
3
4
|
import { encodeToURLParams } from "../utils.mjs";
|
|
4
5
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -6,6 +7,7 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
6
7
|
const hCaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP }) => {
|
|
7
8
|
const response = await betterFetch(siteVerifyURL, {
|
|
8
9
|
method: "POST",
|
|
10
|
+
timeout: CAPTCHA_VERIFY_TIMEOUT_MS,
|
|
9
11
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
10
12
|
body: encodeToURLParams({
|
|
11
13
|
secret: secretKey,
|
|
@@ -243,7 +243,21 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
243
243
|
});
|
|
244
244
|
}
|
|
245
245
|
if (deviceCodeRecord.status === "approved" && deviceCodeRecord.userId) {
|
|
246
|
-
const
|
|
246
|
+
const claimedDeviceCode = await ctx.context.adapter.consumeOne({
|
|
247
|
+
model: "deviceCode",
|
|
248
|
+
where: [{
|
|
249
|
+
field: "deviceCode",
|
|
250
|
+
value: device_code
|
|
251
|
+
}, {
|
|
252
|
+
field: "status",
|
|
253
|
+
value: "approved"
|
|
254
|
+
}]
|
|
255
|
+
});
|
|
256
|
+
if (!claimedDeviceCode?.userId) throw new APIError("BAD_REQUEST", {
|
|
257
|
+
error: "invalid_grant",
|
|
258
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE.message
|
|
259
|
+
});
|
|
260
|
+
const user = await ctx.context.internalAdapter.findUserById(claimedDeviceCode.userId);
|
|
247
261
|
if (!user) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
248
262
|
error: "server_error",
|
|
249
263
|
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.USER_NOT_FOUND.message
|
|
@@ -261,18 +275,11 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
261
275
|
user,
|
|
262
276
|
session
|
|
263
277
|
}), Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / 1e3));
|
|
264
|
-
await ctx.context.adapter.delete({
|
|
265
|
-
model: "deviceCode",
|
|
266
|
-
where: [{
|
|
267
|
-
field: "id",
|
|
268
|
-
value: deviceCodeRecord.id
|
|
269
|
-
}]
|
|
270
|
-
});
|
|
271
278
|
return ctx.json({
|
|
272
279
|
access_token: session.token,
|
|
273
280
|
token_type: "Bearer",
|
|
274
281
|
expires_in: Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / 1e3),
|
|
275
|
-
scope:
|
|
282
|
+
scope: claimedDeviceCode.scope || ""
|
|
276
283
|
}, { headers: {
|
|
277
284
|
"Cache-Control": "no-store",
|
|
278
285
|
Pragma: "no-cache"
|
|
@@ -115,7 +115,7 @@ const createVerificationOTPBodySchema = z.object({
|
|
|
115
115
|
description: "Type of the OTP"
|
|
116
116
|
})
|
|
117
117
|
});
|
|
118
|
-
const createVerificationOTP = (opts) => createAuthEndpoint({
|
|
118
|
+
const createVerificationOTP = (opts) => createAuthEndpoint.serverOnly({
|
|
119
119
|
method: "POST",
|
|
120
120
|
body: createVerificationOTPBodySchema,
|
|
121
121
|
metadata: { openapi: {
|
|
@@ -159,7 +159,7 @@ const getVerificationOTPBodySchema = z.object({
|
|
|
159
159
|
*
|
|
160
160
|
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-get-verification-otp)
|
|
161
161
|
*/
|
|
162
|
-
const getVerificationOTP = (opts) => createAuthEndpoint({
|
|
162
|
+
const getVerificationOTP = (opts) => createAuthEndpoint.serverOnly({
|
|
163
163
|
method: "GET",
|
|
164
164
|
query: getVerificationOTPBodySchema,
|
|
165
165
|
metadata: { openapi: {
|
|
@@ -636,24 +636,7 @@ const requestEmailChangeEmailOTP = (opts) => createAuthEndpoint("/email-otp/requ
|
|
|
636
636
|
}
|
|
637
637
|
if (opts.changeEmail?.verifyCurrentEmail) {
|
|
638
638
|
if (!ctx.body.otp) throw APIError$1.fromStatus("BAD_REQUEST", { message: "OTP is required to verify current email" });
|
|
639
|
-
|
|
640
|
-
if (!currentEmailVerificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
641
|
-
const currentEmailIdentifier = toOTPIdentifier("email-verification", email);
|
|
642
|
-
if (currentEmailVerificationValue.expiresAt < /* @__PURE__ */ new Date()) {
|
|
643
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(currentEmailIdentifier);
|
|
644
|
-
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
|
|
645
|
-
}
|
|
646
|
-
const [otpValue, attempts] = splitAtLastColon(currentEmailVerificationValue.value);
|
|
647
|
-
const allowedAttempts = opts?.allowedAttempts || 3;
|
|
648
|
-
if (attempts && parseInt(attempts) >= allowedAttempts) {
|
|
649
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(currentEmailIdentifier);
|
|
650
|
-
throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
|
|
651
|
-
}
|
|
652
|
-
if (!await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp)) {
|
|
653
|
-
await ctx.context.internalAdapter.updateVerificationByIdentifier(currentEmailIdentifier, { value: `${otpValue}:${parseInt(attempts || "0") + 1}` });
|
|
654
|
-
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
655
|
-
}
|
|
656
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(currentEmailIdentifier);
|
|
639
|
+
await atomicVerifyOTP(ctx, opts, toOTPIdentifier("email-verification", email), ctx.body.otp);
|
|
657
640
|
} else if (ctx.body.otp) ctx.context.logger.warn("OTP provided but not required for verifying current email. If you want to require OTP verification for current email, please set the changeEmail.verifyCurrentEmail option to true in the configuration");
|
|
658
641
|
const otp = opts.generateOTP({
|
|
659
642
|
email: newEmail,
|
|
@@ -723,24 +706,7 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
|
|
|
723
706
|
ctx.context.logger.error("Email is the same");
|
|
724
707
|
throw APIError$1.fromStatus("BAD_REQUEST", { message: "Email is the same" });
|
|
725
708
|
}
|
|
726
|
-
|
|
727
|
-
if (!verificationValue) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
728
|
-
const changeEmailIdentifier = toOTPIdentifier("change-email", `${email}-${newEmail}`);
|
|
729
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) {
|
|
730
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(changeEmailIdentifier);
|
|
731
|
-
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
|
|
732
|
-
}
|
|
733
|
-
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
|
|
734
|
-
const allowedAttempts = opts?.allowedAttempts || 3;
|
|
735
|
-
if (attempts && parseInt(attempts) >= allowedAttempts) {
|
|
736
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(changeEmailIdentifier);
|
|
737
|
-
throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
|
|
738
|
-
}
|
|
739
|
-
if (!await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp)) {
|
|
740
|
-
await ctx.context.internalAdapter.updateVerificationByIdentifier(changeEmailIdentifier, { value: `${otpValue}:${parseInt(attempts || "0") + 1}` });
|
|
741
|
-
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
742
|
-
}
|
|
743
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(changeEmailIdentifier);
|
|
709
|
+
await atomicVerifyOTP(ctx, opts, toOTPIdentifier("change-email", `${email}-${newEmail}`), ctx.body.otp);
|
|
744
710
|
const currentUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
745
711
|
if (!currentUser)
|
|
746
712
|
/**
|
|
@@ -770,29 +736,33 @@ const changeEmailEmailOTP = (opts) => createAuthEndpoint("/email-otp/change-emai
|
|
|
770
736
|
});
|
|
771
737
|
const defaultOTPGenerator = (options) => generateRandomString(options.otpLength ?? 6, "0-9");
|
|
772
738
|
/**
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
739
|
+
* Verifies a single-use OTP with race-condition protection.
|
|
740
|
+
*
|
|
741
|
+
* The atomic consume is the single gate: only the first concurrent caller
|
|
742
|
+
* receives the record, every later racer receives `null` and is rejected, so
|
|
743
|
+
* a correct OTP can only ever be accepted once. When the submitted code is
|
|
744
|
+
* wrong the record is recreated with the same value and expiry and an
|
|
745
|
+
* incremented attempt count so the next try can still find it; the budget is
|
|
746
|
+
* enforced before verification, and a record whose attempts are exhausted is
|
|
747
|
+
* left consumed (no recreate), locking the identifier out.
|
|
776
748
|
*/
|
|
777
749
|
async function atomicVerifyOTP(ctx, opts, identifier, providedOTP) {
|
|
778
|
-
const
|
|
779
|
-
if (
|
|
780
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) {
|
|
750
|
+
const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
751
|
+
if (existing && existing.expiresAt < /* @__PURE__ */ new Date()) {
|
|
781
752
|
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
782
753
|
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.OTP_EXPIRED);
|
|
783
754
|
}
|
|
784
|
-
const
|
|
755
|
+
const consumed = await ctx.context.internalAdapter.consumeVerificationValue(identifier);
|
|
756
|
+
if (!consumed) throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
757
|
+
const [otpValue, attempts] = splitAtLastColon(consumed.value);
|
|
785
758
|
const allowedAttempts = opts?.allowedAttempts || 3;
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
|
|
789
|
-
}
|
|
790
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
759
|
+
const usedAttempts = parseInt(attempts || "0");
|
|
760
|
+
if (usedAttempts >= allowedAttempts) throw APIError$1.from("FORBIDDEN", EMAIL_OTP_ERROR_CODES.TOO_MANY_ATTEMPTS);
|
|
791
761
|
if (!await verifyStoredOTP(ctx, opts, otpValue, providedOTP)) {
|
|
792
762
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
793
|
-
value: `${otpValue}:${
|
|
763
|
+
value: `${otpValue}:${usedAttempts + 1}`,
|
|
794
764
|
identifier,
|
|
795
|
-
expiresAt:
|
|
765
|
+
expiresAt: consumed.expiresAt
|
|
796
766
|
});
|
|
797
767
|
throw APIError$1.from("BAD_REQUEST", EMAIL_OTP_ERROR_CODES.INVALID_OTP);
|
|
798
768
|
}
|
|
@@ -14,6 +14,9 @@ import { APIError } from "@better-auth/core/error";
|
|
|
14
14
|
import { applyDefaultAccessTokenExpiry, createAuthorizationURL, refreshAccessToken, validateAuthorizationCode } from "@better-auth/core/oauth2";
|
|
15
15
|
import { betterFetch } from "@better-fetch/fetch";
|
|
16
16
|
//#region src/plugins/generic-oauth/index.ts
|
|
17
|
+
function isNonEmptyOAuthId(id) {
|
|
18
|
+
return id !== void 0 && id !== null && id !== "";
|
|
19
|
+
}
|
|
17
20
|
/**
|
|
18
21
|
* A generic OAuth plugin that can be used to add OAuth support to any provider
|
|
19
22
|
*/
|
|
@@ -114,14 +117,16 @@ const genericOAuth = (options) => {
|
|
|
114
117
|
const userInfo = c.getUserInfo ? await c.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
|
|
115
118
|
if (!userInfo) return null;
|
|
116
119
|
const userMap = await c.mapProfileToUser?.(userInfo);
|
|
120
|
+
const rawId = isNonEmptyOAuthId(userMap?.id) ? userMap.id : isNonEmptyOAuthId(userInfo.id) ? userInfo.id : isNonEmptyOAuthId(userInfo.sub) ? userInfo.sub : void 0;
|
|
121
|
+
if (rawId === void 0) return null;
|
|
117
122
|
return {
|
|
118
123
|
user: {
|
|
119
|
-
id: userInfo?.id,
|
|
120
124
|
email: userInfo?.email,
|
|
121
125
|
emailVerified: userInfo?.emailVerified,
|
|
122
126
|
image: userInfo?.image,
|
|
123
127
|
name: userInfo?.name,
|
|
124
|
-
...userMap
|
|
128
|
+
...userMap,
|
|
129
|
+
id: String(rawId)
|
|
125
130
|
},
|
|
126
131
|
data: userInfo
|
|
127
132
|
};
|
|
@@ -15,6 +15,9 @@ import * as z from "zod";
|
|
|
15
15
|
import { decodeJwt } from "jose";
|
|
16
16
|
import { betterFetch } from "@better-fetch/fetch";
|
|
17
17
|
//#region src/plugins/generic-oauth/routes.ts
|
|
18
|
+
function isNonEmptyOAuthId(id) {
|
|
19
|
+
return id !== void 0 && id !== null && id !== "";
|
|
20
|
+
}
|
|
18
21
|
const signInWithOAuth2BodySchema = z.object({
|
|
19
22
|
providerId: z.string().meta({ description: "The provider ID for the OAuth provider" }),
|
|
20
23
|
callbackURL: z.string().meta({ description: "The URL to redirect to after sign in" }).optional(),
|
|
@@ -209,8 +212,8 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
209
212
|
ctx.context.logger.error(missingEmailLogMessage(providerConfig.providerId, { source: "generic" }), userInfo);
|
|
210
213
|
redirectOnError(ctx, resolvedErrorURL, "email_is_missing");
|
|
211
214
|
}
|
|
212
|
-
const rawId = mapUser.id
|
|
213
|
-
const id = rawId !== void 0
|
|
215
|
+
const rawId = isNonEmptyOAuthId(mapUser.id) ? mapUser.id : isNonEmptyOAuthId(userInfo.id) ? userInfo.id : isNonEmptyOAuthId(userInfo.sub) ? userInfo.sub : void 0;
|
|
216
|
+
const id = rawId !== void 0 ? String(rawId) : "";
|
|
214
217
|
if (!id) {
|
|
215
218
|
ctx.context.logger.error("Provider did not return an account id (e.g. `sub`). Unable to sign in.", userInfo);
|
|
216
219
|
redirectOnError(ctx, resolvedErrorURL, "id_is_missing");
|
|
@@ -399,19 +402,20 @@ async function getUserInfo(tokens, finalUserInfoUrl) {
|
|
|
399
402
|
}
|
|
400
403
|
}
|
|
401
404
|
if (!finalUserInfoUrl) return null;
|
|
402
|
-
const
|
|
405
|
+
const profile = (await betterFetch(finalUserInfoUrl, {
|
|
403
406
|
method: "GET",
|
|
404
407
|
headers: { Authorization: `Bearer ${tokens.accessToken}` }
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
+
})).data;
|
|
409
|
+
if (!profile) return null;
|
|
410
|
+
const { id: profileId, ...profileFields } = profile;
|
|
411
|
+
const subjectId = isNonEmptyOAuthId(profileId) ? profileId : isNonEmptyOAuthId(profile.sub) ? profile.sub : void 0;
|
|
408
412
|
return {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
email:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
413
|
+
...profileFields,
|
|
414
|
+
...subjectId !== void 0 ? { id: subjectId } : {},
|
|
415
|
+
email: profile?.email,
|
|
416
|
+
emailVerified: profile?.email_verified ?? false,
|
|
417
|
+
image: profile?.picture,
|
|
418
|
+
name: profile?.name
|
|
415
419
|
};
|
|
416
420
|
}
|
|
417
421
|
//#endregion
|
|
@@ -17,7 +17,7 @@ interface HaveIBeenPwnedOptions {
|
|
|
17
17
|
/**
|
|
18
18
|
* Paths to check for password
|
|
19
19
|
*
|
|
20
|
-
* @default ["/sign-up/email", "/change-password", "/reset-password"]
|
|
20
|
+
* @default ["/sign-up/email", "/change-password", "/reset-password", "/email-otp/reset-password", "/phone-number/reset-password", "/admin/create-user", "/admin/set-user-password"]
|
|
21
21
|
*/
|
|
22
22
|
paths?: string[];
|
|
23
23
|
/**
|
|
@@ -31,7 +31,11 @@ const haveIBeenPwned = (options) => {
|
|
|
31
31
|
const paths = options?.paths || [
|
|
32
32
|
"/sign-up/email",
|
|
33
33
|
"/change-password",
|
|
34
|
-
"/reset-password"
|
|
34
|
+
"/reset-password",
|
|
35
|
+
"/email-otp/reset-password",
|
|
36
|
+
"/phone-number/reset-password",
|
|
37
|
+
"/admin/create-user",
|
|
38
|
+
"/admin/set-user-password"
|
|
35
39
|
];
|
|
36
40
|
return {
|
|
37
41
|
id: "have-i-been-pwned",
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -41,10 +41,14 @@ import { getClient, getMetadata, oidcProvider } from "./oidc-provider/index.mjs"
|
|
|
41
41
|
import { getMCPProtectedResourceMetadata, getMCPProviderMetadata, mcp, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, withMcpAuth } from "./mcp/index.mjs";
|
|
42
42
|
import { MULTI_SESSION_ERROR_CODES } from "./multi-session/error-codes.mjs";
|
|
43
43
|
import { MultiSessionConfig, multiSession } from "./multi-session/index.mjs";
|
|
44
|
+
import { OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_MESSAGE_TYPE, POPUP_MARKER_COOKIE } from "./oauth-popup/constants.mjs";
|
|
45
|
+
import { OAUTH_POPUP_ERROR_CODES } from "./oauth-popup/error-codes.mjs";
|
|
46
|
+
import { OAuthPopupData, OAuthPopupMessage } from "./oauth-popup/types.mjs";
|
|
47
|
+
import { OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_SCRIPT_CSP_HASH, oauthPopup } from "./oauth-popup/index.mjs";
|
|
44
48
|
import { OAuthProxyOptions, oAuthProxy } from "./oauth-proxy/index.mjs";
|
|
45
49
|
import { OneTapOptions, oneTap } from "./one-tap/index.mjs";
|
|
46
50
|
import { OneTimeTokenOptions, oneTimeToken } from "./one-time-token/index.mjs";
|
|
47
|
-
import { FieldSchema, OpenAPIModelSchema, Path, generator } from "./open-api/generator.mjs";
|
|
51
|
+
import { FieldSchema, OpenAPIModelSchema, OpenAPIParameter, OpenAPISchema, Path, generator } from "./open-api/generator.mjs";
|
|
48
52
|
import { OpenAPIOptions, openAPI } from "./open-api/index.mjs";
|
|
49
53
|
import { PhoneNumberOptions, UserWithPhoneNumber } from "./phone-number/types.mjs";
|
|
50
54
|
import { phoneNumber } from "./phone-number/index.mjs";
|
|
@@ -62,4 +66,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
|
|
|
62
66
|
import { UsernameOptions, username } from "./username/index.mjs";
|
|
63
67
|
import { hasPermission } from "./organization/has-permission.mjs";
|
|
64
68
|
import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
|
|
65
|
-
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAuthAccessToken, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, Path, PatreonOptions, PhoneNumberOptions, Provider, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth };
|
|
69
|
+
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthAccessToken, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { HIDE_METADATA } from "../utils/hide-metadata.mjs";
|
|
2
2
|
import { createAccessControl, role } from "./access/access.mjs";
|
|
3
3
|
import { MULTI_SESSION_ERROR_CODES } from "./multi-session/error-codes.mjs";
|
|
4
|
+
import { OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_MESSAGE_TYPE, POPUP_MARKER_COOKIE } from "./oauth-popup/constants.mjs";
|
|
5
|
+
import { OAUTH_POPUP_ERROR_CODES } from "./oauth-popup/error-codes.mjs";
|
|
4
6
|
import { TWO_FACTOR_ERROR_CODES } from "./two-factor/error-code.mjs";
|
|
5
7
|
import { twoFactorClient } from "./two-factor/client.mjs";
|
|
6
8
|
import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
|
|
@@ -31,6 +33,7 @@ import { magicLink } from "./magic-link/index.mjs";
|
|
|
31
33
|
import { getClient, getMetadata, oidcProvider } from "./oidc-provider/index.mjs";
|
|
32
34
|
import { getMCPProtectedResourceMetadata, getMCPProviderMetadata, mcp, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, withMcpAuth } from "./mcp/index.mjs";
|
|
33
35
|
import { multiSession } from "./multi-session/index.mjs";
|
|
36
|
+
import { OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_SCRIPT_CSP_HASH, oauthPopup } from "./oauth-popup/index.mjs";
|
|
34
37
|
import { oAuthProxy } from "./oauth-proxy/index.mjs";
|
|
35
38
|
import { oneTap } from "./one-tap/index.mjs";
|
|
36
39
|
import { oneTimeToken } from "./one-time-token/index.mjs";
|
|
@@ -43,4 +46,4 @@ import { siwe } from "./siwe/index.mjs";
|
|
|
43
46
|
import { testUtils } from "./test-utils/index.mjs";
|
|
44
47
|
import { twoFactor } from "./two-factor/index.mjs";
|
|
45
48
|
import { username } from "./username/index.mjs";
|
|
46
|
-
export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth };
|
|
49
|
+
export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, POPUP_MARKER_COOKIE, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth };
|
|
@@ -144,7 +144,7 @@ const jwt = (options) => {
|
|
|
144
144
|
const jwt = await getJwtToken(ctx, options);
|
|
145
145
|
return ctx.json({ token: jwt });
|
|
146
146
|
}),
|
|
147
|
-
signJWT: createAuthEndpoint({
|
|
147
|
+
signJWT: createAuthEndpoint.serverOnly({
|
|
148
148
|
method: "POST",
|
|
149
149
|
metadata: { $Infer: { body: {} } },
|
|
150
150
|
body: signJWTBodySchema
|
|
@@ -158,7 +158,7 @@ const jwt = (options) => {
|
|
|
158
158
|
});
|
|
159
159
|
return c.json({ token: jwt });
|
|
160
160
|
}),
|
|
161
|
-
verifyJWT: createAuthEndpoint({
|
|
161
|
+
verifyJWT: createAuthEndpoint.serverOnly({
|
|
162
162
|
method: "POST",
|
|
163
163
|
metadata: { $Infer: {
|
|
164
164
|
body: {},
|
|
@@ -65,6 +65,7 @@ function createMcpAuthClient(options) {
|
|
|
65
65
|
if (!response.ok) return null;
|
|
66
66
|
const data = await response.json();
|
|
67
67
|
if (!data || !data.userId) return null;
|
|
68
|
+
if (data.accessTokenExpiresAt && new Date(data.accessTokenExpiresAt).getTime() < Date.now()) return null;
|
|
68
69
|
return data;
|
|
69
70
|
} catch {
|
|
70
71
|
return null;
|