@madauth/server 0.3.0-beta.2 → 0.5.0-beta.1
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 +3 -2
- package/dist/bin.js +202 -50
- package/dist/chunk-7DHKDRGY.js +1706 -0
- package/dist/{chunk-DCDTO5LG.js → chunk-A7UYZRBL.js} +1 -0
- package/dist/chunk-NP75Y3IV.js +107 -0
- package/dist/chunk-NYIEFI4B.js +1040 -0
- package/dist/{chunk-56MOVFO4.js → chunk-O2URPWA6.js} +37 -4
- package/dist/{chunk-3QRKWLR7.js → chunk-SISM7YUZ.js} +15 -10
- package/dist/{config-Dg2fZuWR.d.ts → config-CcGlHVAN.d.ts} +19 -8
- package/dist/dynamodb.js +1 -1
- package/dist/entry/azure.d.ts +3 -3
- package/dist/entry/azure.js +4 -4
- package/dist/entry/lambda.d.ts +3 -3
- package/dist/entry/lambda.js +4 -4
- package/dist/entry/node.d.ts +3 -3
- package/dist/entry/node.js +4 -4
- package/dist/index.d.ts +37 -15
- package/dist/index.js +12 -6
- package/dist/{schema-3yhCc2jH.d.ts → schema-BTgDj1Xn.d.ts} +79 -3
- package/dist/sqlite.js +2 -2
- package/dist/standalone/azure.mjs +5511 -922
- package/dist/standalone/lambda.mjs +5461 -872
- package/dist/standalone/main.mjs +5487 -898
- package/dist/store/dynamodb.d.ts +1 -1
- package/dist/store/sqlite.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +21 -0
- package/dist/{user-BMWEWREH.d.ts → user-yoGjrw4k.d.ts} +5 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +1 -1
- package/dist/{webhook-j5u9Kv9l.d.ts → webhook-DlmfG_kS.d.ts} +5 -2
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/package.json +4 -2
- package/dist/chunk-CD7Y4T66.js +0 -62
- package/dist/chunk-DVVBIFIV.js +0 -718
- package/dist/chunk-TI35AZ2U.js +0 -660
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @madauth/server
|
|
2
2
|
|
|
3
|
-
The server of [madAuth](https://github.com/inouiw/madAuth), a self-hostable alternative to Cognito / Auth0: Google
|
|
3
|
+
The server of [madAuth](https://github.com/inouiw/madAuth), a self-hostable alternative to Cognito / Auth0: Google, GitHub, e-mail & password, e-mail link and authenticator-app (TOTP) sign-in with madAuth sessions. It runs with Node, in Docker, on AWS Lambda, on Azure Functions or inside your own Node server.
|
|
4
4
|
|
|
5
5
|
The browser side is [`@madauth/web`](https://www.npmjs.com/package/@madauth/web).
|
|
6
6
|
|
|
@@ -53,7 +53,8 @@ The package also contains self-contained bundles for AWS Lambda (`dist/standalon
|
|
|
53
53
|
## Documentation
|
|
54
54
|
|
|
55
55
|
- [Getting started](https://github.com/inouiw/madAuth/blob/main/docs/getting-started.md): set up madAuth on your machine, step by step.
|
|
56
|
-
- [Running the madAuth server](https://github.com/inouiw/madAuth/blob/main/docs/server.md): the configuration, Google Cloud Console setup, webhooks, hosting and custom store adapters.
|
|
56
|
+
- [Running the madAuth server](https://github.com/inouiw/madAuth/blob/main/docs/server.md): the configuration, Google Cloud Console setup, the sign-in methods and their policies, the authenticator app, webhooks, hosting and custom store adapters.
|
|
57
|
+
- [Password security](https://github.com/inouiw/madAuth/blob/main/docs/password-security.md) and [Authenticator app security](https://github.com/inouiw/madAuth/blob/main/docs/totp-security.md): what the server does to protect accounts.
|
|
57
58
|
|
|
58
59
|
## License
|
|
59
60
|
|
package/dist/bin.js
CHANGED
|
@@ -2,35 +2,45 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createTablesSql,
|
|
4
4
|
upgradeTablesSql
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-O2URPWA6.js";
|
|
6
6
|
import {
|
|
7
7
|
madauthSchema
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NP75Y3IV.js";
|
|
9
9
|
import {
|
|
10
|
+
DEFAULT_PASSWORD_RULES,
|
|
11
|
+
MAX_MIN_LENGTH,
|
|
12
|
+
METHODS,
|
|
13
|
+
SECOND_FACTOR_POLICIES,
|
|
10
14
|
SIGN_IN_METHODS,
|
|
11
15
|
Settings,
|
|
12
16
|
Users,
|
|
13
17
|
anyOn,
|
|
18
|
+
canSignUp,
|
|
14
19
|
checkPasswordPolicy,
|
|
15
20
|
claimsFromJson,
|
|
21
|
+
hasSecondFactor,
|
|
16
22
|
hashPassword,
|
|
23
|
+
initialMethods,
|
|
24
|
+
isConfigured,
|
|
17
25
|
isOn,
|
|
26
|
+
isPasswordScore,
|
|
18
27
|
isValidEmail,
|
|
19
28
|
loadConfig,
|
|
20
29
|
loadStore,
|
|
21
|
-
|
|
30
|
+
methodConfig,
|
|
22
31
|
normalizeEmail,
|
|
23
32
|
parseClaims,
|
|
24
33
|
parseRoles,
|
|
25
|
-
passwordAccountKey
|
|
26
|
-
|
|
34
|
+
passwordAccountKey,
|
|
35
|
+
policyOf
|
|
36
|
+
} from "./chunk-NYIEFI4B.js";
|
|
27
37
|
import {
|
|
28
38
|
generateSigningKey
|
|
29
39
|
} from "./chunk-JDQLSL36.js";
|
|
30
40
|
import {
|
|
31
41
|
DEV_WEBHOOK_RECEIVER_URL,
|
|
32
42
|
generateWebhookSecret
|
|
33
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-SISM7YUZ.js";
|
|
34
44
|
|
|
35
45
|
// src/cli.ts
|
|
36
46
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -42,25 +52,44 @@ var usage = `Usage: npx @madauth/server <command> [--env-file <path>]
|
|
|
42
52
|
Commands:
|
|
43
53
|
init Ask a few questions, write the configuration to .env and print the next steps.
|
|
44
54
|
An answer given as an option is not asked for: --app-url <url>,
|
|
45
|
-
--google-client-id <id>, --google-client-secret <secret>,
|
|
46
|
-
--
|
|
47
|
-
|
|
55
|
+
--google-client-id <id>, --google-client-secret <secret>,
|
|
56
|
+
--github-client-id <id>, --github-client-secret <secret>, --password or
|
|
57
|
+
--no-password, --totp or --no-totp (sign-up with the authenticator app),
|
|
58
|
+
--email-link or --no-email-link (passwordless sign-in by e-mail),
|
|
59
|
+
--sms or --no-sms (SMS codes as a second factor),
|
|
60
|
+
--database <url>, --webhook-url <url>. --yes takes the default for the
|
|
61
|
+
rest. --out <path> writes another file, --force overwrites.
|
|
48
62
|
start Start the server on PORT (default ${PORT})
|
|
49
63
|
generate-key Print a new private ES256 key to use as MADAUTH_SIGNING_KEY
|
|
50
64
|
generate-webhook-secret Print a new secret to use as WEBHOOK_SECRET (madAuth and your receiver)
|
|
51
|
-
create-user <email> Create a user with a password (asks for it), already verified.
|
|
52
|
-
Uses DATABASE_URL
|
|
65
|
+
create-user <email> Create a user with a password (asks for it), already verified. The password
|
|
66
|
+
must follow the password rules. Uses DATABASE_URL from the environment.
|
|
53
67
|
set-roles <email> [role...] Set the roles of a user (the claim "roles"), e.g. to make the first admin:
|
|
54
68
|
set-roles you@example.com admin. Without roles, removes them all.
|
|
55
69
|
The user must exist: signed in once, or created with create-user.
|
|
56
70
|
get-roles <email> Print the roles of a user.
|
|
57
71
|
set-claims <email> <json> Replace the claims of a user, e.g. '{"roles":["admin"],"plan":"pro"}'.
|
|
58
72
|
get-claims <email> Print the claims of a user as JSON.
|
|
59
|
-
set-methods [method...]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
set-methods [method[=policy]...]
|
|
74
|
+
Switch sign-in methods on and off while the server runs: the listed ones
|
|
75
|
+
are on, the others off. Methods: google, github, password, totp (the
|
|
76
|
+
authenticator app on its own), email-link (passwordless sign-in by e-mail),
|
|
77
|
+
and sms (SMS codes as a second factor, next to the app). google, github,
|
|
78
|
+
password and email-link take a policy for the second factor: none (default),
|
|
79
|
+
optional or required, e.g. set-methods google password=required totp email-link=optional sms
|
|
80
|
+
get-methods Print which sign-in methods are switched on, with their policies.
|
|
81
|
+
set-password-rules [min-length=<n>] [min-score=<0-4>]
|
|
82
|
+
Set the rules for new passwords: the minimum length (default 8, at most 128)
|
|
83
|
+
and how hard to guess they must be, 0 (anything) to 4 (default 2: no common
|
|
84
|
+
passwords or simple patterns). What is left out takes its default, e.g.
|
|
85
|
+
set-password-rules min-length=10 min-score=3. Uses DATABASE_URL.
|
|
86
|
+
get-password-rules Print the rules for new passwords.
|
|
87
|
+
remove-totp <email> Remove a user's authenticator app and recovery codes: the last resort when
|
|
88
|
+
both are lost. With a required policy, the user sets it up again next time.
|
|
89
|
+
remove-sms <email> Remove a user's phone number for SMS codes, when they lost the phone.
|
|
90
|
+
The claims commands, remove-totp and remove-sms use DATABASE_URL from the
|
|
91
|
+
environment; the methods commands need the server's whole configuration
|
|
92
|
+
(e.g. --env-file .env).
|
|
64
93
|
schema [--dialect <name>] [--from <version>]
|
|
65
94
|
Print the SQL that creates madAuth's tables for a custom store adapter.
|
|
66
95
|
Dialects: postgres (default), mysql, sqlite. With --from, print only the
|
|
@@ -156,7 +185,12 @@ function parseCliArgs(args) {
|
|
|
156
185
|
"app-url": { type: "string" },
|
|
157
186
|
"google-client-id": { type: "string" },
|
|
158
187
|
"google-client-secret": { type: "string" },
|
|
188
|
+
"github-client-id": { type: "string" },
|
|
189
|
+
"github-client-secret": { type: "string" },
|
|
159
190
|
password: { type: "boolean" },
|
|
191
|
+
totp: { type: "boolean" },
|
|
192
|
+
"email-link": { type: "boolean" },
|
|
193
|
+
sms: { type: "boolean" },
|
|
160
194
|
database: { type: "string" },
|
|
161
195
|
"webhook-url": { type: "string" },
|
|
162
196
|
yes: { type: "boolean" },
|
|
@@ -179,7 +213,17 @@ ${usage}`, exitCode: 1 };
|
|
|
179
213
|
}
|
|
180
214
|
const { values: options, positionals } = parsed;
|
|
181
215
|
const [command, ...rest] = positionals;
|
|
182
|
-
const argumentLimits = {
|
|
216
|
+
const argumentLimits = {
|
|
217
|
+
"set-roles": Infinity,
|
|
218
|
+
"set-methods": Infinity,
|
|
219
|
+
"set-password-rules": Infinity,
|
|
220
|
+
"set-claims": 2,
|
|
221
|
+
"create-user": 1,
|
|
222
|
+
"get-roles": 1,
|
|
223
|
+
"get-claims": 1,
|
|
224
|
+
"remove-totp": 1,
|
|
225
|
+
"remove-sms": 1
|
|
226
|
+
};
|
|
183
227
|
const maxArguments = command ? argumentLimits[command] ?? 0 : 0;
|
|
184
228
|
if (rest.length > maxArguments) {
|
|
185
229
|
return { output: `Unexpected argument: ${rest.at(-1)}
|
|
@@ -223,6 +267,15 @@ ${usage}`, exitCode: 1 };
|
|
|
223
267
|
if (command === "set-methods" || command === "get-methods") {
|
|
224
268
|
return await methods(command, rest, env);
|
|
225
269
|
}
|
|
270
|
+
if (command === "set-password-rules" || command === "get-password-rules") {
|
|
271
|
+
return await passwordRules(command, rest, env);
|
|
272
|
+
}
|
|
273
|
+
if (command === "remove-totp") {
|
|
274
|
+
return await removeTotp(rest[0], env);
|
|
275
|
+
}
|
|
276
|
+
if (command === "remove-sms") {
|
|
277
|
+
return await removeSms(rest[0], env);
|
|
278
|
+
}
|
|
226
279
|
} catch (e) {
|
|
227
280
|
return { output: e instanceof Error ? e.message : String(e), exitCode: 1 };
|
|
228
281
|
}
|
|
@@ -255,18 +308,37 @@ async function init(options, io) {
|
|
|
255
308
|
return fail("--google-client-secret needs a Google client ID.");
|
|
256
309
|
}
|
|
257
310
|
const clientSecret = clientId ? (options["google-client-secret"] ?? await askClientSecret()).trim() : "";
|
|
311
|
+
const githubClientId = await answer(options["github-client-id"], "GitHub client ID (empty: no GitHub sign-in)");
|
|
312
|
+
if (!githubClientId && options["github-client-secret"] !== void 0) {
|
|
313
|
+
return fail("--github-client-secret needs a GitHub client ID.");
|
|
314
|
+
}
|
|
315
|
+
const githubClientSecret = githubClientId ? (options["github-client-secret"] ?? (options.yes ? "" : await io.askSecret("GitHub client secret: "))).trim() : "";
|
|
316
|
+
if (githubClientId && !githubClientSecret) {
|
|
317
|
+
return fail("GitHub sign-in needs the client secret of the OAuth App too (--github-client-secret).");
|
|
318
|
+
}
|
|
258
319
|
const password = options.password ?? /^y/i.test(await answer(void 0, "E-mail & password sign-in? (yes/no)", "yes"));
|
|
259
|
-
|
|
260
|
-
|
|
320
|
+
const totp = options.totp ?? /^y/i.test(await answer(void 0, "Sign-up with the authenticator app alone, without a password? (yes/no)", "no"));
|
|
321
|
+
const emailLink = options["email-link"] ?? /^y/i.test(await answer(void 0, "Passwordless sign-in with an e-mail link? (yes/no)", "no"));
|
|
322
|
+
const sms = options.sms ?? /^y/i.test(await answer(void 0, "SMS codes as a second factor, sent by your webhook receiver? (yes/no)", "no"));
|
|
323
|
+
const emails = password || totp || emailLink;
|
|
324
|
+
const webhook = emails || sms;
|
|
325
|
+
if (!webhook && options["webhook-url"] !== void 0) {
|
|
326
|
+
return fail("--webhook-url is for the e-mails of e-mail & password sign-in, of the e-mail link and of sign-up with the authenticator app, and for SMS codes, which are turned off.");
|
|
261
327
|
}
|
|
262
|
-
if (!clientId && !
|
|
263
|
-
return fail("No sign-in method is chosen. Give a Google client ID, turn on e-mail & password sign-in, or
|
|
328
|
+
if (!clientId && !githubClientId && !emails) {
|
|
329
|
+
return fail("No sign-in method is chosen. Give a Google or GitHub client ID, turn on e-mail & password sign-in, the e-mail link or sign-up with the authenticator app.");
|
|
264
330
|
}
|
|
265
331
|
const database = await answer(options.database, "Database for the users", "sqlite:./madauth.db");
|
|
266
332
|
if (!/^(sqlite|dynamodb):./.test(database)) {
|
|
267
333
|
return fail(`The database must be sqlite:<path> or dynamodb:<table> but is "${database}".`);
|
|
268
334
|
}
|
|
269
|
-
const webhookUrl =
|
|
335
|
+
const webhookUrl = webhook ? await answer(options["webhook-url"], `Webhook URL, where madAuth hands over its ${emails ? sms ? "e-mails and SMS" : "e-mails" : "SMS"}`, "http://localhost:8790/webhook") : "";
|
|
336
|
+
const events = [
|
|
337
|
+
...password ? ["email.send_verification", "email.send_password_reset", "email.send_already_registered", "email.send_password_reset_no_password"] : [],
|
|
338
|
+
...totp ? ["email.send_verification", "email.send_already_registered"] : [],
|
|
339
|
+
...emailLink ? ["email.send_signin_link", "email.send_already_registered"] : [],
|
|
340
|
+
...sms ? ["sms.send_code", "user.second_factor_enabled", "user.second_factor_disabled"] : []
|
|
341
|
+
].filter((type, i, all) => all.indexOf(type) === i).join(",");
|
|
270
342
|
const variable = (name, value, comment) => `# ${comment}
|
|
271
343
|
${name}=${value}
|
|
272
344
|
`;
|
|
@@ -288,14 +360,12 @@ ${name}=${value}
|
|
|
288
360
|
variable("ALLOWED_ORIGINS", origin, "Origins of your apps, comma-separated. Only these may call the server."),
|
|
289
361
|
clientId && variable("GOOGLE_CLIENT_ID", clientId, "Google sign-in: your OAuth client ID, of the type Web application."),
|
|
290
362
|
clientSecret && variable("GOOGLE_CLIENT_SECRET", clientSecret, "Client secret of the same client. It enables the redirect flow."),
|
|
363
|
+
githubClientId && variable("GITHUB_CLIENT_ID", githubClientId, "GitHub sign-in: the client ID of your OAuth App."),
|
|
364
|
+
githubClientSecret && variable("GITHUB_CLIENT_SECRET", githubClientSecret, "Client secret of the same OAuth App."),
|
|
291
365
|
variable("DATABASE_URL", database, "Where the users are stored, whichever way they sign in."),
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"WEBHOOK_EVENTS",
|
|
296
|
-
"email.verify,email.reset,email.already_registered,email.no_password",
|
|
297
|
-
"The types your webhook receiver handles; only these are sent. Add e.g. signup.before or user.created."
|
|
298
|
-
),
|
|
366
|
+
webhook && variable("WEBHOOK_URL", webhookUrl, `Your webhook receiver. madAuth hands its ${sms ? "e-mails and SMS" : "e-mails"} to it.`),
|
|
367
|
+
webhook && variable("WEBHOOK_SECRET", generateWebhookSecret(), "Signs every webhook call. Your receiver needs the same one."),
|
|
368
|
+
webhook && variable("WEBHOOK_EVENTS", events, "The types your webhook receiver handles; only these are sent. Add e.g. user.signing_up or user.signed_up."),
|
|
299
369
|
variable("PORT", String(PORT), "Port the server listens on.")
|
|
300
370
|
].filter(Boolean).join("");
|
|
301
371
|
await loadConfig({ ...parseEnv(content), DATABASE_URL: void 0 }, { store: {} });
|
|
@@ -306,21 +376,27 @@ ${name}=${value}
|
|
|
306
376
|
|
|
307
377
|
Next steps:
|
|
308
378
|
|
|
309
|
-
${nextSteps(file, origin, google, password)}`, exitCode: 0 };
|
|
379
|
+
${nextSteps(file, origin, google, !!githubClientId, password, totp, emailLink, sms)}`, exitCode: 0 };
|
|
310
380
|
}
|
|
311
|
-
function nextSteps(file, origin, google, password) {
|
|
381
|
+
function nextSteps(file, origin, google, github, password, totp = false, emailLink = false, sms = false) {
|
|
312
382
|
const server = `http://localhost:${PORT}`;
|
|
313
383
|
const envFile = /[^\w./-]/.test(file) ? `'${file.replaceAll("'", `'\\''`)}'` : file;
|
|
314
|
-
const providers = [google, password && "Password"].filter(Boolean);
|
|
384
|
+
const providers = [google, github && "GitHub", password && "Password", totp && "Totp", emailLink && "EmailLink"].filter(Boolean);
|
|
385
|
+
const methodList = [google && "google", github && "github", password && "password", totp && "totp", emailLink && "email-link"].filter(Boolean).join(" ");
|
|
315
386
|
const googleOrigins = /* @__PURE__ */ new Set([origin, ...new URL(origin).hostname === "localhost" ? ["http://localhost"] : []]);
|
|
316
387
|
const steps = [
|
|
317
388
|
["Start the madAuth server:", "", ` npx @madauth/server start --env-file ${envFile}`],
|
|
318
|
-
password && [
|
|
319
|
-
|
|
389
|
+
(password || totp || emailLink || sms) && [
|
|
390
|
+
`madAuth hands its ${sms ? "e-mails and SMS" : "e-mails"} to your webhook receiver. For development, start the development receiver,`,
|
|
320
391
|
`which prints them, with the same WEBHOOK_URL and WEBHOOK_SECRET as in ${file}:`,
|
|
321
392
|
"",
|
|
322
393
|
` ${DEV_WEBHOOK_RECEIVER_URL}`
|
|
323
394
|
],
|
|
395
|
+
totp && (google || github || password) && [
|
|
396
|
+
"Signing in with the authenticator app alone is off until an admin lists it. Once the server runs:",
|
|
397
|
+
"",
|
|
398
|
+
` npx @madauth/server set-methods ${methodList} --env-file ${envFile}`
|
|
399
|
+
],
|
|
324
400
|
[
|
|
325
401
|
"Proxy /auth and /.well-known from your app to the server, so that its cookies are first-party.",
|
|
326
402
|
"With Vite, in vite.config.ts:",
|
|
@@ -347,6 +423,11 @@ function nextSteps(file, origin, google, password) {
|
|
|
347
423
|
` Authorized JavaScript origins: ${[...googleOrigins].join(" and ")}`,
|
|
348
424
|
...google === "GoogleRedirect" ? [` Authorized redirect URIs: ${origin}/auth/google/callback`] : []
|
|
349
425
|
],
|
|
426
|
+
github && [
|
|
427
|
+
"In your GitHub OAuth App (https://github.com/settings/developers), set:",
|
|
428
|
+
"",
|
|
429
|
+
` Authorization callback URL: ${origin}/auth/github/callback`
|
|
430
|
+
],
|
|
350
431
|
[
|
|
351
432
|
`Never commit ${file}: it holds the signing key and your secrets. Production needs an https app URL`,
|
|
352
433
|
"and a signing key of its own: run init again for it."
|
|
@@ -401,36 +482,107 @@ ${usage}`, exitCode: 1 };
|
|
|
401
482
|
const roles = Array.isArray(current.roles) ? current.roles : [];
|
|
402
483
|
return { output: roles.length ? `${address}: ${roles.join(" ")}` : `${address} has no roles.`, exitCode: 0 };
|
|
403
484
|
}
|
|
404
|
-
async function methods(command,
|
|
485
|
+
async function methods(command, args, env) {
|
|
405
486
|
const config = await loadConfig(env);
|
|
406
|
-
const
|
|
407
|
-
const
|
|
487
|
+
const forMethods = methodConfig(config);
|
|
488
|
+
const configured = (method) => isConfigured(forMethods, method);
|
|
489
|
+
const signsUp = (method) => canSignUp(forMethods, method);
|
|
490
|
+
const settings = new Settings(config.store, initialMethods(forMethods));
|
|
408
491
|
if (command === "set-methods") {
|
|
409
|
-
const unknown = names.filter((name) => !SIGN_IN_METHODS.includes(name));
|
|
410
|
-
if (unknown.length) {
|
|
411
|
-
return { output: `Unknown sign-in method ${unknown.join(", ")}. Methods: ${SIGN_IN_METHODS.join(", ")}.`, exitCode: 1 };
|
|
412
|
-
}
|
|
413
492
|
const next = {};
|
|
414
|
-
for (const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
493
|
+
for (const arg of args) {
|
|
494
|
+
const [name, policy, ...more] = arg.split("=");
|
|
495
|
+
if (!METHODS.includes(name) || more.length) {
|
|
496
|
+
return { output: `Unknown sign-in method "${arg}". Methods: ${METHODS.join(", ")}, e.g. set-methods google password=required totp sms.`, exitCode: 1 };
|
|
497
|
+
}
|
|
498
|
+
const method = name;
|
|
499
|
+
if (policy !== void 0 && (!hasSecondFactor(method) || !SECOND_FACTOR_POLICIES.includes(policy))) {
|
|
500
|
+
return {
|
|
501
|
+
output: `"${arg}": google, github, password and email-link take a policy for the second factor: ${SECOND_FACTOR_POLICIES.join(", ")}.`,
|
|
502
|
+
exitCode: 1
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
next[method] = hasSecondFactor(method) ? { secondFactor: policy ?? "none" } : {};
|
|
506
|
+
}
|
|
507
|
+
if (!anyOn(signsUp, next)) {
|
|
508
|
+
const usable = SIGN_IN_METHODS.filter(signsUp);
|
|
509
|
+
return {
|
|
510
|
+
output: `That would leave no sign-in method on that creates users, so nobody could sign up. On this server these do: ${usable.join(", ")}.`,
|
|
511
|
+
exitCode: 1
|
|
512
|
+
};
|
|
418
513
|
}
|
|
419
514
|
await settings.changeMethods(() => next, null);
|
|
420
515
|
}
|
|
421
516
|
const current = await settings.methods();
|
|
422
|
-
const
|
|
423
|
-
|
|
517
|
+
const describe = (method) => {
|
|
518
|
+
const policy = hasSecondFactor(method) ? policyOf(method, current) : "none";
|
|
519
|
+
return policy !== "none" ? `${method} (second factor ${policy})` : method;
|
|
520
|
+
};
|
|
521
|
+
const on = METHODS.filter((method) => isOn(method, configured, current)).map(describe);
|
|
522
|
+
const off = METHODS.filter((method) => configured(method) && !isOn(method, configured, current));
|
|
424
523
|
return { output: `Switched on: ${on.join(", ")}.${off.length ? ` Switched off: ${off.join(", ")}.` : ""}`, exitCode: 0 };
|
|
425
524
|
}
|
|
525
|
+
function describeRules(rules) {
|
|
526
|
+
return `New passwords: at least ${rules.minLength} characters, and a strength score of at least ${rules.minScore} (0 to 4).`;
|
|
527
|
+
}
|
|
528
|
+
async function passwordRules(command, args, env) {
|
|
529
|
+
const settings = new Settings(await loadStore(env));
|
|
530
|
+
if (command === "set-password-rules") {
|
|
531
|
+
const fail = (output2) => ({ output: output2, exitCode: 1 });
|
|
532
|
+
const rules = { ...DEFAULT_PASSWORD_RULES };
|
|
533
|
+
for (const arg of args) {
|
|
534
|
+
const [name, value, ...more] = arg.split("=");
|
|
535
|
+
const number = value !== void 0 && /^\d+$/.test(value) && !more.length ? Number(value) : NaN;
|
|
536
|
+
if (name === "min-length") {
|
|
537
|
+
if (!(number >= 1 && number <= MAX_MIN_LENGTH)) return fail(`min-length must be a whole number from 1 to ${MAX_MIN_LENGTH} but is "${value ?? ""}".`);
|
|
538
|
+
rules.minLength = number;
|
|
539
|
+
} else if (name === "min-score") {
|
|
540
|
+
if (!isPasswordScore(number)) return fail(`min-score must be 0, 1, 2, 3 or 4 but is "${value ?? ""}".`);
|
|
541
|
+
rules.minScore = number;
|
|
542
|
+
} else {
|
|
543
|
+
return fail(`Unknown password rule "${arg}". Rules: min-length=<n>, min-score=<0-4>, e.g. set-password-rules min-length=10 min-score=3.`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
await settings.setPasswordRules(rules, null);
|
|
547
|
+
}
|
|
548
|
+
return { output: describeRules(await settings.passwordRules()), exitCode: 0 };
|
|
549
|
+
}
|
|
550
|
+
async function removeTotp(email, env) {
|
|
551
|
+
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
552
|
+
|
|
553
|
+
${usage}`, exitCode: 1 };
|
|
554
|
+
const users = new Users(await loadStore(env));
|
|
555
|
+
const address = normalizeEmail(email);
|
|
556
|
+
const user = await users.findByEmail(address);
|
|
557
|
+
if (!user) return { output: `No user has the e-mail address ${address}.`, exitCode: 1 };
|
|
558
|
+
const removed = await users.removeTotp(user.id);
|
|
559
|
+
return {
|
|
560
|
+
output: removed ? `Removed the authenticator app of ${address}.` : `${address} has no authenticator app.`,
|
|
561
|
+
exitCode: 0
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
async function removeSms(email, env) {
|
|
565
|
+
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
566
|
+
|
|
567
|
+
${usage}`, exitCode: 1 };
|
|
568
|
+
const users = new Users(await loadStore(env));
|
|
569
|
+
const address = normalizeEmail(email);
|
|
570
|
+
const user = await users.findByEmail(address);
|
|
571
|
+
if (!user) return { output: `No user has the e-mail address ${address}.`, exitCode: 1 };
|
|
572
|
+
const removed = await users.removeSms(user.id);
|
|
573
|
+
return {
|
|
574
|
+
output: removed ? `Removed the phone number of ${address}.` : `${address} has no phone number for SMS codes.`,
|
|
575
|
+
exitCode: 0
|
|
576
|
+
};
|
|
577
|
+
}
|
|
426
578
|
async function createUser(email, io, env) {
|
|
427
579
|
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
428
580
|
|
|
429
581
|
${usage}`, exitCode: 1 };
|
|
430
|
-
const
|
|
582
|
+
const store = await loadStore(env);
|
|
431
583
|
const password = await io.askSecret(`Password for ${email}: `);
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
584
|
+
const problem = await checkPasswordPolicy(password, await new Settings(store).passwordRules(), [email]);
|
|
585
|
+
if (problem) return { output: problem.message, exitCode: 1 };
|
|
434
586
|
const users = new Users(store);
|
|
435
587
|
const passwordHash = await hashPassword(password);
|
|
436
588
|
const user = await users.createUser({
|