@madauth/server 0.2.0-beta.2 → 0.3.0-beta.2
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/bin.js +99 -32
- package/dist/{chunk-6EJDVZCD.js → chunk-3QRKWLR7.js} +10 -2
- package/dist/{chunk-CNL562LZ.js → chunk-56MOVFO4.js} +24 -2
- package/dist/{chunk-XNBWZZHT.js → chunk-CD7Y4T66.js} +11 -9
- package/dist/{chunk-7CTAB3QV.js → chunk-DCDTO5LG.js} +2 -2
- package/dist/{chunk-YALS7OMV.js → chunk-DVVBIFIV.js} +222 -122
- package/dist/{chunk-5CKEMC6M.js → chunk-TI35AZ2U.js} +227 -88
- package/dist/{config-BkxADgtp.d.ts → config-Dg2fZuWR.d.ts} +7 -6
- 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 +30 -7
- package/dist/index.js +15 -7
- package/dist/{schema-BSgYVRpd.d.ts → schema-3yhCc2jH.d.ts} +17 -9
- package/dist/sqlite.js +2 -2
- package/dist/standalone/azure.mjs +847 -597
- package/dist/standalone/lambda.mjs +847 -597
- package/dist/standalone/main.mjs +594 -344
- 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 +12 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +1 -1
- package/dist/{webhook-ltwulgcB.d.ts → webhook-j5u9Kv9l.d.ts} +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/package.json +1 -1
- package/dist/user-C1A3BfOo.d.ts +0 -12
|
@@ -10,28 +10,39 @@ import {
|
|
|
10
10
|
signSession,
|
|
11
11
|
signToken,
|
|
12
12
|
userFromClaims
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-DCDTO5LG.js";
|
|
14
14
|
import {
|
|
15
15
|
ADMIN_ROLE,
|
|
16
16
|
MAX_PASSWORD_LENGTH,
|
|
17
|
-
|
|
17
|
+
SIGN_IN_METHODS,
|
|
18
|
+
Settings,
|
|
18
19
|
Users,
|
|
20
|
+
anyOn,
|
|
19
21
|
checkPasswordPolicy,
|
|
22
|
+
claimsFromJson,
|
|
20
23
|
deriveCodeKey,
|
|
24
|
+
googleAccountKey,
|
|
21
25
|
hashPassword,
|
|
26
|
+
isAdmin,
|
|
27
|
+
isOn,
|
|
22
28
|
isValidEmail,
|
|
23
29
|
normalizeEmail,
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
parseClaims,
|
|
31
|
+
parseMethodSettings,
|
|
32
|
+
passwordAccountKey,
|
|
33
|
+
sameClaims,
|
|
34
|
+
toMadauthUser,
|
|
26
35
|
verifyAgainstDummy,
|
|
27
36
|
verifyPassword
|
|
28
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-TI35AZ2U.js";
|
|
29
38
|
import {
|
|
30
39
|
importSigningKeys
|
|
31
40
|
} from "./chunk-JDQLSL36.js";
|
|
32
41
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
WEBHOOK_TIMEOUT_MS,
|
|
43
|
+
createWebhookClient,
|
|
44
|
+
localeOf
|
|
45
|
+
} from "./chunk-3QRKWLR7.js";
|
|
35
46
|
|
|
36
47
|
// src/routes/google.ts
|
|
37
48
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
@@ -62,14 +73,13 @@ async function verifyGoogleIdToken(idToken, opts) {
|
|
|
62
73
|
if (payload.nonce !== opts.nonce) {
|
|
63
74
|
return { ok: false, error: "verification_failed", reason: "nonce mismatch" };
|
|
64
75
|
}
|
|
65
|
-
if (payload.email_verified !== true) {
|
|
76
|
+
if (typeof payload.email !== "string" || !payload.email || payload.email_verified !== true) {
|
|
66
77
|
return { ok: false, error: "email_unverified", reason: "e-mail address not verified" };
|
|
67
78
|
}
|
|
68
|
-
const
|
|
69
|
-
if (typeof payload.
|
|
70
|
-
if (typeof payload.
|
|
71
|
-
|
|
72
|
-
return { ok: true, user };
|
|
79
|
+
const profile = { sub: payload.sub, email: payload.email };
|
|
80
|
+
if (typeof payload.name === "string") profile.name = payload.name;
|
|
81
|
+
if (typeof payload.picture === "string") profile.picture = payload.picture;
|
|
82
|
+
return { ok: true, profile };
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
// src/routes/google.ts
|
|
@@ -87,10 +97,57 @@ async function pkceChallenge(verifier) {
|
|
|
87
97
|
return base64url.encode(new Uint8Array(digest));
|
|
88
98
|
}
|
|
89
99
|
function googleRoutes(app, ctx, google) {
|
|
90
|
-
const { config, keys, secure } = ctx;
|
|
100
|
+
const { config, keys, secure, users } = ctx;
|
|
91
101
|
const { issuer } = config;
|
|
92
102
|
const callbackUrl = `${issuer}/auth/google/callback`;
|
|
103
|
+
const resolveUser = async (profile, locale) => {
|
|
104
|
+
const key = googleAccountKey(profile.sub);
|
|
105
|
+
const emailNormalized = normalizeEmail(profile.email);
|
|
106
|
+
const signedIn = (user) => ({ ok: true, user: toMadauthUser(user, profile), sv: user.sessionVersion });
|
|
107
|
+
const account = await users.findAccountByKey(key);
|
|
108
|
+
if (account) {
|
|
109
|
+
const user = await users.findById(account.userId);
|
|
110
|
+
if (user) {
|
|
111
|
+
if (account.email !== profile.email) await users.updateAccount(account.id, { email: profile.email });
|
|
112
|
+
return signedIn(user);
|
|
113
|
+
}
|
|
114
|
+
await users.deleteAccount(account.id);
|
|
115
|
+
}
|
|
116
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
117
|
+
const existing = await users.findByEmail(emailNormalized);
|
|
118
|
+
if (existing) {
|
|
119
|
+
await users.verifyByProvider(existing);
|
|
120
|
+
if (!await users.linkAccount(existing.id, { key, email: profile.email })) {
|
|
121
|
+
const linked = await users.findAccountByKey(key);
|
|
122
|
+
const owner = linked && await users.findById(linked.userId);
|
|
123
|
+
if (!owner) return { ok: false, error: "temporarily_unavailable", message: "Please try again." };
|
|
124
|
+
return signedIn(owner);
|
|
125
|
+
}
|
|
126
|
+
return signedIn(existing);
|
|
127
|
+
}
|
|
128
|
+
if (attempt === 0) {
|
|
129
|
+
const check = await ctx.checkSignup({ email: profile.email, name: profile.name, locale, method: "google" });
|
|
130
|
+
if (!check.ok) {
|
|
131
|
+
return check.reason === "rejected" ? { ok: false, error: "signup_rejected", message: check.message } : { ok: false, error: "temporarily_unavailable", message: "Signing up is not possible right now. Please try again later." };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const user = await users.createUser({
|
|
135
|
+
email: profile.email,
|
|
136
|
+
emailNormalized,
|
|
137
|
+
name: profile.name ?? null,
|
|
138
|
+
emailVerified: true,
|
|
139
|
+
account: { key, email: profile.email }
|
|
140
|
+
});
|
|
141
|
+
if (user) {
|
|
142
|
+
await ctx.emit("user.created", { user: toMadauthUser(user), method: "google" });
|
|
143
|
+
return signedIn(user);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { ok: false, error: "temporarily_unavailable", message: "Please try again." };
|
|
147
|
+
};
|
|
148
|
+
const disabled = (c) => c.json({ error: "method_disabled", message: "Google sign-in is switched off." }, 403);
|
|
93
149
|
app.post("/auth/google/nonce", async (c) => {
|
|
150
|
+
if (!await ctx.enabled("google")) return disabled(c);
|
|
94
151
|
const nonce = randomString();
|
|
95
152
|
const token = await signToken(await keys, issuer, NONCE_TYP, { nonce }, NONCE_TTL);
|
|
96
153
|
setCookie(c, NONCE_COOKIE, token, {
|
|
@@ -103,6 +160,7 @@ function googleRoutes(app, ctx, google) {
|
|
|
103
160
|
return c.json({ nonce });
|
|
104
161
|
});
|
|
105
162
|
app.post("/auth/google/verify", async (c) => {
|
|
163
|
+
if (!await ctx.enabled("google")) return disabled(c);
|
|
106
164
|
const body = await c.req.json().catch(() => ({}));
|
|
107
165
|
if (typeof body.credential !== "string" || !body.credential) {
|
|
108
166
|
return c.json({ error: "verification_failed", message: "credential is missing" }, 400);
|
|
@@ -120,12 +178,17 @@ function googleRoutes(app, ctx, google) {
|
|
|
120
178
|
return c.json({ error: result.error, message: result.reason }, result.error === "email_unverified" ? 403 : 401);
|
|
121
179
|
}
|
|
122
180
|
deleteCookie(c, NONCE_COOKIE, { path: "/auth/google", secure });
|
|
123
|
-
const
|
|
181
|
+
const resolved = await resolveUser(result.profile, localeOf(body.locale));
|
|
182
|
+
if (!resolved.ok) {
|
|
183
|
+
return c.json({ error: resolved.error, message: resolved.message }, resolved.error === "signup_rejected" ? 403 : 503);
|
|
184
|
+
}
|
|
185
|
+
const user = await ctx.startSession(c, resolved.user, ["google"], { sv: resolved.sv });
|
|
124
186
|
await ctx.emit("user.signed_in", { user, method: "google" });
|
|
125
187
|
return c.json({ user });
|
|
126
188
|
});
|
|
127
189
|
const requireCodeFlow = async (c, next) => {
|
|
128
190
|
if (!google.clientSecret) return c.json({ error: "not_found" }, 404);
|
|
191
|
+
if (!await ctx.enabled("google")) return disabled(c);
|
|
129
192
|
await next();
|
|
130
193
|
};
|
|
131
194
|
app.get("/auth/google/start", requireCodeFlow, async (c) => {
|
|
@@ -135,7 +198,8 @@ function googleRoutes(app, ctx, google) {
|
|
|
135
198
|
state: randomString(),
|
|
136
199
|
nonce: randomString(),
|
|
137
200
|
verifier: randomString(),
|
|
138
|
-
returnTo: returnUrl.href
|
|
201
|
+
returnTo: returnUrl.href,
|
|
202
|
+
locale: localeOf(c.req.query("locale"))
|
|
139
203
|
};
|
|
140
204
|
const token = await signToken(await keys, issuer, OAUTH_TYP, { ...oauth }, OAUTH_TTL);
|
|
141
205
|
setCookie(c, OAUTH_COOKIE, token, {
|
|
@@ -196,7 +260,9 @@ function googleRoutes(app, ctx, google) {
|
|
|
196
260
|
keys: config.jwksResolver
|
|
197
261
|
});
|
|
198
262
|
if (!result.ok) return back(result.error);
|
|
199
|
-
const
|
|
263
|
+
const resolved = await resolveUser(result.profile, typeof oauth.locale === "string" ? oauth.locale : void 0);
|
|
264
|
+
if (!resolved.ok) return back(resolved.error);
|
|
265
|
+
const user = await ctx.startSession(c, resolved.user, ["google"], { sv: resolved.sv });
|
|
200
266
|
await ctx.emit("user.signed_in", { user, method: "google" });
|
|
201
267
|
return back();
|
|
202
268
|
});
|
|
@@ -213,18 +279,16 @@ var MAX_LOCK_MS = 15 * 60 * 1e3;
|
|
|
213
279
|
var MAIL_INTERVAL_MS = 60 * 1e3;
|
|
214
280
|
var VERIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
215
281
|
var RESET_TTL_MS = 30 * 60 * 1e3;
|
|
216
|
-
var WEBHOOK_TIMEOUT_MS = 1e4;
|
|
217
282
|
var VERIFY_LINK_PARAM = "madauth_verify";
|
|
218
283
|
var RESET_LINK_PARAM = "madauth_reset";
|
|
219
284
|
var MAX_NAME_LENGTH = 100;
|
|
220
|
-
var MAX_LOCALE_LENGTH = 35;
|
|
221
|
-
function toMadauthUser(user) {
|
|
222
|
-
const result = { id: user.id, email: user.email };
|
|
223
|
-
if (user.name) result.name = user.name;
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
285
|
function passwordRoutes(app, ctx, password, users, webhook) {
|
|
227
286
|
const { minLength } = password;
|
|
287
|
+
const disabled = (c) => c.json({ error: "method_disabled", message: "E-mail & password sign-in is switched off." }, 403);
|
|
288
|
+
app.use("/auth/password/*", async (c, next) => {
|
|
289
|
+
if (!await ctx.enabled("password")) return disabled(c);
|
|
290
|
+
await next();
|
|
291
|
+
});
|
|
228
292
|
const body = async (c) => {
|
|
229
293
|
const data = await c.req.json().catch(() => null);
|
|
230
294
|
return data && typeof data === "object" ? data : {};
|
|
@@ -260,7 +324,11 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
260
324
|
locale
|
|
261
325
|
});
|
|
262
326
|
};
|
|
263
|
-
const
|
|
327
|
+
const sendAccountEmail = async (user, type, target, locale) => {
|
|
328
|
+
if (!webhook.wants(type)) return true;
|
|
329
|
+
const methods = await users.signInMethods(user.id);
|
|
330
|
+
return sendEmail(user, type, { link: target.href, site: target.host, locale, methods });
|
|
331
|
+
};
|
|
264
332
|
const consume = async (data, purpose) => {
|
|
265
333
|
if (typeof data.token === "string" && data.token) {
|
|
266
334
|
return { userId: await users.consumeLinkToken(data.token, purpose), via: "link" };
|
|
@@ -322,42 +390,31 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
322
390
|
if (policy) return error(c, 400, "weak_password", policy);
|
|
323
391
|
const name = typeof data.name === "string" && data.name.trim() ? data.name.trim().slice(0, MAX_NAME_LENGTH) : null;
|
|
324
392
|
const email = data.email.trim();
|
|
325
|
-
const locale = localeOf(data);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!check.ok) {
|
|
329
|
-
console.error(`[madauth] Webhook "signup.before" failed: ${check.reason}`);
|
|
330
|
-
return unavailable(c);
|
|
331
|
-
}
|
|
332
|
-
const answer = check.body;
|
|
333
|
-
if (answer?.allow === false) {
|
|
334
|
-
const message = typeof answer.message === "string" && answer.message ? answer.message : "Sign-up is not possible with this e-mail address.";
|
|
335
|
-
return error(c, 403, "signup_rejected", message);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
393
|
+
const locale = localeOf(data.locale);
|
|
394
|
+
const check = await ctx.checkSignup({ email, name: name ?? void 0, locale, method: "password" });
|
|
395
|
+
if (!check.ok) return check.reason === "rejected" ? error(c, 403, "signup_rejected", check.message) : unavailable(c);
|
|
338
396
|
const passwordHash = await hashPassword(data.password);
|
|
339
397
|
const existing = await users.findByEmail(normalizeEmail(email));
|
|
340
398
|
if (existing?.emailVerified) {
|
|
341
|
-
|
|
342
|
-
const sent = await sendEmail(existing, "email.already_registered", { link: target.href, site: target.host, locale });
|
|
343
|
-
return sent ? accepted(c) : unavailable(c);
|
|
399
|
+
return await sendAccountEmail(existing, "email.already_registered", target, locale) ? accepted(c) : unavailable(c);
|
|
344
400
|
}
|
|
345
401
|
if (existing) {
|
|
346
402
|
if (Date.now() - existing.lastMailAt < MAIL_INTERVAL_MS) return accepted(c);
|
|
347
403
|
const account = await users.passwordAccount(existing.id);
|
|
348
404
|
if (account) await users.updateAccount(account.id, { secret: passwordHash, failedAttempts: 0, lockedUntil: 0 });
|
|
405
|
+
else await users.linkAccount(existing.id, { key: passwordAccountKey(existing.id), secret: passwordHash });
|
|
349
406
|
await users.updateUser(existing.id, { name });
|
|
350
407
|
return await sendLinkEmail({ ...existing, name }, "verify", target, locale) ? accepted(c) : unavailable(c);
|
|
351
408
|
}
|
|
352
|
-
const user = await users.
|
|
409
|
+
const user = await users.createUser({
|
|
353
410
|
email,
|
|
354
411
|
emailNormalized: normalizeEmail(email),
|
|
355
412
|
name,
|
|
356
|
-
|
|
357
|
-
|
|
413
|
+
emailVerified: false,
|
|
414
|
+
account: (userId) => ({ key: passwordAccountKey(userId), secret: passwordHash })
|
|
358
415
|
});
|
|
359
416
|
if (!user) return accepted(c);
|
|
360
|
-
await ctx.emit("user.created", { user: toMadauthUser(user) });
|
|
417
|
+
await ctx.emit("user.created", { user: toMadauthUser(user), method: "password" });
|
|
361
418
|
return await sendLinkEmail(user, "verify", target, locale) ? accepted(c) : unavailable(c);
|
|
362
419
|
});
|
|
363
420
|
app.post("/auth/password/send-verification", async (c) => {
|
|
@@ -367,7 +424,7 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
367
424
|
if (!isValidEmail(data.email)) return error(c, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
368
425
|
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
369
426
|
if (user && !user.emailVerified && await users.passwordAccount(user.id)) {
|
|
370
|
-
if (!await sendLinkEmail(user, "verify", target, localeOf(data))) return unavailable(c);
|
|
427
|
+
if (!await sendLinkEmail(user, "verify", target, localeOf(data.locale))) return unavailable(c);
|
|
371
428
|
}
|
|
372
429
|
return accepted(c);
|
|
373
430
|
});
|
|
@@ -388,8 +445,11 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
388
445
|
if (target instanceof Response) return target;
|
|
389
446
|
if (!isValidEmail(data.email)) return error(c, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
390
447
|
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
448
|
+
const locale = localeOf(data.locale);
|
|
391
449
|
if (user && await users.passwordAccount(user.id)) {
|
|
392
|
-
if (!await sendLinkEmail(user, "reset", target,
|
|
450
|
+
if (!await sendLinkEmail(user, "reset", target, locale)) return unavailable(c);
|
|
451
|
+
} else if (user) {
|
|
452
|
+
if (!await sendAccountEmail(user, "email.no_password", target, locale)) return unavailable(c);
|
|
393
453
|
}
|
|
394
454
|
return accepted(c);
|
|
395
455
|
});
|
|
@@ -401,12 +461,13 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
401
461
|
const user = consumed.userId ? await users.findById(consumed.userId) : null;
|
|
402
462
|
const account = user ? await users.passwordAccount(user.id) : null;
|
|
403
463
|
if (!user || !account) return invalidVerification(c, consumed);
|
|
404
|
-
|
|
464
|
+
const secret = await hashPassword(data.password);
|
|
465
|
+
if (!await users.updateAccount(account.id, { secret, failedAttempts: 0, lockedUntil: 0 })) return invalidVerification(c, consumed);
|
|
405
466
|
const sessionVersion = user.sessionVersion + 1;
|
|
406
467
|
await users.updateUser(user.id, { emailVerified: true, sessionVersion, wrongCodes: 0 });
|
|
407
468
|
await users.clearVerifications(user.id);
|
|
408
469
|
const result = await ctx.startSession(c, toMadauthUser(user), ["pwd"], { sv: sessionVersion });
|
|
409
|
-
await ctx.emit("
|
|
470
|
+
await ctx.emit("email.password_reset", { user: result });
|
|
410
471
|
await ctx.emit("user.signed_in", { user: result, method: "password" });
|
|
411
472
|
return c.json({ user: result });
|
|
412
473
|
});
|
|
@@ -419,12 +480,9 @@ function createApp(config) {
|
|
|
419
480
|
const keys = importSigningKeys(config.signingKey);
|
|
420
481
|
const secure = issuer.startsWith("https:");
|
|
421
482
|
const isAllowedOrigin = (origin) => !!origin && allowedOrigins.includes(origin);
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
const current = roles && user.email ? await roles.get(normalizeEmail(user.email)) : [];
|
|
426
|
-
return current.length ? { ...rest, roles: current } : rest;
|
|
427
|
-
};
|
|
483
|
+
const users = new Users(config.store, deriveCodeKey(config.signingKey.d));
|
|
484
|
+
const settings = new Settings(config.store);
|
|
485
|
+
const available = (method) => method === "google" ? !!config.google : !!config.password;
|
|
428
486
|
const issueSession = async (c, user, amr, extra) => {
|
|
429
487
|
const signingKeys = await keys;
|
|
430
488
|
const [sessionToken, renewalToken] = await Promise.all([
|
|
@@ -476,12 +534,29 @@ function createApp(config) {
|
|
|
476
534
|
return url;
|
|
477
535
|
},
|
|
478
536
|
async startSession(c, user, amr, extra) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return signedIn;
|
|
537
|
+
await issueSession(c, user, amr, extra);
|
|
538
|
+
return user;
|
|
482
539
|
},
|
|
483
|
-
users
|
|
540
|
+
users,
|
|
541
|
+
settings,
|
|
484
542
|
webhook: config.webhook ? createWebhookClient(config.webhook, config.webhookFetch) : void 0,
|
|
543
|
+
async enabled(method) {
|
|
544
|
+
return isOn(method, available, await settings.methods());
|
|
545
|
+
},
|
|
546
|
+
async checkSignup(data) {
|
|
547
|
+
if (!this.webhook?.wants("signup.before")) return { ok: true };
|
|
548
|
+
const check = await this.webhook.call("signup.before", data, WEBHOOK_TIMEOUT_MS);
|
|
549
|
+
if (!check.ok) {
|
|
550
|
+
console.error(`[madauth] Webhook "signup.before" failed: ${check.reason}`);
|
|
551
|
+
return { ok: false, reason: "unavailable" };
|
|
552
|
+
}
|
|
553
|
+
const answer = check.body;
|
|
554
|
+
if (answer?.allow === false) {
|
|
555
|
+
const message = typeof answer.message === "string" && answer.message ? answer.message : "Sign-up is not possible with this e-mail address.";
|
|
556
|
+
return { ok: false, reason: "rejected", message };
|
|
557
|
+
}
|
|
558
|
+
return { ok: true };
|
|
559
|
+
},
|
|
485
560
|
async emit(type, data) {
|
|
486
561
|
if (!this.webhook?.wants(type)) return;
|
|
487
562
|
const result = await this.webhook.call(type, data, EVENT_TIMEOUT_MS);
|
|
@@ -511,29 +586,28 @@ function createApp(config) {
|
|
|
511
586
|
c.header("Cache-Control", "public, max-age=3600");
|
|
512
587
|
return c.json({ keys: [publicJwk] });
|
|
513
588
|
});
|
|
514
|
-
app.get(
|
|
515
|
-
|
|
516
|
-
(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
589
|
+
app.get("/auth/config", async (c) => {
|
|
590
|
+
const methods = await settings.methods();
|
|
591
|
+
const on = (method) => isOn(method, available, methods);
|
|
592
|
+
return c.json({
|
|
593
|
+
google: on("google") ? { clientId: config.google.clientId, codeFlow: !!config.google.clientSecret } : null,
|
|
594
|
+
password: on("password") ? { minLength: config.password.minLength } : null
|
|
595
|
+
});
|
|
596
|
+
});
|
|
521
597
|
if (config.google) googleRoutes(app, ctx, config.google);
|
|
522
|
-
if (config.password && ctx.
|
|
598
|
+
if (config.password && ctx.webhook) passwordRoutes(app, ctx, config.password, users, ctx.webhook);
|
|
523
599
|
const tokenClaims = async (c, cookie, typ) => readToken(await keys, issuer, typ, getCookie2(c, cookie));
|
|
524
|
-
const hasEnded = async (claims) => {
|
|
525
|
-
if (!claims.sub.startsWith("usr_")) return false;
|
|
526
|
-
const stored = await ctx.users?.findById(claims.sub);
|
|
527
|
-
return !stored || stored.sessionVersion !== claims.sv;
|
|
528
|
-
};
|
|
529
600
|
const checked = async (claims) => {
|
|
530
|
-
const
|
|
531
|
-
|
|
601
|
+
const stored = await users.findById(claims.sub);
|
|
602
|
+
if (!stored || stored.sessionVersion !== claims.sv) return null;
|
|
603
|
+
const { claims: _, ...user } = userFromClaims(claims);
|
|
604
|
+
const current = claimsFromJson(stored.claims);
|
|
605
|
+
return Object.keys(current).length ? { ...user, claims: current } : user;
|
|
532
606
|
};
|
|
533
607
|
const currentSession = async (c) => {
|
|
534
608
|
const session = await tokenClaims(c, SESSION_COOKIE, SESSION_TYP);
|
|
535
609
|
const user = session ? await checked(session) : null;
|
|
536
|
-
const upToDate = session && user &&
|
|
610
|
+
const upToDate = session && user && sameClaims(user.claims, session.claims) && Date.now() / 1e3 - session.iat <= sessionTtlSeconds / 2;
|
|
537
611
|
if (upToDate) return user;
|
|
538
612
|
const renewal = await tokenClaims(c, RENEWAL_COOKIE, RENEWAL_TYP);
|
|
539
613
|
const sameSignIn = session && user && renewal && renewal.sub === session.sub && renewal.sv === session.sv && renewal.email === session.email;
|
|
@@ -559,56 +633,82 @@ function createApp(config) {
|
|
|
559
633
|
app.post("/auth/account/delete", async (c) => {
|
|
560
634
|
const user = await currentSession(c);
|
|
561
635
|
if (!user) return c.json({ error: "no_session" }, 401);
|
|
562
|
-
|
|
563
|
-
const passwordUserId = ctx.users && user.email ? await ctx.users.deleteByEmail(normalizeEmail(user.email)) : null;
|
|
636
|
+
await users.deleteById(user.id);
|
|
564
637
|
clearSession(c);
|
|
565
|
-
await ctx.emit("user.deleted", { user
|
|
638
|
+
await ctx.emit("user.deleted", { user });
|
|
566
639
|
return c.body(null, 204);
|
|
567
640
|
});
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
});
|
|
611
|
-
|
|
641
|
+
const admin = async (c) => {
|
|
642
|
+
const user = await currentSession(c);
|
|
643
|
+
if (!user) return { answer: c.json({ error: "no_session" }, 401) };
|
|
644
|
+
const email = user.email ? normalizeEmail(user.email) : void 0;
|
|
645
|
+
if (!email || !isAdmin(user)) {
|
|
646
|
+
const message = `Only users with the role "${ADMIN_ROLE}" can manage claims and settings.`;
|
|
647
|
+
return { answer: c.json({ error: "forbidden", message }, 403) };
|
|
648
|
+
}
|
|
649
|
+
return { email };
|
|
650
|
+
};
|
|
651
|
+
const body = async (c) => {
|
|
652
|
+
const data = await c.req.json().catch(() => null);
|
|
653
|
+
return data && typeof data === "object" ? data : {};
|
|
654
|
+
};
|
|
655
|
+
const invalidEmail = (c) => c.json({ error: "invalid_email", message: "This is not a valid e-mail address." }, 400);
|
|
656
|
+
const noUser = (c) => c.json({ error: "user_not_found", message: "No user has this e-mail address." }, 404);
|
|
657
|
+
app.post("/auth/admin/claims/get", async (c) => {
|
|
658
|
+
const caller = await admin(c);
|
|
659
|
+
if ("answer" in caller) return caller.answer;
|
|
660
|
+
const data = await body(c);
|
|
661
|
+
if (!isValidEmail(data.email)) return invalidEmail(c);
|
|
662
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
663
|
+
if (!user) return noUser(c);
|
|
664
|
+
return c.json({ email: user.emailNormalized, userId: user.id, claims: claimsFromJson(user.claims) });
|
|
665
|
+
});
|
|
666
|
+
app.post("/auth/admin/claims/set", async (c) => {
|
|
667
|
+
const caller = await admin(c);
|
|
668
|
+
if ("answer" in caller) return caller.answer;
|
|
669
|
+
const data = await body(c);
|
|
670
|
+
if (!isValidEmail(data.email)) return invalidEmail(c);
|
|
671
|
+
const claims = parseClaims(data.claims);
|
|
672
|
+
if (!claims) {
|
|
673
|
+
return c.json(
|
|
674
|
+
{
|
|
675
|
+
error: "invalid_claims",
|
|
676
|
+
message: 'claims must be a JSON object of at most 2048 characters whose keys are names (letters, digits and "_", starting with a letter). "roles" must be a list of names of lower-case letters, digits, "-" and "_".'
|
|
677
|
+
},
|
|
678
|
+
400
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
682
|
+
if (!user) return noUser(c);
|
|
683
|
+
await users.updateUser(user.id, { claims: Object.keys(claims).length ? JSON.stringify(claims) : null });
|
|
684
|
+
const email = user.emailNormalized;
|
|
685
|
+
await ctx.emit("user.claims_changed", { userId: user.id, email, claims, by: caller.email });
|
|
686
|
+
return c.json({ email, userId: user.id, claims });
|
|
687
|
+
});
|
|
688
|
+
const settingsAnswer = (methods) => ({
|
|
689
|
+
methods: Object.fromEntries(
|
|
690
|
+
SIGN_IN_METHODS.map((method) => [method, { available: available(method), enabled: isOn(method, available, methods) }])
|
|
691
|
+
)
|
|
692
|
+
});
|
|
693
|
+
app.post("/auth/admin/settings/get", async (c) => {
|
|
694
|
+
const caller = await admin(c);
|
|
695
|
+
if ("answer" in caller) return caller.answer;
|
|
696
|
+
return c.json(settingsAnswer(await settings.methods()));
|
|
697
|
+
});
|
|
698
|
+
app.post("/auth/admin/settings/set", async (c) => {
|
|
699
|
+
const caller = await admin(c);
|
|
700
|
+
if ("answer" in caller) return caller.answer;
|
|
701
|
+
const data = await body(c);
|
|
702
|
+
const invalid = (message) => c.json({ error: "invalid_settings", message }, 400);
|
|
703
|
+
const changes = parseMethodSettings(data.methods);
|
|
704
|
+
if (!changes) return invalid(`methods must be an object with true or false for ${SIGN_IN_METHODS.join(" and/or ")}.`);
|
|
705
|
+
const methods = await settings.changeMethods((current) => {
|
|
706
|
+
const next = { ...current, ...changes };
|
|
707
|
+
return anyOn(available, next) ? next : null;
|
|
708
|
+
}, caller.email);
|
|
709
|
+
if (!methods) return invalid("At least one sign-in method must stay on.");
|
|
710
|
+
return c.json(settingsAnswer(methods));
|
|
711
|
+
});
|
|
612
712
|
return app;
|
|
613
713
|
}
|
|
614
714
|
|