@protobase/server 0.46.7 → 0.46.8
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/better-auth/create-auth.d.ts +19 -0
- package/dist/better-auth/create-auth.js +6 -2
- package/dist/better-auth/leaked-passwords.js +47 -0
- package/dist/better-auth/passkey-plugin.js +1 -1
- package/dist/gotrue/email-code-routes.js +2 -0
- package/dist/gotrue/errors.js +2 -1
- package/dist/gotrue/user-routes.js +2 -0
- package/dist/package.js +1 -1
- package/package.json +4 -4
|
@@ -111,6 +111,12 @@ export type CreateAuthOptions = {
|
|
|
111
111
|
* Supabase Auth's. The first sign-in with one stores the password again as Better Auth hashes it. Default none.
|
|
112
112
|
*/
|
|
113
113
|
passwordHashes?: PasswordHash[];
|
|
114
|
+
/**
|
|
115
|
+
* Refuses a new password that appears in a known data breach, by Have I Been Pwned's password list: at sign-up, from
|
|
116
|
+
* an invitation, and when a password is set, changed, reset or given by an admin. Only the first five characters of
|
|
117
|
+
* the password's SHA-1 hash leave the server. While the list cannot be reached, setting a password fails. Default off.
|
|
118
|
+
*/
|
|
119
|
+
leakedPasswords?: boolean;
|
|
114
120
|
/**
|
|
115
121
|
* The identity provider the operator's staff sign in with to sign in as people of this app, for support. Defaults
|
|
116
122
|
* to the one the platform passes in `PROTOBASE_OPERATOR_ISSUER`, `PROTOBASE_OPERATOR_CLIENT_ID` and
|
|
@@ -4511,6 +4517,17 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4511
4517
|
status: boolean;
|
|
4512
4518
|
}>;
|
|
4513
4519
|
};
|
|
4520
|
+
} | {
|
|
4521
|
+
id: "protobase-leaked-passwords";
|
|
4522
|
+
hooks: {
|
|
4523
|
+
before: {
|
|
4524
|
+
matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
|
|
4525
|
+
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
4526
|
+
}[];
|
|
4527
|
+
};
|
|
4528
|
+
$ERROR_CODES: {
|
|
4529
|
+
PASSWORD_COMPROMISED: import("better-auth").RawError<"PASSWORD_COMPROMISED">;
|
|
4530
|
+
};
|
|
4514
4531
|
} | {
|
|
4515
4532
|
id: "protobase-gotrue-sessions";
|
|
4516
4533
|
endpoints: {
|
|
@@ -8790,6 +8807,8 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
8790
8807
|
/** The policy until an admin saves one. */
|
|
8791
8808
|
signInPolicy: SignInPolicy;
|
|
8792
8809
|
passwordReset: boolean;
|
|
8810
|
+
/** New passwords are checked against Have I Been Pwned's list of leaked ones (`leakedPasswords`). */
|
|
8811
|
+
leakedPasswords: boolean;
|
|
8793
8812
|
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
8794
8813
|
signUp: boolean;
|
|
8795
8814
|
/** The role of accounts made by open sign-up, while it is offered. */
|
|
@@ -13,6 +13,9 @@ import { platformMailer } from "../mail/platform-mailer.js";
|
|
|
13
13
|
import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
|
|
14
14
|
import { defaultInvitationUrl } from "./invitations/invitation-link-hook.js";
|
|
15
15
|
import { lastAdminGuardPlugin } from "./last-admin-guard.js";
|
|
16
|
+
import { setPasswordPath, setPasswordPlugin } from "./set-password.js";
|
|
17
|
+
import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
|
|
18
|
+
import { leakedPasswordsPlugin } from "./leaked-passwords.js";
|
|
16
19
|
import { userDeletion } from "./user-deletion.js";
|
|
17
20
|
import { revokeMcpGrants } from "./mcp/revoke-grants.js";
|
|
18
21
|
import { coreNames, pluginNames } from "./snake-case-names.js";
|
|
@@ -25,7 +28,6 @@ import { resolveOrganizations } from "./organizations/options.js";
|
|
|
25
28
|
import { passkeyHosts, readPasskeyDomain, resolvePasskeyDomain } from "./passkey-domain.js";
|
|
26
29
|
import { passkeyPlugin } from "./passkey-plugin.js";
|
|
27
30
|
import { passwordResetOptions } from "./password-reset.js";
|
|
28
|
-
import { setPasswordPath, setPasswordPlugin } from "./set-password.js";
|
|
29
31
|
import { rolesPlugin } from "./roles/roles-plugin.js";
|
|
30
32
|
import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
|
|
31
33
|
import { appSignInPolicy } from "./app-sign-in-policy.js";
|
|
@@ -33,7 +35,6 @@ import { assertNoEndpointKeyClashes } from "./endpoint-key-clashes.js";
|
|
|
33
35
|
import { emailInDomains, resolveSamlProviders, samlDisabledPaths, samlGuardPlugin, samlPlugin } from "./saml-sign-in.js";
|
|
34
36
|
import { noAccountMailPlugin } from "./no-account-mail.js";
|
|
35
37
|
import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
|
|
36
|
-
import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
|
|
37
38
|
import { confirmedOnCreate, confirmedOnUpdate, emailConfirmedField } from "./email-confirmed.js";
|
|
38
39
|
import { rehashPasswordsPlugin, verifyAcceptedHash } from "./password-hashes.js";
|
|
39
40
|
import { staffSignInPlugin } from "./staff-plugin.js";
|
|
@@ -182,6 +183,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
182
183
|
...saml.length > 0 ? [samlPlugin(saml, options.baseURL), samlGuardPlugin(samlIds)] : [],
|
|
183
184
|
signInPolicyPlugin(policy),
|
|
184
185
|
setPasswordPlugin(),
|
|
186
|
+
...options.leakedPasswords ? [leakedPasswordsPlugin()] : [],
|
|
185
187
|
goTrueSessionsPlugin({
|
|
186
188
|
issuer: goTrueIssuer(options.baseURL),
|
|
187
189
|
jwt: jwtOptions,
|
|
@@ -384,6 +386,8 @@ const createAuth = (options) => {
|
|
|
384
386
|
/** The policy until an admin saves one. */
|
|
385
387
|
signInPolicy,
|
|
386
388
|
passwordReset: Boolean(mailer),
|
|
389
|
+
/** New passwords are checked against Have I Been Pwned's list of leaked ones (`leakedPasswords`). */
|
|
390
|
+
leakedPasswords: Boolean(options.leakedPasswords),
|
|
387
391
|
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
388
392
|
signUp: Boolean(signUpRole && mailer),
|
|
389
393
|
/** The role of accounts made by open sign-up, while it is offered. */
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { passwordLength } from "./new-account-password.js";
|
|
2
|
+
import { setPasswordPath } from "./set-password.js";
|
|
3
|
+
import { signUpPaths } from "./sign-up/sign-up-plugin.js";
|
|
4
|
+
import { APIError, createAuthMiddleware } from "better-auth/api";
|
|
5
|
+
import { haveIBeenPwned, isPasswordCompromised } from "better-auth/plugins/haveibeenpwned";
|
|
6
|
+
//#region src/better-auth/leaked-passwords.ts
|
|
7
|
+
const { PASSWORD_COMPROMISED } = haveIBeenPwned().$ERROR_CODES;
|
|
8
|
+
const leakedPasswordMessage = "This password appears in a known data breach. Choose a different one.";
|
|
9
|
+
const passwordFields = {
|
|
10
|
+
"/sign-up/email": "password",
|
|
11
|
+
"/change-password": "newPassword",
|
|
12
|
+
"/reset-password": "newPassword",
|
|
13
|
+
"/admin/create-user": "password",
|
|
14
|
+
"/admin/set-user-password": "newPassword",
|
|
15
|
+
[setPasswordPath]: "newPassword",
|
|
16
|
+
[signUpPaths.create]: "password",
|
|
17
|
+
"/invitation/sign-up": "password"
|
|
18
|
+
};
|
|
19
|
+
/** Refuses a password from the Have I Been Pwned corpus with `400 PASSWORD_COMPROMISED`, as Better Auth's plugin does. */
|
|
20
|
+
const rejectLeakedPassword = async (password) => {
|
|
21
|
+
if (!await isPasswordCompromised(password)) return;
|
|
22
|
+
throw APIError.from("BAD_REQUEST", {
|
|
23
|
+
code: PASSWORD_COMPROMISED.code,
|
|
24
|
+
message: leakedPasswordMessage
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Better Auth's haveIBeenPwned check, on every endpoint of `passwordFields`. It runs before the endpoint, where the
|
|
29
|
+
* plugin itself checks only once the password is hashed: admin account creation, invitation sign-up and sign-up have
|
|
30
|
+
* made the account or used the ticket by then. Only the first five characters of the password's SHA-1 hash leave the
|
|
31
|
+
* server; a password outside the allowed lengths is left to the endpoint, which refuses it without a lookup. When the
|
|
32
|
+
* list cannot be reached, the request fails with a 500.
|
|
33
|
+
*/
|
|
34
|
+
const leakedPasswordsPlugin = () => ({
|
|
35
|
+
id: "protobase-leaked-passwords",
|
|
36
|
+
hooks: { before: [{
|
|
37
|
+
matcher: (ctx) => ctx.path !== void 0 && Object.hasOwn(passwordFields, ctx.path),
|
|
38
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
39
|
+
const password = ctx.body?.[passwordFields[ctx.path]];
|
|
40
|
+
if (typeof password !== "string" || password.length < passwordLength.min || password.length > passwordLength.max) return;
|
|
41
|
+
await rejectLeakedPassword(password);
|
|
42
|
+
})
|
|
43
|
+
}] },
|
|
44
|
+
$ERROR_CODES: { PASSWORD_COMPROMISED }
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
export { leakedPasswordsPlugin, rejectLeakedPassword };
|
|
@@ -2,9 +2,9 @@ import { pluginNames } from "./snake-case-names.js";
|
|
|
2
2
|
import * as z$1 from "zod";
|
|
3
3
|
import { APIError, createAuthEndpoint, getSessionFromCtx } from "better-auth/api";
|
|
4
4
|
import { generateRandomString } from "better-auth/crypto";
|
|
5
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
5
6
|
import { PASSKEY_ERROR_CODES, passkey } from "@better-auth/passkey";
|
|
6
7
|
import { generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
|
|
7
|
-
import { setSessionCookie } from "better-auth/cookies";
|
|
8
8
|
//#region src/better-auth/passkey-plugin.ts
|
|
9
9
|
const challengeCookie = "better-auth-passkey";
|
|
10
10
|
const challengeSeconds = 300;
|
|
@@ -3,6 +3,7 @@ import { readSignInPolicy } from "../better-auth/policy-store.js";
|
|
|
3
3
|
import { checkNewAccountPassword } from "../better-auth/new-account-password.js";
|
|
4
4
|
import { createSignedUpAccount } from "../better-auth/sign-up/sign-up-account.js";
|
|
5
5
|
import { consumeSignUpTicket } from "../better-auth/sign-up/sign-up-ticket.js";
|
|
6
|
+
import { rejectLeakedPassword } from "../better-auth/leaked-passwords.js";
|
|
6
7
|
import { GoTrueError } from "./errors.js";
|
|
7
8
|
import { callBetterAuth, signedInToken } from "./forward.js";
|
|
8
9
|
import { hasPendingSignUp, savePendingSignUp, takePendingSignUp } from "./pending-sign-up.js";
|
|
@@ -79,6 +80,7 @@ const emailCodeRoutes = ({ auth, context, sessionOf }) => {
|
|
|
79
80
|
if (!auth.signUp) throw signUpDisabled();
|
|
80
81
|
checkMetadata(data);
|
|
81
82
|
checkNewAccountPassword(await signUpPolicy(), password);
|
|
83
|
+
if (auth.leakedPasswords) await rejectLeakedPassword(password);
|
|
82
84
|
await sendSignUpCode(c.req.raw, email);
|
|
83
85
|
await savePendingSignUp(await context(), email, {
|
|
84
86
|
passwordHash: await (await context()).password.hash(password),
|
package/dist/gotrue/errors.js
CHANGED
|
@@ -35,7 +35,8 @@ const known = {
|
|
|
35
35
|
BANNED_USER: [403, "user_banned"],
|
|
36
36
|
NO_ADMIN_YET: [422, "signup_disabled"],
|
|
37
37
|
PASSWORD_LENGTH: [422, "weak_password"],
|
|
38
|
-
PASSWORD_REQUIRED: [422, "weak_password"]
|
|
38
|
+
PASSWORD_REQUIRED: [422, "weak_password"],
|
|
39
|
+
PASSWORD_COMPROMISED: [422, "weak_password"]
|
|
39
40
|
};
|
|
40
41
|
/** A refusal of Better Auth, by its status and body, as GoTrue's: a known code becomes GoTrue's, any other its own in lower case. */
|
|
41
42
|
const fromBetterAuth = (status, body) => {
|
|
@@ -2,6 +2,7 @@ import { effectiveSignInPolicy } from "../better-auth/sign-in-policy.js";
|
|
|
2
2
|
import { readSignInPolicy } from "../better-auth/policy-store.js";
|
|
3
3
|
import { goTrueUser, userMetadata } from "./user-json.js";
|
|
4
4
|
import { checkNewAccountPassword } from "../better-auth/new-account-password.js";
|
|
5
|
+
import { rejectLeakedPassword } from "../better-auth/leaked-passwords.js";
|
|
5
6
|
import { revokeMcpGrants } from "../better-auth/mcp/revoke-grants.js";
|
|
6
7
|
import { goTrueTokens } from "./access-tokens.js";
|
|
7
8
|
import { GoTrueError } from "./errors.js";
|
|
@@ -75,6 +76,7 @@ const userRoutes = ({ auth, context }) => {
|
|
|
75
76
|
if (data !== void 0 && !auth.userMetadata) throw new GoTrueError(400, "validation_failed", "This app keeps no user metadata: leave data out");
|
|
76
77
|
if (password !== void 0) {
|
|
77
78
|
checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(ctx.adapter, auth.signInPolicy)).policy, { mail: auth.mail }), password);
|
|
79
|
+
if (auth.leakedPasswords) await rejectLeakedPassword(password);
|
|
78
80
|
const hash = await ctx.password.hash(password);
|
|
79
81
|
if (await ctx.internalAdapter.findCredentialAccount(user.id)) await ctx.internalAdapter.updatePassword(user.id, hash);
|
|
80
82
|
else await ctx.internalAdapter.linkAccount({
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.8",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@better-auth/passkey": "^1.7.12",
|
|
37
37
|
"@better-auth/sso": "^1.7.12",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
39
|
-
"@protobase/layout": "0.46.
|
|
40
|
-
"@protobase/query": "0.46.
|
|
41
|
-
"@protobase/schema": "0.46.
|
|
39
|
+
"@protobase/layout": "0.46.8",
|
|
40
|
+
"@protobase/query": "0.46.8",
|
|
41
|
+
"@protobase/schema": "0.46.8",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|