@connextar/house 0.11.0 → 0.13.0
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/README.md +581 -19
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/second-factor-index.d.ts +7 -0
- package/dist/auth/second-factor-index.d.ts.map +1 -0
- package/dist/auth/second-factor-index.js +7 -0
- package/dist/auth/second-factor-index.js.map +1 -0
- package/dist/auth/second-factor-next.d.ts +39 -0
- package/dist/auth/second-factor-next.d.ts.map +1 -0
- package/dist/auth/second-factor-next.js +131 -0
- package/dist/auth/second-factor-next.js.map +1 -0
- package/dist/auth/second-factor-routes.d.ts +29 -0
- package/dist/auth/second-factor-routes.d.ts.map +1 -0
- package/dist/auth/second-factor-routes.js +35 -0
- package/dist/auth/second-factor-routes.js.map +1 -0
- package/dist/auth/second-factor.d.ts +117 -0
- package/dist/auth/second-factor.d.ts.map +1 -0
- package/dist/auth/second-factor.js +127 -0
- package/dist/auth/second-factor.js.map +1 -0
- package/dist/auth/two-factor-policy.d.ts +30 -1
- package/dist/auth/two-factor-policy.d.ts.map +1 -1
- package/dist/auth/two-factor-policy.js +39 -1
- package/dist/auth/two-factor-policy.js.map +1 -1
- package/dist/jobs/health.d.ts +17 -4
- package/dist/jobs/health.d.ts.map +1 -1
- package/dist/jobs/health.js +10 -7
- package/dist/jobs/health.js.map +1 -1
- package/dist/jobs/index.d.ts +4 -3
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +3 -2
- package/dist/jobs/index.js.map +1 -1
- package/dist/jobs/job-health-list.d.ts +15 -3
- package/dist/jobs/job-health-list.d.ts.map +1 -1
- package/dist/jobs/job-health-list.js +11 -7
- package/dist/jobs/job-health-list.js.map +1 -1
- package/dist/jobs/prune.d.ts +45 -0
- package/dist/jobs/prune.d.ts.map +1 -0
- package/dist/jobs/prune.js +58 -0
- package/dist/jobs/prune.js.map +1 -0
- package/dist/jobs/registry.d.ts +19 -4
- package/dist/jobs/registry.d.ts.map +1 -1
- package/dist/jobs/registry.js +16 -1
- package/dist/jobs/registry.js.map +1 -1
- package/dist/jobs/scheduler.d.ts +12 -1
- package/dist/jobs/scheduler.d.ts.map +1 -1
- package/dist/jobs/scheduler.js +24 -9
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/jobs/store.d.ts +26 -1
- package/dist/jobs/store.d.ts.map +1 -1
- package/dist/jobs/store.js +35 -4
- package/dist/jobs/store.js.map +1 -1
- package/dist/password/common.d.ts +2 -0
- package/dist/password/common.d.ts.map +1 -0
- package/dist/password/common.js +58 -0
- package/dist/password/common.js.map +1 -0
- package/dist/password/estimate-index.d.ts +10 -0
- package/dist/password/estimate-index.d.ts.map +1 -0
- package/dist/password/estimate-index.js +10 -0
- package/dist/password/estimate-index.js.map +1 -0
- package/dist/password/estimate.d.ts +65 -0
- package/dist/password/estimate.d.ts.map +1 -0
- package/dist/password/estimate.js +612 -0
- package/dist/password/estimate.js.map +1 -0
- package/dist/password/index.d.ts +22 -0
- package/dist/password/index.d.ts.map +1 -0
- package/dist/password/index.js +22 -0
- package/dist/password/index.js.map +1 -0
- package/dist/password/password-input.d.ts +55 -0
- package/dist/password/password-input.d.ts.map +1 -0
- package/dist/password/password-input.js +54 -0
- package/dist/password/password-input.js.map +1 -0
- package/dist/password/password-strength.d.ts +59 -0
- package/dist/password/password-strength.d.ts.map +1 -0
- package/dist/password/password-strength.js +74 -0
- package/dist/password/password-strength.js.map +1 -0
- package/dist/password/reveal.d.ts +56 -0
- package/dist/password/reveal.d.ts.map +1 -0
- package/dist/password/reveal.js +109 -0
- package/dist/password/reveal.js.map +1 -0
- package/package.json +19 -1
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export { TWO_FACTOR_BLOCKED_MESSAGE, TWO_FACTOR_BLOCKED_PATHS, isTwoFactorBlockedPath, shouldBlockForTwoFactor, totpSecretFromUri, type TwoFactorBlockedPath, type TwoFactorLookup, } from "./two-factor-policy.js";
|
|
1
|
+
export { TWO_FACTOR_BLOCKED_MESSAGE, TWO_FACTOR_BLOCKED_PATHS, isTwoFactorBlockedPath, magicLinkRefusalRetired, shouldBlockForTwoFactor, totpSecretFromUri, type AuthContextLike, type TwoFactorBlockedPath, type TwoFactorLookup, } from "./two-factor-policy.js";
|
|
2
|
+
export { DEFAULT_CODE_SCREEN_PATH, DEFAULT_NEXT_PARAM, SECOND_FACTOR_CHALLENGE_ROUTES, SECOND_FACTOR_EVERYWHERE_ID, type SecondFactorChallengeRoute, } from "./second-factor-routes.js";
|
|
3
|
+
export { magicLinkConfirmHref, magicLinkVerifyHref, safeNextPath, secondFactorNext } from "./second-factor-next.js";
|
|
2
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,8BAA8B,EAC9B,2BAA2B,EAC3B,KAAK,0BAA0B,GAChC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/auth/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export { TWO_FACTOR_BLOCKED_MESSAGE, TWO_FACTOR_BLOCKED_PATHS, isTwoFactorBlockedPath, shouldBlockForTwoFactor, totpSecretFromUri, } from "./two-factor-policy.js";
|
|
1
|
+
export { TWO_FACTOR_BLOCKED_MESSAGE, TWO_FACTOR_BLOCKED_PATHS, isTwoFactorBlockedPath, magicLinkRefusalRetired, shouldBlockForTwoFactor, totpSecretFromUri, } from "./two-factor-policy.js";
|
|
2
|
+
export { DEFAULT_CODE_SCREEN_PATH, DEFAULT_NEXT_PARAM, SECOND_FACTOR_CHALLENGE_ROUTES, SECOND_FACTOR_EVERYWHERE_ID, } from "./second-factor-routes.js";
|
|
3
|
+
export { magicLinkConfirmHref, magicLinkVerifyHref, safeNextPath, secondFactorNext } from "./second-factor-next.js";
|
|
2
4
|
//# sourceMappingURL=index.js.map
|
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,GAIlB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,8BAA8B,EAC9B,2BAA2B,GAE5B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/auth/second-factor` — the server half: a Better Auth plugin. Kept off `@connextar/house/auth`
|
|
3
|
+
* so that entry point stays free of any Better Auth import and safe for client components.
|
|
4
|
+
*/
|
|
5
|
+
export { findTwoFactorSignInHook, secondFactorEverywhere, type SecondFactorEverywhereOptions, } from "./second-factor.js";
|
|
6
|
+
export { DEFAULT_CODE_SCREEN_PATH, DEFAULT_NEXT_PARAM, SECOND_FACTOR_CHALLENGE_ROUTES, SECOND_FACTOR_EVERYWHERE_ID, type SecondFactorChallengeRoute, } from "./second-factor-routes.js";
|
|
7
|
+
//# sourceMappingURL=second-factor-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor-index.d.ts","sourceRoot":"","sources":["../../src/auth/second-factor-index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,KAAK,6BAA6B,GACnC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,8BAA8B,EAC9B,2BAA2B,EAC3B,KAAK,0BAA0B,GAChC,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/auth/second-factor` — the server half: a Better Auth plugin. Kept off `@connextar/house/auth`
|
|
3
|
+
* so that entry point stays free of any Better Auth import and safe for client components.
|
|
4
|
+
*/
|
|
5
|
+
export { findTwoFactorSignInHook, secondFactorEverywhere, } from "./second-factor.js";
|
|
6
|
+
export { DEFAULT_CODE_SCREEN_PATH, DEFAULT_NEXT_PARAM, SECOND_FACTOR_CHALLENGE_ROUTES, SECOND_FACTOR_EVERYWHERE_ID, } from "./second-factor-routes.js";
|
|
7
|
+
//# sourceMappingURL=second-factor-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor-index.js","sourceRoot":"","sources":["../../src/auth/second-factor-index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,uBAAuB,EACvB,sBAAsB,GAEvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,8BAA8B,EAC9B,2BAA2B,GAE5B,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `candidate` if it is a same-site path, otherwise `fallback`. The result always starts with a single `/`, so it is
|
|
3
|
+
* safe to hand straight to `router.replace()` or `redirect()`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function safeNextPath(candidate: string | null | undefined, fallback?: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* The code screen's `next`, made safe. Pass the page's search params (a `URLSearchParams`, Next's
|
|
8
|
+
* `ReadonlyURLSearchParams`, or a raw `?a=b` string); read it once the code — or a backup code — has been accepted:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const { error } = await authClient.twoFactor.verifyTotp({ code, trustDevice });
|
|
12
|
+
* if (!error) router.replace(secondFactorNext(useSearchParams()));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function secondFactorNext(search: string | {
|
|
16
|
+
get(name: string): string | null;
|
|
17
|
+
} | null | undefined, options?: {
|
|
18
|
+
param?: string;
|
|
19
|
+
fallback?: string;
|
|
20
|
+
}): string;
|
|
21
|
+
/**
|
|
22
|
+
* The same-site path of `location` (absolute or relative) on `origin`, or null when it points anywhere else.
|
|
23
|
+
* Used by the server to turn a sign-in route's `callbackURL` into the code screen's `next`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sameSitePath(location: string | null | undefined, origin: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* The code screen's URL on `origin`, carrying `next` when it is worth carrying: a safe same-site path that is not
|
|
28
|
+
* the site root (the default anyway) and not the code screen itself (which would loop).
|
|
29
|
+
*/
|
|
30
|
+
export declare function codeScreenUrl(origin: string, codeScreenPath: string, next: string | null, param?: string): string;
|
|
31
|
+
export declare function magicLinkConfirmHref(verifyUrl: string, confirmPath: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* The verify URL to send the "Continue" button to, or null when the page was opened without a token. `verifyPath`
|
|
34
|
+
* is the app's own route, e.g. `/api/auth/magic-link/verify`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function magicLinkVerifyHref(search: string | {
|
|
37
|
+
get(name: string): string | null;
|
|
38
|
+
}, verifyPath: string): string | null;
|
|
39
|
+
//# sourceMappingURL=second-factor-next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor-next.d.ts","sourceRoot":"","sources":["../../src/auth/second-factor-next.ts"],"names":[],"mappings":"AAcA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,SAAM,GAAG,MAAM,CAezF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,GAAG;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACxE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,KAAK,GAAE,MAA2B,GACjC,MAAM,CAIR;AAkBD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAQnF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,GAAG;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,EACrD,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAaf"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { DEFAULT_NEXT_PARAM } from "./second-factor-routes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where to send someone once their second factor checks out — and, as importantly, where not to.
|
|
4
|
+
*
|
|
5
|
+
* The code screen is reached by a redirect that carries the page the person was originally heading for as `?next=`.
|
|
6
|
+
* Anything in a URL is attacker-controlled, so `next` is only ever honoured as a path on this site: never another
|
|
7
|
+
* origin, never a protocol-relative `//host`, never a backslash (browsers read `/\host` as `//host`), never a
|
|
8
|
+
* `javascript:` URL, never control characters. Anything else falls back.
|
|
9
|
+
*/
|
|
10
|
+
const PROBE_ORIGIN = "http://next.invalid";
|
|
11
|
+
const MAX_NEXT_LENGTH = 2048;
|
|
12
|
+
/**
|
|
13
|
+
* `candidate` if it is a same-site path, otherwise `fallback`. The result always starts with a single `/`, so it is
|
|
14
|
+
* safe to hand straight to `router.replace()` or `redirect()`.
|
|
15
|
+
*/
|
|
16
|
+
export function safeNextPath(candidate, fallback = "/") {
|
|
17
|
+
if (typeof candidate !== "string" || candidate.length === 0 || candidate.length > MAX_NEXT_LENGTH)
|
|
18
|
+
return fallback;
|
|
19
|
+
if (!candidate.startsWith("/") || candidate.startsWith("//"))
|
|
20
|
+
return fallback;
|
|
21
|
+
// Backslashes and control characters are normalised away by browsers in ways that turn `/\evil.com` into
|
|
22
|
+
// `//evil.com`; nothing legitimate needs them in a path we generated.
|
|
23
|
+
// eslint-disable-next-line no-control-regex
|
|
24
|
+
if (/[\\\u0000-\u001f\u007f]/.test(candidate))
|
|
25
|
+
return fallback;
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(candidate, PROBE_ORIGIN);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
if (url.origin !== PROBE_ORIGIN)
|
|
34
|
+
return fallback;
|
|
35
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The code screen's `next`, made safe. Pass the page's search params (a `URLSearchParams`, Next's
|
|
39
|
+
* `ReadonlyURLSearchParams`, or a raw `?a=b` string); read it once the code — or a backup code — has been accepted:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const { error } = await authClient.twoFactor.verifyTotp({ code, trustDevice });
|
|
43
|
+
* if (!error) router.replace(secondFactorNext(useSearchParams()));
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function secondFactorNext(search, options = {}) {
|
|
47
|
+
const param = options.param ?? DEFAULT_NEXT_PARAM;
|
|
48
|
+
const fallback = options.fallback ?? "/";
|
|
49
|
+
if (!search)
|
|
50
|
+
return fallback;
|
|
51
|
+
const params = typeof search === "string" ? new URLSearchParams(search) : search;
|
|
52
|
+
return safeNextPath(params.get(param), fallback);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The same-site path of `location` (absolute or relative) on `origin`, or null when it points anywhere else.
|
|
56
|
+
* Used by the server to turn a sign-in route's `callbackURL` into the code screen's `next`.
|
|
57
|
+
*/
|
|
58
|
+
export function sameSitePath(location, origin) {
|
|
59
|
+
if (!location)
|
|
60
|
+
return null;
|
|
61
|
+
let url;
|
|
62
|
+
let base;
|
|
63
|
+
try {
|
|
64
|
+
base = new URL(origin);
|
|
65
|
+
url = new URL(location, base);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (url.origin !== base.origin)
|
|
71
|
+
return null;
|
|
72
|
+
const path = safeNextPath(`${url.pathname}${url.search}${url.hash}`, "");
|
|
73
|
+
return path === "" ? null : path;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The code screen's URL on `origin`, carrying `next` when it is worth carrying: a safe same-site path that is not
|
|
77
|
+
* the site root (the default anyway) and not the code screen itself (which would loop).
|
|
78
|
+
*/
|
|
79
|
+
export function codeScreenUrl(origin, codeScreenPath, next, param = DEFAULT_NEXT_PARAM) {
|
|
80
|
+
const url = new URL(codeScreenPath, origin);
|
|
81
|
+
if (next && next !== "/" && new URL(next, origin).pathname !== url.pathname)
|
|
82
|
+
url.searchParams.set(param, next);
|
|
83
|
+
return url.toString();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Email link scanners (Microsoft Defender Safe Links, Mimecast, Proofpoint…) open links before the person does.
|
|
87
|
+
* A magic link is single-use, so the scanner spends it: without two-factor the scanner is handed a session it
|
|
88
|
+
* throws away, with two-factor it is handed the pending code state — and either way the person's own click then
|
|
89
|
+
* finds the link used.
|
|
90
|
+
*
|
|
91
|
+
* The mitigation is a confirm step: email a link to a page of the app that shows a "Continue signing in" button,
|
|
92
|
+
* and only that button goes to Better Auth's verify URL. Scanners fetch; they do not press buttons.
|
|
93
|
+
*
|
|
94
|
+
* `magicLinkConfirmHref()` turns the verify URL Better Auth gives `sendMagicLink` into that page's URL, carrying
|
|
95
|
+
* only the token and the redirect targets; `magicLinkVerifyHref()` rebuilds the verify URL on the page, from the
|
|
96
|
+
* app's own fixed verify path — never from a URL taken off the query string, which would be an open redirect. The
|
|
97
|
+
* verify endpoint still origin-checks the redirect targets itself.
|
|
98
|
+
*/
|
|
99
|
+
const MAGIC_LINK_PARAMS = ["token", "callbackURL", "newUserCallbackURL", "errorCallbackURL"];
|
|
100
|
+
export function magicLinkConfirmHref(verifyUrl, confirmPath) {
|
|
101
|
+
const source = new URL(verifyUrl);
|
|
102
|
+
const target = new URL(confirmPath, source.origin);
|
|
103
|
+
for (const name of MAGIC_LINK_PARAMS) {
|
|
104
|
+
const value = source.searchParams.get(name);
|
|
105
|
+
if (value !== null)
|
|
106
|
+
target.searchParams.set(name, value);
|
|
107
|
+
}
|
|
108
|
+
return target.toString();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The verify URL to send the "Continue" button to, or null when the page was opened without a token. `verifyPath`
|
|
112
|
+
* is the app's own route, e.g. `/api/auth/magic-link/verify`.
|
|
113
|
+
*/
|
|
114
|
+
export function magicLinkVerifyHref(search, verifyPath) {
|
|
115
|
+
const params = typeof search === "string" ? new URLSearchParams(search) : search;
|
|
116
|
+
const token = params.get("token");
|
|
117
|
+
if (!token)
|
|
118
|
+
return null;
|
|
119
|
+
const out = new URLSearchParams();
|
|
120
|
+
for (const name of MAGIC_LINK_PARAMS) {
|
|
121
|
+
const value = name === "token" ? token : params.get(name);
|
|
122
|
+
if (value === null)
|
|
123
|
+
continue;
|
|
124
|
+
// Redirect targets must be same-site paths here; Better Auth resolves them against its own base URL.
|
|
125
|
+
if (name !== "token" && safeNextPath(value, "") === "")
|
|
126
|
+
continue;
|
|
127
|
+
out.set(name, value);
|
|
128
|
+
}
|
|
129
|
+
return `${safeNextPath(verifyPath)}?${out.toString()}`;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=second-factor-next.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor-next.js","sourceRoot":"","sources":["../../src/auth/second-factor-next.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary shared by the server plugin (`@connextar/house/auth/second-factor`) and the pure helpers in
|
|
3
|
+
* `@connextar/house/auth`. Kept free of any Better Auth import so a client component or a before-hook can read it
|
|
4
|
+
* without pulling the server in.
|
|
5
|
+
*/
|
|
6
|
+
/** The Better Auth plugin id of `secondFactorEverywhere()`. */
|
|
7
|
+
export declare const SECOND_FACTOR_EVERYWHERE_ID = "second-factor-everywhere";
|
|
8
|
+
/**
|
|
9
|
+
* Every route that hands out a session without a password, and so without Better Auth's two-factor check — the
|
|
10
|
+
* ones `secondFactorEverywhere()` can put the code screen behind.
|
|
11
|
+
*
|
|
12
|
+
* - `/magic-link/verify` — the link in the email.
|
|
13
|
+
* - `/callback/:id` — Google, Microsoft, Apple and every other built-in social provider's OAuth callback.
|
|
14
|
+
* - `/oauth2/callback/:providerId` — the generic-OAuth plugin's callback.
|
|
15
|
+
* - `/sign-in/social` — only when called with an `idToken` (native Google/Apple sign-in); the redirect flow makes no
|
|
16
|
+
* session here, so there is nothing to challenge.
|
|
17
|
+
* - `/one-tap/callback` — Google One Tap.
|
|
18
|
+
* - `/sign-in/email-otp` — the email-OTP plugin's sign-in.
|
|
19
|
+
*
|
|
20
|
+
* Passkeys (`/passkey/verify-authentication`) are deliberately absent and cannot be added: a passkey is possession
|
|
21
|
+
* of a device and phishing-resistant, at least as strong as password + code, so it stays a single step.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SECOND_FACTOR_CHALLENGE_ROUTES: readonly ["/magic-link/verify", "/callback/:id", "/oauth2/callback/:providerId", "/sign-in/social", "/one-tap/callback", "/sign-in/email-otp"];
|
|
24
|
+
export type SecondFactorChallengeRoute = (typeof SECOND_FACTOR_CHALLENGE_ROUTES)[number];
|
|
25
|
+
/** Where the code screen lives unless an app says otherwise — the path every house app already uses. */
|
|
26
|
+
export declare const DEFAULT_CODE_SCREEN_PATH = "/auth/two-factor";
|
|
27
|
+
/** The query parameter that carries "where to go once the code checks out". */
|
|
28
|
+
export declare const DEFAULT_NEXT_PARAM = "next";
|
|
29
|
+
//# sourceMappingURL=second-factor-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor-routes.d.ts","sourceRoot":"","sources":["../../src/auth/second-factor-routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+DAA+D;AAC/D,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,8BAA8B,gJAOjC,CAAC;AAEX,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,8BAA8B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzF,wGAAwG;AACxG,eAAO,MAAM,wBAAwB,qBAAqB,CAAC;AAE3D,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB,SAAS,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary shared by the server plugin (`@connextar/house/auth/second-factor`) and the pure helpers in
|
|
3
|
+
* `@connextar/house/auth`. Kept free of any Better Auth import so a client component or a before-hook can read it
|
|
4
|
+
* without pulling the server in.
|
|
5
|
+
*/
|
|
6
|
+
/** The Better Auth plugin id of `secondFactorEverywhere()`. */
|
|
7
|
+
export const SECOND_FACTOR_EVERYWHERE_ID = "second-factor-everywhere";
|
|
8
|
+
/**
|
|
9
|
+
* Every route that hands out a session without a password, and so without Better Auth's two-factor check — the
|
|
10
|
+
* ones `secondFactorEverywhere()` can put the code screen behind.
|
|
11
|
+
*
|
|
12
|
+
* - `/magic-link/verify` — the link in the email.
|
|
13
|
+
* - `/callback/:id` — Google, Microsoft, Apple and every other built-in social provider's OAuth callback.
|
|
14
|
+
* - `/oauth2/callback/:providerId` — the generic-OAuth plugin's callback.
|
|
15
|
+
* - `/sign-in/social` — only when called with an `idToken` (native Google/Apple sign-in); the redirect flow makes no
|
|
16
|
+
* session here, so there is nothing to challenge.
|
|
17
|
+
* - `/one-tap/callback` — Google One Tap.
|
|
18
|
+
* - `/sign-in/email-otp` — the email-OTP plugin's sign-in.
|
|
19
|
+
*
|
|
20
|
+
* Passkeys (`/passkey/verify-authentication`) are deliberately absent and cannot be added: a passkey is possession
|
|
21
|
+
* of a device and phishing-resistant, at least as strong as password + code, so it stays a single step.
|
|
22
|
+
*/
|
|
23
|
+
export const SECOND_FACTOR_CHALLENGE_ROUTES = [
|
|
24
|
+
"/magic-link/verify",
|
|
25
|
+
"/callback/:id",
|
|
26
|
+
"/oauth2/callback/:providerId",
|
|
27
|
+
"/sign-in/social",
|
|
28
|
+
"/one-tap/callback",
|
|
29
|
+
"/sign-in/email-otp",
|
|
30
|
+
];
|
|
31
|
+
/** Where the code screen lives unless an app says otherwise — the path every house app already uses. */
|
|
32
|
+
export const DEFAULT_CODE_SCREEN_PATH = "/auth/two-factor";
|
|
33
|
+
/** The query parameter that carries "where to go once the code checks out". */
|
|
34
|
+
export const DEFAULT_NEXT_PARAM = "next";
|
|
35
|
+
//# sourceMappingURL=second-factor-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor-routes.js","sourceRoot":"","sources":["../../src/auth/second-factor-routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,2BAA2B,GAAG,0BAA0B,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG;IAC5C,oBAAoB;IACpB,eAAe;IACf,8BAA8B;IAC9B,iBAAiB;IACjB,mBAAmB;IACnB,oBAAoB;CACZ,CAAC;AAIX,wGAAwG;AACxG,MAAM,CAAC,MAAM,wBAAwB,GAAG,kBAAkB,CAAC;AAE3D,+EAA+E;AAC/E,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { BetterAuthPlugin } from "better-auth";
|
|
2
|
+
import { type SecondFactorChallengeRoute } from "./second-factor-routes.js";
|
|
3
|
+
/**
|
|
4
|
+
* The second factor after a magic link and after a social sign-in — every sign-in path consistent.
|
|
5
|
+
*
|
|
6
|
+
* Better Auth's `twoFactor` plugin swaps the session for a pending "enter your code" state only on the password
|
|
7
|
+
* routes (`/sign-in/email`, `/sign-in/username`, `/sign-in/phone-number`). A magic link (`/magic-link/verify`) and a
|
|
8
|
+
* social callback (`/callback/:id`) hand out a full session with no code. This plugin runs **the two-factor plugin's
|
|
9
|
+
* own after-hook** on those routes too, so an account with two-factor on gets exactly what a password sign-in gets:
|
|
10
|
+
*
|
|
11
|
+
* - no session survives — the one the route just made is deleted and its cookies expired;
|
|
12
|
+
* - the pending state is Better Auth's own (its signed `two_factor` cookie and verification row), so
|
|
13
|
+
* `/two-factor/verify-totp`, `/two-factor/verify-backup-code` and `/two-factor/verify-otp` finish the sign-in
|
|
14
|
+
* unchanged — trust-this-device, attempt limits, account lockout and all;
|
|
15
|
+
* - a device with a valid trust cookie skips the code, by the plugin's own rule, and has the cookie rotated;
|
|
16
|
+
* - a browser redirect (the link, the OAuth callback) is sent to the app's code screen instead of `callbackURL`, with
|
|
17
|
+
* the original destination carried as a same-site `?next=` — anything off-site is dropped;
|
|
18
|
+
* - a JSON caller (magic-link verify without a `callbackURL`, `/sign-in/social` with an `idToken`) gets the plugin's
|
|
19
|
+
* own `{ twoFactorRedirect: true }`, which `twoFactorClient`'s `onTwoFactorRedirect` already handles.
|
|
20
|
+
*
|
|
21
|
+
* Nothing about Better Auth's pending state is copied here — no cookie names, no signing, no verification
|
|
22
|
+
* identifiers — so a Better Auth upgrade that changes them changes them here too. What this relies on is that the
|
|
23
|
+
* two-factor plugin exposes its after-hook, that the hook reads `ctx.context.newSession`, and that it answers
|
|
24
|
+
* `{ twoFactorRedirect: true }` when it takes the session away; `second-factor.test.ts` pins each of those against
|
|
25
|
+
* the installed Better Auth, and `init` refuses to start if the hook cannot be found.
|
|
26
|
+
*
|
|
27
|
+
* Passkeys are untouched: a passkey is possession of a device and phishing-resistant, so it stays one step.
|
|
28
|
+
*
|
|
29
|
+
* **Order**: list it after `twoFactor()` and before `nextCookies()`. It composes with an app's own
|
|
30
|
+
* `second-factor-session` plugin unchanged: the session a magic link or social callback creates is deleted before the
|
|
31
|
+
* response leaves, and the session that counts is created by `/two-factor/verify-totp` (or the backup-code route),
|
|
32
|
+
* which those plugins already record as second-factor-proven.
|
|
33
|
+
*/
|
|
34
|
+
export interface SecondFactorEverywhereOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Turn the challenge off for this app (`false`) while keeping the plugin listed. With it off, `magicLinkRefusalRetired()`
|
|
37
|
+
* answers false again, so an app still using the old magic-link refusal gets it back. Default `true`.
|
|
38
|
+
*/
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
/** The app's code screen, a path on the app's origin. Default `/auth/two-factor`. */
|
|
41
|
+
codeScreenPath?: string;
|
|
42
|
+
/** The query parameter that carries the original destination to the code screen. Default `next`. */
|
|
43
|
+
nextParam?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The origin the code screen (and every acceptable `next`) lives on. Default: the origin of Better Auth's
|
|
46
|
+
* `baseURL`, which is the app itself whenever auth is mounted under the app (`/api/auth`).
|
|
47
|
+
*/
|
|
48
|
+
appOrigin?: string;
|
|
49
|
+
/** Which routes get the challenge. Default: all of {@link SECOND_FACTOR_CHALLENGE_ROUTES}. */
|
|
50
|
+
routes?: readonly SecondFactorChallengeRoute[];
|
|
51
|
+
/**
|
|
52
|
+
* Which OAuth providers get the challenge on the social routes (`/callback/:id`, `/oauth2/callback/:providerId`,
|
|
53
|
+
* `/sign-in/social`, `/one-tap/callback` counts as `google`). Default `"all"`.
|
|
54
|
+
*/
|
|
55
|
+
providers?: "all" | readonly string[];
|
|
56
|
+
}
|
|
57
|
+
/** The slice of the endpoint context an after-hook is handed that this reads. */
|
|
58
|
+
interface AfterHookInput {
|
|
59
|
+
path?: string;
|
|
60
|
+
params?: Record<string, string | undefined> | null;
|
|
61
|
+
body?: unknown;
|
|
62
|
+
context: {
|
|
63
|
+
baseURL: string;
|
|
64
|
+
returned?: unknown;
|
|
65
|
+
responseHeaders?: Headers | null;
|
|
66
|
+
newSession?: {
|
|
67
|
+
user?: {
|
|
68
|
+
twoFactorEnabled?: boolean | null;
|
|
69
|
+
} | null;
|
|
70
|
+
} | null;
|
|
71
|
+
options: {
|
|
72
|
+
plugins?: readonly BetterAuthPlugin[] | null;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
interface HookResult {
|
|
77
|
+
response?: unknown;
|
|
78
|
+
headers?: Headers | null;
|
|
79
|
+
}
|
|
80
|
+
type RawHook = {
|
|
81
|
+
matcher: (context: AfterHookInput) => boolean;
|
|
82
|
+
handler: (context: AfterHookInput) => Promise<HookResult | undefined>;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The two-factor plugin's own after-hook: the one that matches a password sign-in. Exported for the pinning tests.
|
|
86
|
+
* Returns null when the plugin is missing, or no longer exposes a hook this way.
|
|
87
|
+
*/
|
|
88
|
+
export declare function findTwoFactorSignInHook(plugins: readonly BetterAuthPlugin[] | null | undefined): RawHook | null;
|
|
89
|
+
export declare function secondFactorEverywhere(options?: SecondFactorEverywhereOptions): {
|
|
90
|
+
id: "second-factor-everywhere";
|
|
91
|
+
options: {
|
|
92
|
+
enabled: boolean;
|
|
93
|
+
codeScreenPath: string;
|
|
94
|
+
nextParam: string;
|
|
95
|
+
/**
|
|
96
|
+
* The origin the code screen (and every acceptable `next`) lives on. Default: the origin of Better Auth's
|
|
97
|
+
* `baseURL`, which is the app itself whenever auth is mounted under the app (`/api/auth`).
|
|
98
|
+
*/
|
|
99
|
+
appOrigin?: string;
|
|
100
|
+
/** Which routes get the challenge. Default: all of {@link SECOND_FACTOR_CHALLENGE_ROUTES}. */
|
|
101
|
+
routes?: readonly SecondFactorChallengeRoute[];
|
|
102
|
+
/**
|
|
103
|
+
* Which OAuth providers get the challenge on the social routes (`/callback/:id`, `/oauth2/callback/:providerId`,
|
|
104
|
+
* `/sign-in/social`, `/one-tap/callback` counts as `google`). Default `"all"`.
|
|
105
|
+
*/
|
|
106
|
+
providers?: "all" | readonly string[];
|
|
107
|
+
};
|
|
108
|
+
init(ctx: import("better-auth").AuthContext): void;
|
|
109
|
+
hooks: {
|
|
110
|
+
after: {
|
|
111
|
+
matcher: (context: import("better-auth").HookEndpointContext) => boolean;
|
|
112
|
+
handler: import("better-auth/api").AuthMiddleware;
|
|
113
|
+
}[];
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
export {};
|
|
117
|
+
//# sourceMappingURL=second-factor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor.d.ts","sourceRoot":"","sources":["../../src/auth/second-factor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,EAKL,KAAK,0BAA0B,EAChC,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oGAAoG;IACpG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IAC/C;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,SAAS,MAAM,EAAE,CAAC;CACvC;AAKD,iFAAiF;AACjF,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IACnD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,eAAe,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QACjC,UAAU,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE;gBAAE,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;aAAE,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;QAC5E,OAAO,EAAE;YAAE,OAAO,CAAC,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAA;SAAE,CAAC;KAC3D,CAAC;CACH;AAED,UAAU,UAAU;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1B;AAED,KAAK,OAAO,GAAG;IACb,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC;IAC9C,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;CACvE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAY/G;AA8CD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,6BAAkC;;;;;;QAvGhF;;;WAGG;oBACS,MAAM;QAClB,8FAA8F;iBACrF,SAAS,0BAA0B,EAAE;QAC9C;;;WAGG;oBACS,KAAK,GAAG,SAAS,MAAM,EAAE;;;;;;;;;EAiKtC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { APIError, isAPIError } from "better-auth/api";
|
|
2
|
+
import { codeScreenUrl, sameSitePath } from "./second-factor-next.js";
|
|
3
|
+
import { DEFAULT_CODE_SCREEN_PATH, DEFAULT_NEXT_PARAM, SECOND_FACTOR_CHALLENGE_ROUTES, SECOND_FACTOR_EVERYWHERE_ID, } from "./second-factor-routes.js";
|
|
4
|
+
/**
|
|
5
|
+
* The two-factor plugin's own after-hook: the one that matches a password sign-in. Exported for the pinning tests.
|
|
6
|
+
* Returns null when the plugin is missing, or no longer exposes a hook this way.
|
|
7
|
+
*/
|
|
8
|
+
export function findTwoFactorSignInHook(plugins) {
|
|
9
|
+
const plugin = plugins?.find((p) => p.id === "two-factor");
|
|
10
|
+
const hooks = (plugin?.hooks?.after ?? []);
|
|
11
|
+
const probe = { path: "/sign-in/email" };
|
|
12
|
+
for (const hook of hooks) {
|
|
13
|
+
try {
|
|
14
|
+
if (typeof hook.handler === "function" && hook.matcher(probe))
|
|
15
|
+
return hook;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// A matcher that needs more than a path is not the one we are looking for.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/** The provider a social route is completing, or null on routes that have none. */
|
|
24
|
+
function providerOf(input) {
|
|
25
|
+
switch (input.path) {
|
|
26
|
+
case "/callback/:id":
|
|
27
|
+
return input.params?.id ?? null;
|
|
28
|
+
case "/oauth2/callback/:providerId":
|
|
29
|
+
return input.params?.providerId ?? null;
|
|
30
|
+
case "/sign-in/social": {
|
|
31
|
+
const provider = input.body?.provider;
|
|
32
|
+
return typeof provider === "string" ? provider : null;
|
|
33
|
+
}
|
|
34
|
+
case "/one-tap/callback":
|
|
35
|
+
return "google";
|
|
36
|
+
default:
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function jsonBody(response) {
|
|
41
|
+
// `ctx.json()` answers the plain object inside hooks, and a `{ _flag: "json", body }` wrapper when the caller
|
|
42
|
+
// asked for a Response; accept either.
|
|
43
|
+
if (response && typeof response === "object" && response._flag === "json") {
|
|
44
|
+
return response.body;
|
|
45
|
+
}
|
|
46
|
+
return response;
|
|
47
|
+
}
|
|
48
|
+
function isTwoFactorRedirect(response) {
|
|
49
|
+
const body = jsonBody(response);
|
|
50
|
+
return !!body && typeof body === "object" && body.twoFactorRedirect === true;
|
|
51
|
+
}
|
|
52
|
+
function redirectLocation(input) {
|
|
53
|
+
const returned = input.context.returned;
|
|
54
|
+
if (!isAPIError(returned))
|
|
55
|
+
return null;
|
|
56
|
+
const status = returned.statusCode;
|
|
57
|
+
if (status === undefined || status < 300 || status >= 400)
|
|
58
|
+
return null;
|
|
59
|
+
return (input.context.responseHeaders?.get("location") ??
|
|
60
|
+
returned.headers?.get?.("location") ??
|
|
61
|
+
null);
|
|
62
|
+
}
|
|
63
|
+
export function secondFactorEverywhere(options = {}) {
|
|
64
|
+
const enabled = options.enabled !== false;
|
|
65
|
+
const routes = new Set(options.routes ?? SECOND_FACTOR_CHALLENGE_ROUTES);
|
|
66
|
+
const providers = options.providers ?? "all";
|
|
67
|
+
const codeScreenPath = options.codeScreenPath ?? DEFAULT_CODE_SCREEN_PATH;
|
|
68
|
+
const nextParam = options.nextParam ?? DEFAULT_NEXT_PARAM;
|
|
69
|
+
if (!codeScreenPath.startsWith("/") || codeScreenPath.startsWith("//")) {
|
|
70
|
+
throw new Error(`[second-factor-everywhere] codeScreenPath must be a path on the app, got "${codeScreenPath}".`);
|
|
71
|
+
}
|
|
72
|
+
function applies(input) {
|
|
73
|
+
if (!enabled || !input.path || !routes.has(input.path))
|
|
74
|
+
return false;
|
|
75
|
+
const provider = providerOf(input);
|
|
76
|
+
if (provider === null || providers === "all")
|
|
77
|
+
return true;
|
|
78
|
+
return providers.includes(provider);
|
|
79
|
+
}
|
|
80
|
+
async function handler(input) {
|
|
81
|
+
// Only a sign-in that actually made a session for a two-factor account is ours. Account linking, errors, the
|
|
82
|
+
// redirect leg of `/sign-in/social` and accounts without two-factor all fall through untouched.
|
|
83
|
+
if (!input.context.newSession?.user?.twoFactorEnabled)
|
|
84
|
+
return {};
|
|
85
|
+
const twoFactorHook = findTwoFactorSignInHook(input.context.options.plugins);
|
|
86
|
+
// `init` already refused to start without it; if it has gone missing since, fail closed rather than let the
|
|
87
|
+
// session through — the response is lost, and with it the session cookie.
|
|
88
|
+
if (!twoFactorHook)
|
|
89
|
+
throw new Error("[second-factor-everywhere] Better Auth's two-factor plugin hook is missing.");
|
|
90
|
+
const location = redirectLocation(input);
|
|
91
|
+
const result = (await twoFactorHook.handler(input)) ?? {};
|
|
92
|
+
// Trusted device: the plugin let the session stand and rotated the trust cookie. Keep both, change nothing else.
|
|
93
|
+
if (result.response === undefined || !isTwoFactorRedirect(result.response))
|
|
94
|
+
return result;
|
|
95
|
+
// A JSON caller gets exactly what a password sign-in gets.
|
|
96
|
+
if (location === null)
|
|
97
|
+
return result;
|
|
98
|
+
const origin = options.appOrigin ?? new URL(input.context.baseURL).origin;
|
|
99
|
+
const target = codeScreenUrl(origin, codeScreenPath, sameSitePath(location, origin), nextParam);
|
|
100
|
+
const headers = new Headers(result.headers ?? undefined);
|
|
101
|
+
headers.set("location", target);
|
|
102
|
+
return { response: new APIError("FOUND", undefined, { location: target }), headers };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
id: SECOND_FACTOR_EVERYWHERE_ID,
|
|
106
|
+
options: { ...options, enabled, codeScreenPath, nextParam },
|
|
107
|
+
init(ctx) {
|
|
108
|
+
if (!enabled)
|
|
109
|
+
return;
|
|
110
|
+
const plugins = ctx.options.plugins ?? [];
|
|
111
|
+
if (!findTwoFactorSignInHook(plugins)) {
|
|
112
|
+
throw new Error("[second-factor-everywhere] needs Better Auth's twoFactor() plugin, listed in `plugins`, exposing its " +
|
|
113
|
+
"password sign-in after-hook. Either it is missing, or this Better Auth version changed how it enforces the " +
|
|
114
|
+
"second factor — check @connextar/house's supported Better Auth range.");
|
|
115
|
+
}
|
|
116
|
+
const ownIndex = plugins.findIndex((p) => p.id === SECOND_FACTOR_EVERYWHERE_ID);
|
|
117
|
+
const nextCookiesIndex = plugins.findIndex((p) => p.id === "next-cookies");
|
|
118
|
+
if (nextCookiesIndex !== -1 && nextCookiesIndex < ownIndex) {
|
|
119
|
+
ctx.logger.warn("[second-factor-everywhere] is listed after nextCookies(); list it before, so the cookies it sets reach Next.");
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
hooks: {
|
|
123
|
+
after: [{ matcher: applies, handler }],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=second-factor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor.js","sourceRoot":"","sources":["../../src/auth/second-factor.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* This lives in the package rather than in each app because it is a security
|
|
19
19
|
* decision that has to be the same everywhere, and because getting it wrong is
|
|
20
20
|
* silent: two-factor keeps *looking* enabled while doing nothing.
|
|
21
|
+
*
|
|
22
|
+
* **Superseded by `secondFactorEverywhere()`** (`@connextar/house/auth/second-factor`),
|
|
23
|
+
* which instead puts the code screen after a magic link *and* after a social
|
|
24
|
+
* sign-in, so neither needs refusing. Pass the hook's `ctx.context` to
|
|
25
|
+
* {@link shouldBlockForTwoFactor} and the refusal retires itself while that plugin
|
|
26
|
+
* is live — see {@link magicLinkRefusalRetired}.
|
|
21
27
|
*/
|
|
22
28
|
export declare const TWO_FACTOR_BLOCKED_PATHS: readonly ["/sign-in/magic-link"];
|
|
23
29
|
export type TwoFactorBlockedPath = (typeof TWO_FACTOR_BLOCKED_PATHS)[number];
|
|
@@ -37,7 +43,30 @@ export type TwoFactorLookup = (email: string) => Promise<boolean>;
|
|
|
37
43
|
* answering "does this account exist, and does it have two-factor on?" for
|
|
38
44
|
* anyone with an email address and a spare minute.
|
|
39
45
|
*/
|
|
40
|
-
export declare function shouldBlockForTwoFactor(path: string, email: string | undefined | null, lookup: TwoFactorLookup
|
|
46
|
+
export declare function shouldBlockForTwoFactor(path: string, email: string | undefined | null, lookup: TwoFactorLookup,
|
|
47
|
+
/**
|
|
48
|
+
* The hook's `ctx.context`. Passing it retires the refusal once `secondFactorEverywhere()` is installed and
|
|
49
|
+
* challenging magic links — see {@link magicLinkRefusalRetired}.
|
|
50
|
+
*/
|
|
51
|
+
authContext?: AuthContextLike | null): Promise<boolean>;
|
|
52
|
+
/** The slice of Better Auth's `ctx.context` (or `auth.options`) this reads. */
|
|
53
|
+
export interface AuthContextLike {
|
|
54
|
+
options?: {
|
|
55
|
+
plugins?: readonly unknown[] | null;
|
|
56
|
+
} | null;
|
|
57
|
+
plugins?: readonly unknown[] | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* True when refusing magic links for two-factor accounts is no longer needed, because
|
|
61
|
+
* `secondFactorEverywhere()` from `@connextar/house/auth/second-factor` is installed, enabled, and puts the code
|
|
62
|
+
* screen behind `/magic-link/verify`.
|
|
63
|
+
*
|
|
64
|
+
* This is how an app retires its old before-hook refusal *safely*: the refusal steps aside exactly while its
|
|
65
|
+
* replacement is live, and comes back on its own if the replacement is ever switched off (`enabled: false`) or
|
|
66
|
+
* removed — so turning the challenge off can never quietly reopen the magic-link hole. Pass `ctx.context` from a
|
|
67
|
+
* hook, or `auth.options`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function magicLinkRefusalRetired(authContext: AuthContextLike): boolean;
|
|
41
70
|
/**
|
|
42
71
|
* Pull the shared secret out of an `otpauth://` URI, for the "can't scan it?"
|
|
43
72
|
* manual-entry path during enrolment.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"two-factor-policy.d.ts","sourceRoot":"","sources":["../../src/auth/two-factor-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"two-factor-policy.d.ts","sourceRoot":"","sources":["../../src/auth/two-factor-policy.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,wBAAwB,kCAAmC,CAAC;AAEzE,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7E,eAAO,MAAM,0BAA0B,QAEa,CAAC;AAErD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wFAAwF;AACxF,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,MAAM,EAAE,eAAe;AACvB;;;GAGG;AACH,WAAW,CAAC,EAAE,eAAe,GAAG,IAAI,GACnC,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,eAAe,GAAG,OAAO,CAU7E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIhE"}
|