@madauth/server 0.3.0-beta.2 → 0.4.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 +104 -42
- package/dist/chunk-3PACOYTG.js +1185 -0
- package/dist/{chunk-DCDTO5LG.js → chunk-A7UYZRBL.js} +1 -0
- package/dist/{chunk-CD7Y4T66.js → chunk-GA4TF7EI.js} +24 -2
- package/dist/{chunk-TI35AZ2U.js → chunk-ISJGOCF3.js} +250 -83
- package/dist/{chunk-56MOVFO4.js → chunk-OMIX3ERC.js} +11 -2
- package/dist/{chunk-3QRKWLR7.js → chunk-Y7ZSUROX.js} +4 -1
- package/dist/{config-Dg2fZuWR.d.ts → config-DkE_2D3-.d.ts} +5 -2
- 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 +29 -14
- package/dist/index.js +10 -6
- package/dist/{schema-3yhCc2jH.d.ts → schema-DTeiSuQB.d.ts} +39 -2
- package/dist/sqlite.js +2 -2
- package/dist/standalone/azure.mjs +1316 -676
- package/dist/standalone/lambda.mjs +1311 -671
- package/dist/standalone/main.mjs +1142 -502
- 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 +2 -0
- package/dist/{user-BMWEWREH.d.ts → user-BZs84sui.d.ts} +5 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +1 -1
- package/dist/{webhook-j5u9Kv9l.d.ts → webhook-B6P50ocX.d.ts} +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-DVVBIFIV.js +0 -718
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, e-mail & password 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,41 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createTablesSql,
|
|
4
4
|
upgradeTablesSql
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OMIX3ERC.js";
|
|
6
6
|
import {
|
|
7
7
|
madauthSchema
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GA4TF7EI.js";
|
|
9
9
|
import {
|
|
10
|
+
SECOND_FACTOR_POLICIES,
|
|
10
11
|
SIGN_IN_METHODS,
|
|
11
12
|
Settings,
|
|
12
13
|
Users,
|
|
13
14
|
anyOn,
|
|
15
|
+
canSignUp,
|
|
14
16
|
checkPasswordPolicy,
|
|
15
17
|
claimsFromJson,
|
|
16
18
|
hashPassword,
|
|
19
|
+
initialMethods,
|
|
20
|
+
isConfigured,
|
|
17
21
|
isOn,
|
|
18
22
|
isValidEmail,
|
|
19
23
|
loadConfig,
|
|
20
24
|
loadStore,
|
|
21
25
|
loadUserStoreConfig,
|
|
26
|
+
methodConfig,
|
|
22
27
|
normalizeEmail,
|
|
23
28
|
parseClaims,
|
|
24
29
|
parseRoles,
|
|
25
|
-
passwordAccountKey
|
|
26
|
-
|
|
30
|
+
passwordAccountKey,
|
|
31
|
+
policyOf
|
|
32
|
+
} from "./chunk-ISJGOCF3.js";
|
|
27
33
|
import {
|
|
28
34
|
generateSigningKey
|
|
29
35
|
} from "./chunk-JDQLSL36.js";
|
|
30
36
|
import {
|
|
31
37
|
DEV_WEBHOOK_RECEIVER_URL,
|
|
32
38
|
generateWebhookSecret
|
|
33
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-Y7ZSUROX.js";
|
|
34
40
|
|
|
35
41
|
// src/cli.ts
|
|
36
42
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -43,8 +49,9 @@ Commands:
|
|
|
43
49
|
init Ask a few questions, write the configuration to .env and print the next steps.
|
|
44
50
|
An answer given as an option is not asked for: --app-url <url>,
|
|
45
51
|
--google-client-id <id>, --google-client-secret <secret>, --password or
|
|
46
|
-
--no-password, --
|
|
47
|
-
|
|
52
|
+
--no-password, --totp or --no-totp (sign-up with the authenticator app),
|
|
53
|
+
--database <url>, --webhook-url <url>. --yes takes the default for the
|
|
54
|
+
rest. --out <path> writes another file, --force overwrites.
|
|
48
55
|
start Start the server on PORT (default ${PORT})
|
|
49
56
|
generate-key Print a new private ES256 key to use as MADAUTH_SIGNING_KEY
|
|
50
57
|
generate-webhook-secret Print a new secret to use as WEBHOOK_SECRET (madAuth and your receiver)
|
|
@@ -56,11 +63,17 @@ Commands:
|
|
|
56
63
|
get-roles <email> Print the roles of a user.
|
|
57
64
|
set-claims <email> <json> Replace the claims of a user, e.g. '{"roles":["admin"],"plan":"pro"}'.
|
|
58
65
|
get-claims <email> Print the claims of a user as JSON.
|
|
59
|
-
set-methods [method...]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
set-methods [method[=policy]...]
|
|
67
|
+
Switch sign-in methods on and off while the server runs: the listed ones
|
|
68
|
+
are on, the others off. Methods: google, password, totp (the authenticator
|
|
69
|
+
app on its own). google and password take a policy for the authenticator
|
|
70
|
+
app as a second factor: none (default), optional or required, e.g.
|
|
71
|
+
set-methods google password=required totp
|
|
72
|
+
get-methods Print which sign-in methods are switched on, with their policies.
|
|
73
|
+
remove-totp <email> Remove a user's authenticator app and recovery codes: the last resort when
|
|
74
|
+
both are lost. With a required policy, the user sets it up again next time.
|
|
75
|
+
The claims commands and remove-totp use DATABASE_URL from the environment;
|
|
76
|
+
the methods commands need the server's whole configuration (e.g. --env-file .env).
|
|
64
77
|
schema [--dialect <name>] [--from <version>]
|
|
65
78
|
Print the SQL that creates madAuth's tables for a custom store adapter.
|
|
66
79
|
Dialects: postgres (default), mysql, sqlite. With --from, print only the
|
|
@@ -157,6 +170,7 @@ function parseCliArgs(args) {
|
|
|
157
170
|
"google-client-id": { type: "string" },
|
|
158
171
|
"google-client-secret": { type: "string" },
|
|
159
172
|
password: { type: "boolean" },
|
|
173
|
+
totp: { type: "boolean" },
|
|
160
174
|
database: { type: "string" },
|
|
161
175
|
"webhook-url": { type: "string" },
|
|
162
176
|
yes: { type: "boolean" },
|
|
@@ -179,7 +193,15 @@ ${usage}`, exitCode: 1 };
|
|
|
179
193
|
}
|
|
180
194
|
const { values: options, positionals } = parsed;
|
|
181
195
|
const [command, ...rest] = positionals;
|
|
182
|
-
const argumentLimits = {
|
|
196
|
+
const argumentLimits = {
|
|
197
|
+
"set-roles": Infinity,
|
|
198
|
+
"set-methods": Infinity,
|
|
199
|
+
"set-claims": 2,
|
|
200
|
+
"create-user": 1,
|
|
201
|
+
"get-roles": 1,
|
|
202
|
+
"get-claims": 1,
|
|
203
|
+
"remove-totp": 1
|
|
204
|
+
};
|
|
183
205
|
const maxArguments = command ? argumentLimits[command] ?? 0 : 0;
|
|
184
206
|
if (rest.length > maxArguments) {
|
|
185
207
|
return { output: `Unexpected argument: ${rest.at(-1)}
|
|
@@ -223,6 +245,9 @@ ${usage}`, exitCode: 1 };
|
|
|
223
245
|
if (command === "set-methods" || command === "get-methods") {
|
|
224
246
|
return await methods(command, rest, env);
|
|
225
247
|
}
|
|
248
|
+
if (command === "remove-totp") {
|
|
249
|
+
return await removeTotp(rest[0], env);
|
|
250
|
+
}
|
|
226
251
|
} catch (e) {
|
|
227
252
|
return { output: e instanceof Error ? e.message : String(e), exitCode: 1 };
|
|
228
253
|
}
|
|
@@ -256,17 +281,20 @@ async function init(options, io) {
|
|
|
256
281
|
}
|
|
257
282
|
const clientSecret = clientId ? (options["google-client-secret"] ?? await askClientSecret()).trim() : "";
|
|
258
283
|
const password = options.password ?? /^y/i.test(await answer(void 0, "E-mail & password sign-in? (yes/no)", "yes"));
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
const totp = options.totp ?? /^y/i.test(await answer(void 0, "Sign-up with the authenticator app alone, without a password? (yes/no)", "no"));
|
|
285
|
+
const emails = password || totp;
|
|
286
|
+
if (!emails && options["webhook-url"] !== void 0) {
|
|
287
|
+
return fail("--webhook-url is for the e-mails of e-mail & password sign-in and of sign-up with the authenticator app, which are turned off.");
|
|
261
288
|
}
|
|
262
|
-
if (!clientId && !
|
|
263
|
-
return fail("No sign-in method is chosen. Give a Google client ID, turn on e-mail & password sign-in
|
|
289
|
+
if (!clientId && !emails) {
|
|
290
|
+
return fail("No sign-in method is chosen. Give a Google client ID, turn on e-mail & password sign-in or sign-up with the authenticator app.");
|
|
264
291
|
}
|
|
265
292
|
const database = await answer(options.database, "Database for the users", "sqlite:./madauth.db");
|
|
266
293
|
if (!/^(sqlite|dynamodb):./.test(database)) {
|
|
267
294
|
return fail(`The database must be sqlite:<path> or dynamodb:<table> but is "${database}".`);
|
|
268
295
|
}
|
|
269
|
-
const webhookUrl =
|
|
296
|
+
const webhookUrl = emails ? await answer(options["webhook-url"], "Webhook URL, where madAuth hands over its e-mails", "http://localhost:8790/webhook") : "";
|
|
297
|
+
const events = password ? "email.verify,email.reset,email.already_registered,email.no_password" : "email.verify,email.already_registered";
|
|
270
298
|
const variable = (name, value, comment) => `# ${comment}
|
|
271
299
|
${name}=${value}
|
|
272
300
|
`;
|
|
@@ -289,13 +317,9 @@ ${name}=${value}
|
|
|
289
317
|
clientId && variable("GOOGLE_CLIENT_ID", clientId, "Google sign-in: your OAuth client ID, of the type Web application."),
|
|
290
318
|
clientSecret && variable("GOOGLE_CLIENT_SECRET", clientSecret, "Client secret of the same client. It enables the redirect flow."),
|
|
291
319
|
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
|
-
),
|
|
320
|
+
emails && variable("WEBHOOK_URL", webhookUrl, "Your webhook receiver. madAuth hands its e-mails to it."),
|
|
321
|
+
emails && variable("WEBHOOK_SECRET", generateWebhookSecret(), "Signs every webhook call. Your receiver needs the same one."),
|
|
322
|
+
emails && variable("WEBHOOK_EVENTS", events, "The types your webhook receiver handles; only these are sent. Add e.g. signup.before or user.created."),
|
|
299
323
|
variable("PORT", String(PORT), "Port the server listens on.")
|
|
300
324
|
].filter(Boolean).join("");
|
|
301
325
|
await loadConfig({ ...parseEnv(content), DATABASE_URL: void 0 }, { store: {} });
|
|
@@ -306,21 +330,27 @@ ${name}=${value}
|
|
|
306
330
|
|
|
307
331
|
Next steps:
|
|
308
332
|
|
|
309
|
-
${nextSteps(file, origin, google, password)}`, exitCode: 0 };
|
|
333
|
+
${nextSteps(file, origin, google, password, totp)}`, exitCode: 0 };
|
|
310
334
|
}
|
|
311
|
-
function nextSteps(file, origin, google, password) {
|
|
335
|
+
function nextSteps(file, origin, google, password, totp = false) {
|
|
312
336
|
const server = `http://localhost:${PORT}`;
|
|
313
337
|
const envFile = /[^\w./-]/.test(file) ? `'${file.replaceAll("'", `'\\''`)}'` : file;
|
|
314
|
-
const providers = [google, password && "Password"].filter(Boolean);
|
|
338
|
+
const providers = [google, password && "Password", totp && "Totp"].filter(Boolean);
|
|
339
|
+
const methodList = [google && "google", password && "password", totp && "totp"].filter(Boolean).join(" ");
|
|
315
340
|
const googleOrigins = /* @__PURE__ */ new Set([origin, ...new URL(origin).hostname === "localhost" ? ["http://localhost"] : []]);
|
|
316
341
|
const steps = [
|
|
317
342
|
["Start the madAuth server:", "", ` npx @madauth/server start --env-file ${envFile}`],
|
|
318
|
-
password && [
|
|
343
|
+
(password || totp) && [
|
|
319
344
|
"madAuth hands its e-mails to your webhook receiver. For development, start the development receiver,",
|
|
320
345
|
`which prints them, with the same WEBHOOK_URL and WEBHOOK_SECRET as in ${file}:`,
|
|
321
346
|
"",
|
|
322
347
|
` ${DEV_WEBHOOK_RECEIVER_URL}`
|
|
323
348
|
],
|
|
349
|
+
totp && (google || password) && [
|
|
350
|
+
"Signing in with the authenticator app alone is off until an admin lists it. Once the server runs:",
|
|
351
|
+
"",
|
|
352
|
+
` npx @madauth/server set-methods ${methodList} --env-file ${envFile}`
|
|
353
|
+
],
|
|
324
354
|
[
|
|
325
355
|
"Proxy /auth and /.well-known from your app to the server, so that its cookies are first-party.",
|
|
326
356
|
"With Vite, in vite.config.ts:",
|
|
@@ -401,28 +431,60 @@ ${usage}`, exitCode: 1 };
|
|
|
401
431
|
const roles = Array.isArray(current.roles) ? current.roles : [];
|
|
402
432
|
return { output: roles.length ? `${address}: ${roles.join(" ")}` : `${address} has no roles.`, exitCode: 0 };
|
|
403
433
|
}
|
|
404
|
-
async function methods(command,
|
|
434
|
+
async function methods(command, args, env) {
|
|
405
435
|
const config = await loadConfig(env);
|
|
406
|
-
const
|
|
407
|
-
const
|
|
436
|
+
const forMethods = methodConfig(config);
|
|
437
|
+
const configured = (method) => isConfigured(forMethods, method);
|
|
438
|
+
const signsUp = (method) => canSignUp(forMethods, method);
|
|
439
|
+
const settings = new Settings(config.store, initialMethods(forMethods));
|
|
408
440
|
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
441
|
const next = {};
|
|
414
|
-
for (const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
442
|
+
for (const arg of args) {
|
|
443
|
+
const [name, policy, ...more] = arg.split("=");
|
|
444
|
+
if (!SIGN_IN_METHODS.includes(name) || more.length) {
|
|
445
|
+
return { output: `Unknown sign-in method "${arg}". Methods: ${SIGN_IN_METHODS.join(", ")}, e.g. set-methods google password=required totp.`, exitCode: 1 };
|
|
446
|
+
}
|
|
447
|
+
const method = name;
|
|
448
|
+
if (policy !== void 0 && (method === "totp" || !SECOND_FACTOR_POLICIES.includes(policy))) {
|
|
449
|
+
return {
|
|
450
|
+
output: `"${arg}": google and password take a policy for the authenticator app as a second factor: ${SECOND_FACTOR_POLICIES.join(", ")}.`,
|
|
451
|
+
exitCode: 1
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
next[method] = method === "totp" ? {} : { secondFactor: policy ?? "none" };
|
|
455
|
+
}
|
|
456
|
+
if (!anyOn(signsUp, next)) {
|
|
457
|
+
const usable = SIGN_IN_METHODS.filter(signsUp);
|
|
458
|
+
return {
|
|
459
|
+
output: `That would leave no sign-in method on that creates users, so nobody could sign up. On this server these do: ${usable.join(", ")}.`,
|
|
460
|
+
exitCode: 1
|
|
461
|
+
};
|
|
418
462
|
}
|
|
419
463
|
await settings.changeMethods(() => next, null);
|
|
420
464
|
}
|
|
421
465
|
const current = await settings.methods();
|
|
422
|
-
const
|
|
423
|
-
|
|
466
|
+
const describe = (method) => {
|
|
467
|
+
const policy = policyOf(method, current);
|
|
468
|
+
return method !== "totp" && policy !== "none" ? `${method} (authenticator app ${policy})` : method;
|
|
469
|
+
};
|
|
470
|
+
const on = SIGN_IN_METHODS.filter((method) => isOn(method, configured, current)).map(describe);
|
|
471
|
+
const off = SIGN_IN_METHODS.filter((method) => configured(method) && !isOn(method, configured, current));
|
|
424
472
|
return { output: `Switched on: ${on.join(", ")}.${off.length ? ` Switched off: ${off.join(", ")}.` : ""}`, exitCode: 0 };
|
|
425
473
|
}
|
|
474
|
+
async function removeTotp(email, env) {
|
|
475
|
+
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
476
|
+
|
|
477
|
+
${usage}`, exitCode: 1 };
|
|
478
|
+
const users = new Users(await loadStore(env));
|
|
479
|
+
const address = normalizeEmail(email);
|
|
480
|
+
const user = await users.findByEmail(address);
|
|
481
|
+
if (!user) return { output: `No user has the e-mail address ${address}.`, exitCode: 1 };
|
|
482
|
+
const removed = await users.removeTotp(user.id);
|
|
483
|
+
return {
|
|
484
|
+
output: removed ? `Removed the authenticator app of ${address}.` : `${address} has no authenticator app.`,
|
|
485
|
+
exitCode: 0
|
|
486
|
+
};
|
|
487
|
+
}
|
|
426
488
|
async function createUser(email, io, env) {
|
|
427
489
|
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
428
490
|
|