@madauth/server 0.2.0-beta.1 → 0.3.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/dist/bin.js +98 -31
- 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-6EJDVZCD.js → chunk-NPNM26J3.js} +9 -2
- package/dist/{chunk-5CKEMC6M.js → chunk-ODITZLMM.js} +218 -86
- package/dist/{chunk-YALS7OMV.js → chunk-ZSBKNV5W.js} +217 -122
- package/dist/{config-BkxADgtp.d.ts → config-DEBYi8hb.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 +799 -564
- package/dist/standalone/lambda.mjs +799 -564
- package/dist/standalone/main.mjs +563 -328
- 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-DCDP6SaE.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-ODITZLMM.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-NPNM26J3.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,6 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
260
324
|
locale
|
|
261
325
|
});
|
|
262
326
|
};
|
|
263
|
-
const localeOf = (data) => typeof data.locale === "string" && data.locale.length <= MAX_LOCALE_LENGTH ? data.locale : void 0;
|
|
264
327
|
const consume = async (data, purpose) => {
|
|
265
328
|
if (typeof data.token === "string" && data.token) {
|
|
266
329
|
return { userId: await users.consumeLinkToken(data.token, purpose), via: "link" };
|
|
@@ -322,19 +385,9 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
322
385
|
if (policy) return error(c, 400, "weak_password", policy);
|
|
323
386
|
const name = typeof data.name === "string" && data.name.trim() ? data.name.trim().slice(0, MAX_NAME_LENGTH) : null;
|
|
324
387
|
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
|
-
}
|
|
388
|
+
const locale = localeOf(data.locale);
|
|
389
|
+
const check = await ctx.checkSignup({ email, name: name ?? void 0, locale, method: "password" });
|
|
390
|
+
if (!check.ok) return check.reason === "rejected" ? error(c, 403, "signup_rejected", check.message) : unavailable(c);
|
|
338
391
|
const passwordHash = await hashPassword(data.password);
|
|
339
392
|
const existing = await users.findByEmail(normalizeEmail(email));
|
|
340
393
|
if (existing?.emailVerified) {
|
|
@@ -346,18 +399,19 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
346
399
|
if (Date.now() - existing.lastMailAt < MAIL_INTERVAL_MS) return accepted(c);
|
|
347
400
|
const account = await users.passwordAccount(existing.id);
|
|
348
401
|
if (account) await users.updateAccount(account.id, { secret: passwordHash, failedAttempts: 0, lockedUntil: 0 });
|
|
402
|
+
else await users.linkAccount(existing.id, { key: passwordAccountKey(existing.id), secret: passwordHash });
|
|
349
403
|
await users.updateUser(existing.id, { name });
|
|
350
404
|
return await sendLinkEmail({ ...existing, name }, "verify", target, locale) ? accepted(c) : unavailable(c);
|
|
351
405
|
}
|
|
352
|
-
const user = await users.
|
|
406
|
+
const user = await users.createUser({
|
|
353
407
|
email,
|
|
354
408
|
emailNormalized: normalizeEmail(email),
|
|
355
409
|
name,
|
|
356
|
-
|
|
357
|
-
|
|
410
|
+
emailVerified: false,
|
|
411
|
+
account: (userId) => ({ key: passwordAccountKey(userId), secret: passwordHash })
|
|
358
412
|
});
|
|
359
413
|
if (!user) return accepted(c);
|
|
360
|
-
await ctx.emit("user.created", { user: toMadauthUser(user) });
|
|
414
|
+
await ctx.emit("user.created", { user: toMadauthUser(user), method: "password" });
|
|
361
415
|
return await sendLinkEmail(user, "verify", target, locale) ? accepted(c) : unavailable(c);
|
|
362
416
|
});
|
|
363
417
|
app.post("/auth/password/send-verification", async (c) => {
|
|
@@ -367,7 +421,7 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
367
421
|
if (!isValidEmail(data.email)) return error(c, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
368
422
|
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
369
423
|
if (user && !user.emailVerified && await users.passwordAccount(user.id)) {
|
|
370
|
-
if (!await sendLinkEmail(user, "verify", target, localeOf(data))) return unavailable(c);
|
|
424
|
+
if (!await sendLinkEmail(user, "verify", target, localeOf(data.locale))) return unavailable(c);
|
|
371
425
|
}
|
|
372
426
|
return accepted(c);
|
|
373
427
|
});
|
|
@@ -388,8 +442,8 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
388
442
|
if (target instanceof Response) return target;
|
|
389
443
|
if (!isValidEmail(data.email)) return error(c, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
390
444
|
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
391
|
-
if (user && await users.passwordAccount(user.id)) {
|
|
392
|
-
if (!await sendLinkEmail(user, "reset", target, localeOf(data))) return unavailable(c);
|
|
445
|
+
if (user && (user.emailVerified || await users.passwordAccount(user.id))) {
|
|
446
|
+
if (!await sendLinkEmail(user, "reset", target, localeOf(data.locale))) return unavailable(c);
|
|
393
447
|
}
|
|
394
448
|
return accepted(c);
|
|
395
449
|
});
|
|
@@ -399,14 +453,16 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
399
453
|
if (policy) return error(c, 400, "weak_password", policy);
|
|
400
454
|
const consumed = await consume(data, "reset");
|
|
401
455
|
const user = consumed.userId ? await users.findById(consumed.userId) : null;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
456
|
+
if (!user) return invalidVerification(c, consumed);
|
|
457
|
+
const secret = await hashPassword(data.password);
|
|
458
|
+
const account = await users.passwordAccount(user.id);
|
|
459
|
+
if (account) await users.updateAccount(account.id, { secret, failedAttempts: 0, lockedUntil: 0 });
|
|
460
|
+
else await users.linkAccount(user.id, { key: passwordAccountKey(user.id), secret });
|
|
405
461
|
const sessionVersion = user.sessionVersion + 1;
|
|
406
462
|
await users.updateUser(user.id, { emailVerified: true, sessionVersion, wrongCodes: 0 });
|
|
407
463
|
await users.clearVerifications(user.id);
|
|
408
464
|
const result = await ctx.startSession(c, toMadauthUser(user), ["pwd"], { sv: sessionVersion });
|
|
409
|
-
await ctx.emit("
|
|
465
|
+
await ctx.emit("email.password_reset", { user: result });
|
|
410
466
|
await ctx.emit("user.signed_in", { user: result, method: "password" });
|
|
411
467
|
return c.json({ user: result });
|
|
412
468
|
});
|
|
@@ -419,12 +475,9 @@ function createApp(config) {
|
|
|
419
475
|
const keys = importSigningKeys(config.signingKey);
|
|
420
476
|
const secure = issuer.startsWith("https:");
|
|
421
477
|
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
|
-
};
|
|
478
|
+
const users = new Users(config.store, deriveCodeKey(config.signingKey.d));
|
|
479
|
+
const settings = new Settings(config.store);
|
|
480
|
+
const available = (method) => method === "google" ? !!config.google : !!config.password;
|
|
428
481
|
const issueSession = async (c, user, amr, extra) => {
|
|
429
482
|
const signingKeys = await keys;
|
|
430
483
|
const [sessionToken, renewalToken] = await Promise.all([
|
|
@@ -476,12 +529,29 @@ function createApp(config) {
|
|
|
476
529
|
return url;
|
|
477
530
|
},
|
|
478
531
|
async startSession(c, user, amr, extra) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return signedIn;
|
|
532
|
+
await issueSession(c, user, amr, extra);
|
|
533
|
+
return user;
|
|
482
534
|
},
|
|
483
|
-
users
|
|
535
|
+
users,
|
|
536
|
+
settings,
|
|
484
537
|
webhook: config.webhook ? createWebhookClient(config.webhook, config.webhookFetch) : void 0,
|
|
538
|
+
async enabled(method) {
|
|
539
|
+
return isOn(method, available, await settings.methods());
|
|
540
|
+
},
|
|
541
|
+
async checkSignup(data) {
|
|
542
|
+
if (!this.webhook?.wants("signup.before")) return { ok: true };
|
|
543
|
+
const check = await this.webhook.call("signup.before", data, WEBHOOK_TIMEOUT_MS);
|
|
544
|
+
if (!check.ok) {
|
|
545
|
+
console.error(`[madauth] Webhook "signup.before" failed: ${check.reason}`);
|
|
546
|
+
return { ok: false, reason: "unavailable" };
|
|
547
|
+
}
|
|
548
|
+
const answer = check.body;
|
|
549
|
+
if (answer?.allow === false) {
|
|
550
|
+
const message = typeof answer.message === "string" && answer.message ? answer.message : "Sign-up is not possible with this e-mail address.";
|
|
551
|
+
return { ok: false, reason: "rejected", message };
|
|
552
|
+
}
|
|
553
|
+
return { ok: true };
|
|
554
|
+
},
|
|
485
555
|
async emit(type, data) {
|
|
486
556
|
if (!this.webhook?.wants(type)) return;
|
|
487
557
|
const result = await this.webhook.call(type, data, EVENT_TIMEOUT_MS);
|
|
@@ -511,29 +581,28 @@ function createApp(config) {
|
|
|
511
581
|
c.header("Cache-Control", "public, max-age=3600");
|
|
512
582
|
return c.json({ keys: [publicJwk] });
|
|
513
583
|
});
|
|
514
|
-
app.get(
|
|
515
|
-
|
|
516
|
-
(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
584
|
+
app.get("/auth/config", async (c) => {
|
|
585
|
+
const methods = await settings.methods();
|
|
586
|
+
const on = (method) => isOn(method, available, methods);
|
|
587
|
+
return c.json({
|
|
588
|
+
google: on("google") ? { clientId: config.google.clientId, codeFlow: !!config.google.clientSecret } : null,
|
|
589
|
+
password: on("password") ? { minLength: config.password.minLength } : null
|
|
590
|
+
});
|
|
591
|
+
});
|
|
521
592
|
if (config.google) googleRoutes(app, ctx, config.google);
|
|
522
|
-
if (config.password && ctx.
|
|
593
|
+
if (config.password && ctx.webhook) passwordRoutes(app, ctx, config.password, users, ctx.webhook);
|
|
523
594
|
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
595
|
const checked = async (claims) => {
|
|
530
|
-
const
|
|
531
|
-
|
|
596
|
+
const stored = await users.findById(claims.sub);
|
|
597
|
+
if (!stored || stored.sessionVersion !== claims.sv) return null;
|
|
598
|
+
const { claims: _, ...user } = userFromClaims(claims);
|
|
599
|
+
const current = claimsFromJson(stored.claims);
|
|
600
|
+
return Object.keys(current).length ? { ...user, claims: current } : user;
|
|
532
601
|
};
|
|
533
602
|
const currentSession = async (c) => {
|
|
534
603
|
const session = await tokenClaims(c, SESSION_COOKIE, SESSION_TYP);
|
|
535
604
|
const user = session ? await checked(session) : null;
|
|
536
|
-
const upToDate = session && user &&
|
|
605
|
+
const upToDate = session && user && sameClaims(user.claims, session.claims) && Date.now() / 1e3 - session.iat <= sessionTtlSeconds / 2;
|
|
537
606
|
if (upToDate) return user;
|
|
538
607
|
const renewal = await tokenClaims(c, RENEWAL_COOKIE, RENEWAL_TYP);
|
|
539
608
|
const sameSignIn = session && user && renewal && renewal.sub === session.sub && renewal.sv === session.sv && renewal.email === session.email;
|
|
@@ -559,56 +628,82 @@ function createApp(config) {
|
|
|
559
628
|
app.post("/auth/account/delete", async (c) => {
|
|
560
629
|
const user = await currentSession(c);
|
|
561
630
|
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;
|
|
631
|
+
await users.deleteById(user.id);
|
|
564
632
|
clearSession(c);
|
|
565
|
-
await ctx.emit("user.deleted", { user
|
|
633
|
+
await ctx.emit("user.deleted", { user });
|
|
566
634
|
return c.body(null, 204);
|
|
567
635
|
});
|
|
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
|
-
|
|
636
|
+
const admin = async (c) => {
|
|
637
|
+
const user = await currentSession(c);
|
|
638
|
+
if (!user) return { answer: c.json({ error: "no_session" }, 401) };
|
|
639
|
+
const email = user.email ? normalizeEmail(user.email) : void 0;
|
|
640
|
+
if (!email || !isAdmin(user)) {
|
|
641
|
+
const message = `Only users with the role "${ADMIN_ROLE}" can manage claims and settings.`;
|
|
642
|
+
return { answer: c.json({ error: "forbidden", message }, 403) };
|
|
643
|
+
}
|
|
644
|
+
return { email };
|
|
645
|
+
};
|
|
646
|
+
const body = async (c) => {
|
|
647
|
+
const data = await c.req.json().catch(() => null);
|
|
648
|
+
return data && typeof data === "object" ? data : {};
|
|
649
|
+
};
|
|
650
|
+
const invalidEmail = (c) => c.json({ error: "invalid_email", message: "This is not a valid e-mail address." }, 400);
|
|
651
|
+
const noUser = (c) => c.json({ error: "user_not_found", message: "No user has this e-mail address." }, 404);
|
|
652
|
+
app.post("/auth/admin/claims/get", async (c) => {
|
|
653
|
+
const caller = await admin(c);
|
|
654
|
+
if ("answer" in caller) return caller.answer;
|
|
655
|
+
const data = await body(c);
|
|
656
|
+
if (!isValidEmail(data.email)) return invalidEmail(c);
|
|
657
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
658
|
+
if (!user) return noUser(c);
|
|
659
|
+
return c.json({ email: user.emailNormalized, userId: user.id, claims: claimsFromJson(user.claims) });
|
|
660
|
+
});
|
|
661
|
+
app.post("/auth/admin/claims/set", async (c) => {
|
|
662
|
+
const caller = await admin(c);
|
|
663
|
+
if ("answer" in caller) return caller.answer;
|
|
664
|
+
const data = await body(c);
|
|
665
|
+
if (!isValidEmail(data.email)) return invalidEmail(c);
|
|
666
|
+
const claims = parseClaims(data.claims);
|
|
667
|
+
if (!claims) {
|
|
668
|
+
return c.json(
|
|
669
|
+
{
|
|
670
|
+
error: "invalid_claims",
|
|
671
|
+
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 "_".'
|
|
672
|
+
},
|
|
673
|
+
400
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
677
|
+
if (!user) return noUser(c);
|
|
678
|
+
await users.updateUser(user.id, { claims: Object.keys(claims).length ? JSON.stringify(claims) : null });
|
|
679
|
+
const email = user.emailNormalized;
|
|
680
|
+
await ctx.emit("user.claims_changed", { userId: user.id, email, claims, by: caller.email });
|
|
681
|
+
return c.json({ email, userId: user.id, claims });
|
|
682
|
+
});
|
|
683
|
+
const settingsAnswer = (methods) => ({
|
|
684
|
+
methods: Object.fromEntries(
|
|
685
|
+
SIGN_IN_METHODS.map((method) => [method, { available: available(method), enabled: isOn(method, available, methods) }])
|
|
686
|
+
)
|
|
687
|
+
});
|
|
688
|
+
app.post("/auth/admin/settings/get", async (c) => {
|
|
689
|
+
const caller = await admin(c);
|
|
690
|
+
if ("answer" in caller) return caller.answer;
|
|
691
|
+
return c.json(settingsAnswer(await settings.methods()));
|
|
692
|
+
});
|
|
693
|
+
app.post("/auth/admin/settings/set", async (c) => {
|
|
694
|
+
const caller = await admin(c);
|
|
695
|
+
if ("answer" in caller) return caller.answer;
|
|
696
|
+
const data = await body(c);
|
|
697
|
+
const invalid = (message) => c.json({ error: "invalid_settings", message }, 400);
|
|
698
|
+
const changes = parseMethodSettings(data.methods);
|
|
699
|
+
if (!changes) return invalid(`methods must be an object with true or false for ${SIGN_IN_METHODS.join(" and/or ")}.`);
|
|
700
|
+
const methods = await settings.changeMethods((current) => {
|
|
701
|
+
const next = { ...current, ...changes };
|
|
702
|
+
return anyOn(available, next) ? next : null;
|
|
703
|
+
}, caller.email);
|
|
704
|
+
if (!methods) return invalid("At least one sign-in method must stay on.");
|
|
705
|
+
return c.json(settingsAnswer(methods));
|
|
706
|
+
});
|
|
612
707
|
return app;
|
|
613
708
|
}
|
|
614
709
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JWK, JWTVerifyGetKey } from 'jose';
|
|
2
|
-
import { S as StoreAdapter } from './schema-
|
|
3
|
-
import { W as WebhookSettings } from './webhook-
|
|
2
|
+
import { S as StoreAdapter } from './schema-3yhCc2jH.js';
|
|
3
|
+
import { W as WebhookSettings } from './webhook-DCDP6SaE.js';
|
|
4
4
|
|
|
5
5
|
interface MadauthConfig {
|
|
6
6
|
/** Public base URL of the server (MADAUTH_ISSUER). */
|
|
@@ -22,13 +22,14 @@ interface MadauthConfig {
|
|
|
22
22
|
/** Enables the server-side code flow (GOOGLE_CLIENT_SECRET). */
|
|
23
23
|
clientSecret?: string;
|
|
24
24
|
};
|
|
25
|
-
/**
|
|
25
|
+
/** Where the users, their accounts, claims and the settings are kept (DATABASE_URL or the `store` option). */
|
|
26
|
+
store: StoreAdapter;
|
|
27
|
+
/** E-mail & password sign-in; off unless the webhook sends the e-mails (`email.verify` and `email.reset`). */
|
|
26
28
|
password?: {
|
|
27
29
|
/** PASSWORD_MIN_LENGTH. */
|
|
28
30
|
minLength: number;
|
|
29
|
-
store: StoreAdapter;
|
|
30
31
|
};
|
|
31
|
-
/** Where madAuth sends e-mails, sign-up checks and events (WEBHOOK_URL)
|
|
32
|
+
/** Where madAuth sends e-mails, sign-up checks and events (WEBHOOK_URL). */
|
|
32
33
|
webhook?: WebhookSettings;
|
|
33
34
|
/** Test hook: resolves Google's signing keys. Defaults to Google's published JWKS. */
|
|
34
35
|
jwksResolver?: JWTVerifyGetKey;
|
|
@@ -42,7 +43,7 @@ declare const envVars: {
|
|
|
42
43
|
readonly ALLOWED_ORIGINS: true;
|
|
43
44
|
readonly GOOGLE_CLIENT_ID: false;
|
|
44
45
|
readonly GOOGLE_CLIENT_SECRET: false;
|
|
45
|
-
readonly DATABASE_URL:
|
|
46
|
+
readonly DATABASE_URL: true;
|
|
46
47
|
readonly PASSWORD_MIN_LENGTH: false;
|
|
47
48
|
readonly WEBHOOK_URL: false;
|
|
48
49
|
readonly WEBHOOK_SECRET: false;
|
package/dist/dynamodb.js
CHANGED
package/dist/entry/azure.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { E as EntryOptions } from '../config-
|
|
1
|
+
import { E as EntryOptions } from '../config-DEBYi8hb.js';
|
|
2
2
|
import 'jose';
|
|
3
|
-
import '../schema-
|
|
4
|
-
import '../webhook-
|
|
3
|
+
import '../schema-3yhCc2jH.js';
|
|
4
|
+
import '../webhook-DCDP6SaE.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Registers madAuth as an Azure Functions v4 HTTP function that handles every route. Pass `store` to use your
|
package/dist/entry/azure.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createApp
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-ZSBKNV5W.js";
|
|
4
|
+
import "../chunk-DCDTO5LG.js";
|
|
5
5
|
import {
|
|
6
6
|
loadConfig,
|
|
7
7
|
resolveEnv
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-ODITZLMM.js";
|
|
9
9
|
import "../chunk-JDQLSL36.js";
|
|
10
|
-
import "../chunk-
|
|
10
|
+
import "../chunk-NPNM26J3.js";
|
|
11
11
|
|
|
12
12
|
// src/entry/azure.ts
|
|
13
13
|
import { app } from "@azure/functions";
|
package/dist/entry/lambda.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { LambdaEvent, LambdaContext, handle } from 'hono/aws-lambda';
|
|
2
|
-
import { E as EntryOptions } from '../config-
|
|
2
|
+
import { E as EntryOptions } from '../config-DEBYi8hb.js';
|
|
3
3
|
import 'jose';
|
|
4
|
-
import '../schema-
|
|
5
|
-
import '../webhook-
|
|
4
|
+
import '../schema-3yhCc2jH.js';
|
|
5
|
+
import '../webhook-DCDP6SaE.js';
|
|
6
6
|
|
|
7
7
|
type LambdaHandler = (event: LambdaEvent, context: LambdaContext) => ReturnType<ReturnType<typeof handle>>;
|
|
8
8
|
/**
|
package/dist/entry/lambda.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createApp
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-ZSBKNV5W.js";
|
|
4
|
+
import "../chunk-DCDTO5LG.js";
|
|
5
5
|
import {
|
|
6
6
|
loadConfig,
|
|
7
7
|
resolveEnv
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-ODITZLMM.js";
|
|
9
9
|
import "../chunk-JDQLSL36.js";
|
|
10
|
-
import "../chunk-
|
|
10
|
+
import "../chunk-NPNM26J3.js";
|
|
11
11
|
|
|
12
12
|
// src/entry/lambda.ts
|
|
13
13
|
import { handle } from "hono/aws-lambda";
|
package/dist/entry/node.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { E as EntryOptions } from '../config-
|
|
1
|
+
import { E as EntryOptions } from '../config-DEBYi8hb.js';
|
|
2
2
|
import 'jose';
|
|
3
|
-
import '../schema-
|
|
4
|
-
import '../webhook-
|
|
3
|
+
import '../schema-3yhCc2jH.js';
|
|
4
|
+
import '../webhook-DCDP6SaE.js';
|
|
5
5
|
|
|
6
6
|
type StartOptions = EntryOptions;
|
|
7
7
|
/**
|