@dotpress/auth 1.3.1 → 2.0.0-alpha.0
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/LICENSE +1 -1
- package/README.md +355 -329
- package/dist/index.d.ts +679 -7
- package/dist/index.js +1226 -19
- package/dist/index.js.map +1 -0
- package/package.json +32 -46
- package/dist/__tests__/functional/cookies.test.d.ts +0 -1
- package/dist/__tests__/functional/cookies.test.js +0 -621
- package/dist/__tests__/functional/handlers.test.d.ts +0 -1
- package/dist/__tests__/functional/handlers.test.js +0 -239
- package/dist/__tests__/functional/middlewares.test.d.ts +0 -1
- package/dist/__tests__/functional/middlewares.test.js +0 -111
- package/dist/__tests__/unit/cookies.test.d.ts +0 -1
- package/dist/__tests__/unit/cookies.test.js +0 -466
- package/dist/__tests__/unit/tokens.test.d.ts +0 -1
- package/dist/__tests__/unit/tokens.test.js +0 -225
- package/dist/lib/cookies.d.ts +0 -42
- package/dist/lib/cookies.js +0 -136
- package/dist/lib/generators.d.ts +0 -1
- package/dist/lib/generators.js +0 -4
- package/dist/lib/handlers.d.ts +0 -2
- package/dist/lib/handlers.js +0 -263
- package/dist/lib/ip.d.ts +0 -2
- package/dist/lib/ip.js +0 -15
- package/dist/lib/middleware.d.ts +0 -2
- package/dist/lib/middleware.js +0 -23
- package/dist/lib/settings.d.ts +0 -3
- package/dist/lib/settings.js +0 -42
- package/dist/lib/tokens.d.ts +0 -38
- package/dist/lib/tokens.js +0 -112
- package/dist/types/types.d.ts +0 -107
- package/dist/types/types.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,22 +1,1229 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
// src/context.ts
|
|
2
|
+
import { declareContextKey } from "@dotpress/core";
|
|
3
|
+
declareContextKey("user", "authPlugin");
|
|
4
|
+
declareContextKey("auth", "authPlugin");
|
|
5
|
+
|
|
6
|
+
// src/auth.ts
|
|
7
|
+
import bcrypt from "bcryptjs";
|
|
8
|
+
import {
|
|
9
|
+
CompactSign,
|
|
10
|
+
SignJWT,
|
|
11
|
+
calculateJwkThumbprint,
|
|
12
|
+
compactVerify,
|
|
13
|
+
decodeProtectedHeader,
|
|
14
|
+
errors,
|
|
15
|
+
exportJWK,
|
|
16
|
+
importPKCS8,
|
|
17
|
+
importSPKI,
|
|
18
|
+
jwtVerify
|
|
19
|
+
} from "jose";
|
|
20
|
+
|
|
21
|
+
// src/crypto.ts
|
|
22
|
+
var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
23
|
+
var sha256 = async (value) => toHex(
|
|
24
|
+
new Uint8Array(
|
|
25
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// src/csrf.ts
|
|
30
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
31
|
+
var hostOf = (url) => {
|
|
32
|
+
if (url === void 0) return void 0;
|
|
33
|
+
try {
|
|
34
|
+
return new URL(url).host;
|
|
35
|
+
} catch {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var isCookieAuthenticationAllowed = async (req, isTrustedOrigin) => {
|
|
40
|
+
if (SAFE_METHODS.has(req.method)) return true;
|
|
41
|
+
const origin = req.header("origin");
|
|
42
|
+
if (origin !== void 0 && await isTrustedOrigin(origin)) return true;
|
|
43
|
+
const site = req.header("sec-fetch-site");
|
|
44
|
+
if (site !== void 0) return site === "same-origin" || site === "none";
|
|
45
|
+
if (origin === void 0) return true;
|
|
46
|
+
const originHost = hostOf(origin);
|
|
47
|
+
return originHost !== void 0 && originHost === (req.header("host") ?? hostOf(req.url));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/rate-limit.ts
|
|
51
|
+
var memoryRateLimitStore = (options = {}) => {
|
|
52
|
+
const maxKeys = options.maxKeys ?? 1e4;
|
|
53
|
+
if (!Number.isInteger(maxKeys) || maxKeys < 1) {
|
|
54
|
+
throw new RangeError("Invalid maxKeys: expected a positive integer.");
|
|
55
|
+
}
|
|
56
|
+
const counters = /* @__PURE__ */ new Map();
|
|
57
|
+
const makeRoom = (now) => {
|
|
58
|
+
if (counters.size < maxKeys) return;
|
|
59
|
+
for (const [key, state] of counters) {
|
|
60
|
+
if (state.resetAt.getTime() <= now) counters.delete(key);
|
|
61
|
+
}
|
|
62
|
+
for (const key of counters.keys()) {
|
|
63
|
+
if (counters.size < maxKeys) break;
|
|
64
|
+
counters.delete(key);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
async increment(key, windowSeconds) {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const current = counters.get(key);
|
|
71
|
+
if (current && current.resetAt.getTime() > now) {
|
|
72
|
+
current.count += 1;
|
|
73
|
+
return { ...current };
|
|
74
|
+
}
|
|
75
|
+
counters.delete(key);
|
|
76
|
+
makeRoom(now);
|
|
77
|
+
const state = { count: 1, resetAt: new Date(now + windowSeconds * 1e3) };
|
|
78
|
+
counters.set(key, state);
|
|
79
|
+
return { ...state };
|
|
80
|
+
},
|
|
81
|
+
async reset(key) {
|
|
82
|
+
counters.delete(key);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/auth.ts
|
|
88
|
+
var ALGORITHM = "RS256";
|
|
89
|
+
var ACCESS_TOKEN_TYPE = "at+jwt";
|
|
90
|
+
var DEFAULT_IDLE_TIMEOUT = 14 * 24 * 60 * 60;
|
|
91
|
+
var AuthError = class extends Error {
|
|
92
|
+
constructor(code, options) {
|
|
93
|
+
super(code, options);
|
|
94
|
+
this.code = code;
|
|
95
|
+
this.name = "AuthError";
|
|
96
|
+
}
|
|
97
|
+
code;
|
|
98
|
+
};
|
|
99
|
+
var DEFAULT_COOKIE_SETTINGS = {
|
|
100
|
+
enabled: false,
|
|
101
|
+
httpOnly: true,
|
|
102
|
+
secure: true,
|
|
103
|
+
sameSite: "lax",
|
|
104
|
+
path: "/",
|
|
105
|
+
refreshTokenPath: "/auth",
|
|
106
|
+
refreshTokenSameSite: "strict",
|
|
107
|
+
trustedOrigins: [],
|
|
108
|
+
accessTokenName: "dotpress_access_token",
|
|
109
|
+
refreshTokenName: "dotpress_refresh_token",
|
|
110
|
+
accessTokenInCookie: false,
|
|
111
|
+
domain: void 0
|
|
112
|
+
};
|
|
113
|
+
var defaultUserRoles = (user) => {
|
|
114
|
+
const { roles, role } = user;
|
|
115
|
+
if (Array.isArray(roles)) {
|
|
116
|
+
return roles.filter((value) => typeof value === "string");
|
|
117
|
+
}
|
|
118
|
+
return typeof role === "string" ? [role] : [];
|
|
119
|
+
};
|
|
120
|
+
var checkSession = (session, name) => {
|
|
121
|
+
for (const key of ["idleTimeout", "absoluteTimeout"]) {
|
|
122
|
+
const value = session?.[key];
|
|
123
|
+
if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) {
|
|
124
|
+
throw new RangeError(
|
|
125
|
+
`Invalid ${name}.${key}: expected a positive number of seconds.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var DEFAULT_LOGIN_LIMITS = {
|
|
131
|
+
usernameAndIp: { max: 5, window: 60 },
|
|
132
|
+
username: { max: 100, window: 60 * 60 }
|
|
133
|
+
};
|
|
134
|
+
var resolveLoginThrottle = (input) => {
|
|
135
|
+
if (input === false) return void 0;
|
|
136
|
+
const limit = (name) => {
|
|
137
|
+
const value = input?.limits?.[name];
|
|
138
|
+
if (value === false) return void 0;
|
|
139
|
+
const resolved = value ?? DEFAULT_LOGIN_LIMITS[name];
|
|
140
|
+
if (!Number.isInteger(resolved.max) || resolved.max < 1 || !Number.isFinite(resolved.window) || resolved.window <= 0) {
|
|
141
|
+
throw new RangeError(
|
|
142
|
+
`Invalid loginThrottle.limits.${name}: expected a positive integer max and a positive window, in seconds.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return { max: resolved.max, window: resolved.window };
|
|
146
|
+
};
|
|
147
|
+
const store = input?.store ?? memoryRateLimitStore();
|
|
148
|
+
if (typeof store.increment !== "function" || typeof store.reset !== "function") {
|
|
149
|
+
throw new TypeError(
|
|
150
|
+
"Invalid loginThrottle.store: expected increment() and reset() methods."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
store,
|
|
155
|
+
limits: {
|
|
156
|
+
usernameAndIp: limit("usernameAndIp"),
|
|
157
|
+
username: limit("username")
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
var resolveSettings = (settings) => {
|
|
162
|
+
if (!settings?.keys?.secretKey || !settings.keys.publicKey) {
|
|
163
|
+
throw new TypeError(
|
|
164
|
+
"createAuth() requires keys.secretKey and keys.publicKey."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (!settings.dataProvider) {
|
|
168
|
+
throw new TypeError("createAuth() requires a dataProvider.");
|
|
169
|
+
}
|
|
170
|
+
if (typeof settings.issuer !== "string" || settings.issuer === "") {
|
|
171
|
+
throw new TypeError(
|
|
172
|
+
"createAuth() requires an issuer (iss claim), such as the URL of the API."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (settings.audience !== void 0 && settings.audience === "") {
|
|
176
|
+
throw new TypeError("Invalid audience: expected a non-empty string.");
|
|
177
|
+
}
|
|
178
|
+
const clients = settings.clients;
|
|
179
|
+
if (clients && (!Array.isArray(clients.allowed) || clients.allowed.length === 0 || clients.allowed.some((id) => typeof id !== "string" || id === ""))) {
|
|
180
|
+
throw new TypeError(
|
|
181
|
+
"Invalid clients.allowed: expected a non-empty list of client identifiers."
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const rounds = settings.passwordHashRounds;
|
|
185
|
+
if (rounds !== void 0 && (!Number.isInteger(rounds) || rounds < 4 || rounds > 31)) {
|
|
186
|
+
throw new RangeError(
|
|
187
|
+
"Invalid passwordHashRounds: expected an integer between 4 and 31."
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if ("refreshTokenTTLInDays" in settings) {
|
|
191
|
+
throw new TypeError(
|
|
192
|
+
"refreshTokenTTLInDays is replaced by session.idleTimeout, in seconds."
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
for (const origin of settings.cookies?.trustedOrigins ?? []) {
|
|
196
|
+
let valid = false;
|
|
197
|
+
try {
|
|
198
|
+
valid = new URL(origin).origin === origin;
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
if (!valid) {
|
|
202
|
+
throw new TypeError(
|
|
203
|
+
`Invalid cookies.trustedOrigins: "${origin}" is not an origin (expected a scheme and a host, such as https://app.example.com, without path or trailing slash).`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
checkSession(settings.session, "session");
|
|
208
|
+
for (const [clientId, session] of Object.entries(clients?.sessions ?? {})) {
|
|
209
|
+
if (!clients?.allowed.includes(clientId)) {
|
|
210
|
+
throw new TypeError(
|
|
211
|
+
`Invalid clients.sessions: "${clientId}" is not in clients.allowed.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
checkSession(session, `clients.sessions.${clientId}`);
|
|
215
|
+
}
|
|
216
|
+
const reuseInterval = settings.refreshTokenReuseInterval;
|
|
217
|
+
if (reuseInterval !== void 0 && (!Number.isFinite(reuseInterval) || reuseInterval < 0)) {
|
|
218
|
+
throw new RangeError(
|
|
219
|
+
"Invalid refreshTokenReuseInterval: expected a non-negative number of seconds."
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
keys: settings.keys,
|
|
224
|
+
dataProvider: settings.dataProvider,
|
|
225
|
+
accessTokenTTL: settings.accessTokenTTL ?? 900,
|
|
226
|
+
session: {
|
|
227
|
+
idleTimeout: settings.session?.idleTimeout ?? DEFAULT_IDLE_TIMEOUT,
|
|
228
|
+
absoluteTimeout: settings.session?.absoluteTimeout
|
|
229
|
+
},
|
|
230
|
+
issuer: settings.issuer,
|
|
231
|
+
audience: settings.audience ?? settings.issuer,
|
|
232
|
+
cookies: resolveCookies(settings.cookies),
|
|
233
|
+
clients: settings.clients,
|
|
234
|
+
onLoginFailed: settings.onLoginFailed,
|
|
235
|
+
onLoginSuccess: settings.onLoginSuccess,
|
|
236
|
+
loginThrottle: resolveLoginThrottle(settings.loginThrottle),
|
|
237
|
+
beforeLogin: settings.beforeLogin,
|
|
238
|
+
passwordHashRounds: settings.passwordHashRounds ?? 12,
|
|
239
|
+
usernameClaim: settings.usernameClaim ?? false,
|
|
240
|
+
refreshTokenReuseInterval: settings.refreshTokenReuseInterval ?? 10,
|
|
241
|
+
onRefreshTokenReuse: settings.onRefreshTokenReuse,
|
|
242
|
+
getUserRoles: settings.getUserRoles ?? defaultUserRoles
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
var cookieName = (setting, name, base, attributes) => {
|
|
246
|
+
const { secure, domain, path } = attributes;
|
|
247
|
+
const hostAllowed = secure && domain === void 0 && path === "/";
|
|
248
|
+
if (name === void 0) {
|
|
249
|
+
if (!secure) return base;
|
|
250
|
+
return hostAllowed ? `__Host-${base}` : `__Secure-${base}`;
|
|
251
|
+
}
|
|
252
|
+
const lowerName = name.toLowerCase();
|
|
253
|
+
if (lowerName.startsWith("__host-") && !hostAllowed) {
|
|
254
|
+
throw new TypeError(
|
|
255
|
+
`Invalid cookies.${setting}: a __Host- cookie must be secure, without domain, and with path "/".`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (lowerName.startsWith("__secure-") && !secure) {
|
|
259
|
+
throw new TypeError(
|
|
260
|
+
`Invalid cookies.${setting}: a __Secure- cookie must be secure.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return name;
|
|
264
|
+
};
|
|
265
|
+
var resolveCookies = (settings = {}) => {
|
|
266
|
+
const merged = { ...DEFAULT_COOKIE_SETTINGS };
|
|
267
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
268
|
+
if (value !== void 0) Object.assign(merged, { [key]: value });
|
|
269
|
+
}
|
|
270
|
+
for (const key of ["path", "refreshTokenPath"]) {
|
|
271
|
+
if (!merged[key].startsWith("/")) {
|
|
272
|
+
throw new TypeError(
|
|
273
|
+
`Invalid cookies.${key}: expected a path starting with "/".`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const { secure, domain } = merged;
|
|
278
|
+
return {
|
|
279
|
+
...merged,
|
|
280
|
+
refreshTokenSameSite: settings.refreshTokenSameSite ?? (settings.sameSite === "none" ? "none" : "strict"),
|
|
281
|
+
accessTokenName: cookieName(
|
|
282
|
+
"accessTokenName",
|
|
283
|
+
settings.accessTokenName,
|
|
284
|
+
"dotpress_access_token",
|
|
285
|
+
{ secure, domain, path: merged.path }
|
|
286
|
+
),
|
|
287
|
+
refreshTokenName: cookieName(
|
|
288
|
+
"refreshTokenName",
|
|
289
|
+
settings.refreshTokenName,
|
|
290
|
+
"dotpress_refresh_token",
|
|
291
|
+
{ secure, domain, path: merged.refreshTokenPath }
|
|
292
|
+
)
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
var PKCS1_PUBLIC_KEY_ERROR = 'PKCS#1 keys ("BEGIN RSA PUBLIC KEY") are not supported, use SPKI ("BEGIN PUBLIC KEY"). Convert it with: openssl rsa -RSAPublicKey_in -in public.pem -pubout -out public.spki.pem';
|
|
296
|
+
var loadKey = async (kind, load) => {
|
|
297
|
+
try {
|
|
298
|
+
return await load();
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new TypeError(
|
|
301
|
+
`Invalid auth ${kind}: expected an RSA PEM key (${error.message}).`,
|
|
302
|
+
{ cause: error }
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
var loadPublicKey = async (kind, pem, kid) => {
|
|
307
|
+
if (pem.includes("BEGIN RSA PUBLIC KEY")) {
|
|
308
|
+
throw new TypeError(`Invalid auth ${kind}: ${PKCS1_PUBLIC_KEY_ERROR}`);
|
|
309
|
+
}
|
|
310
|
+
const key = await loadKey(
|
|
311
|
+
kind,
|
|
312
|
+
() => importSPKI(pem, ALGORITHM, { extractable: true })
|
|
313
|
+
);
|
|
314
|
+
const { kty, n, e } = await exportJWK(key);
|
|
315
|
+
const jwk = { kty, n, e };
|
|
316
|
+
return { kid: kid ?? await calculateJwkThumbprint(jwk), key, jwk };
|
|
317
|
+
};
|
|
318
|
+
var importKeys = async ({
|
|
319
|
+
secretKey,
|
|
320
|
+
publicKey,
|
|
321
|
+
kid,
|
|
322
|
+
additionalPublicKeys = []
|
|
323
|
+
}) => {
|
|
324
|
+
if (secretKey.includes("BEGIN RSA PRIVATE KEY")) {
|
|
325
|
+
throw new TypeError(
|
|
326
|
+
'Invalid auth secretKey: PKCS#1 keys ("BEGIN RSA PRIVATE KEY") are not supported, use PKCS#8 ("BEGIN PRIVATE KEY"). Convert it with: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const privateKey = await loadKey(
|
|
330
|
+
"secretKey",
|
|
331
|
+
() => importPKCS8(secretKey, ALGORITHM)
|
|
332
|
+
);
|
|
333
|
+
const active = await loadPublicKey("publicKey", publicKey, kid);
|
|
334
|
+
try {
|
|
335
|
+
const probe = await new CompactSign(new Uint8Array([1])).setProtectedHeader({ alg: ALGORITHM }).sign(privateKey);
|
|
336
|
+
await compactVerify(probe, active.key);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
throw new TypeError(
|
|
339
|
+
"Invalid auth keys: publicKey does not match secretKey.",
|
|
340
|
+
{ cause: error }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const verificationKeys = [active];
|
|
344
|
+
for (const [index, entry] of additionalPublicKeys.entries()) {
|
|
345
|
+
const { publicKey: pem, kid: entryKid } = typeof entry === "string" ? { publicKey: entry, kid: void 0 } : entry;
|
|
346
|
+
const loaded = await loadPublicKey(
|
|
347
|
+
`additionalPublicKeys[${index}]`,
|
|
348
|
+
pem,
|
|
349
|
+
entryKid
|
|
350
|
+
);
|
|
351
|
+
const existing = verificationKeys.find((item) => item.kid === loaded.kid);
|
|
352
|
+
if (!existing) {
|
|
353
|
+
verificationKeys.push(loaded);
|
|
354
|
+
} else if (existing.jwk.n !== loaded.jwk.n || existing.jwk.e !== loaded.jwk.e) {
|
|
355
|
+
throw new TypeError(
|
|
356
|
+
`Invalid auth keys: kid "${loaded.kid}" is used by several keys.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { privateKey, kid: active.kid, verificationKeys };
|
|
361
|
+
};
|
|
362
|
+
var verifyWithKeys = async (token, verificationKeys, verify) => {
|
|
363
|
+
const { kid } = decodeProtectedHeader(token);
|
|
364
|
+
const candidates = kid === void 0 ? verificationKeys : verificationKeys.filter((item) => item.kid === kid);
|
|
365
|
+
if (candidates.length === 0) {
|
|
366
|
+
throw new errors.JWKSNoMatchingKey(`Unknown key id: ${String(kid)}`);
|
|
367
|
+
}
|
|
368
|
+
let lastError;
|
|
369
|
+
for (const candidate of candidates) {
|
|
370
|
+
try {
|
|
371
|
+
return await verify(candidate.key);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (!(error instanceof errors.JWSSignatureVerificationFailed)) throw error;
|
|
374
|
+
lastError = error;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
throw lastError;
|
|
378
|
+
};
|
|
379
|
+
var BCRYPT_HASH = /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/;
|
|
380
|
+
var nowInSeconds = () => Math.floor(Date.now() / 1e3);
|
|
381
|
+
var toSession = (claims, roles) => {
|
|
382
|
+
if (typeof claims.sub !== "string" || claims.sub === "") {
|
|
383
|
+
throw new AuthError("INVALID_ACCESS_TOKEN");
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
userId: claims.sub,
|
|
387
|
+
username: typeof claims.username === "string" ? claims.username : void 0,
|
|
388
|
+
tokenId: typeof claims.jti === "string" ? claims.jti : "",
|
|
389
|
+
sessionId: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
390
|
+
clientId: typeof claims.client_id === "string" ? claims.client_id : void 0,
|
|
391
|
+
roles,
|
|
392
|
+
claims
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
var createAuth = (input) => {
|
|
396
|
+
const settings = resolveSettings(input);
|
|
397
|
+
let keys;
|
|
398
|
+
const getKeys = () => {
|
|
399
|
+
keys ??= importKeys(settings.keys).catch((error) => {
|
|
400
|
+
keys = void 0;
|
|
401
|
+
throw error;
|
|
402
|
+
});
|
|
403
|
+
return keys;
|
|
404
|
+
};
|
|
405
|
+
let dummyHash;
|
|
406
|
+
const getDummyHash = () => {
|
|
407
|
+
dummyHash ??= bcrypt.hash(
|
|
408
|
+
toHex(crypto.getRandomValues(new Uint8Array(16))),
|
|
409
|
+
settings.passwordHashRounds
|
|
410
|
+
);
|
|
411
|
+
return dummyHash;
|
|
412
|
+
};
|
|
413
|
+
const sessionPolicy = (clientId) => {
|
|
414
|
+
const override = clientId === void 0 ? void 0 : settings.clients?.sessions?.[clientId];
|
|
415
|
+
if (!override) return settings.session;
|
|
416
|
+
return {
|
|
417
|
+
idleTimeout: override.idleTimeout ?? settings.session.idleTimeout,
|
|
418
|
+
absoluteTimeout: "absoluteTimeout" in override ? override.absoluteTimeout : settings.session.absoluteTimeout
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
const sessionEnd = (authTime, clientId) => {
|
|
422
|
+
const { absoluteTimeout } = sessionPolicy(clientId);
|
|
423
|
+
return absoluteTimeout === void 0 ? void 0 : authTime + absoluteTimeout;
|
|
424
|
+
};
|
|
425
|
+
const signAccessToken = async (claims) => {
|
|
426
|
+
const { privateKey, kid } = await getKeys();
|
|
427
|
+
const jwtId = crypto.randomUUID();
|
|
428
|
+
const issuedAt = nowInSeconds();
|
|
429
|
+
const expiresAt = Math.min(
|
|
430
|
+
issuedAt + settings.accessTokenTTL,
|
|
431
|
+
sessionEnd(claims.auth_time, claims.client_id) ?? Infinity
|
|
432
|
+
);
|
|
433
|
+
const token = await new SignJWT({
|
|
434
|
+
// Personal data: written on demand only.
|
|
435
|
+
...settings.usernameClaim && claims.username && { username: claims.username },
|
|
436
|
+
auth_time: claims.auth_time,
|
|
437
|
+
...claims.sid !== void 0 && { sid: claims.sid },
|
|
438
|
+
...claims.client_id !== void 0 && { client_id: claims.client_id }
|
|
439
|
+
}).setProtectedHeader({ alg: ALGORITHM, typ: ACCESS_TOKEN_TYPE, kid }).setSubject(claims.sub).setIssuer(settings.issuer).setAudience(settings.audience).setJti(jwtId).setIssuedAt(issuedAt).setExpirationTime(expiresAt).sign(privateKey);
|
|
440
|
+
return { jwtId, accessToken: token, expiresAt: new Date(expiresAt * 1e3) };
|
|
441
|
+
};
|
|
442
|
+
const generateRefreshToken = async () => {
|
|
443
|
+
const token = toHex(crypto.getRandomValues(new Uint8Array(128)));
|
|
444
|
+
return { token, tokenHash: await sha256(token) };
|
|
445
|
+
};
|
|
446
|
+
const issueTokens = async (claims, familyId) => {
|
|
447
|
+
const access = await signAccessToken({ ...claims, sid: familyId });
|
|
448
|
+
const refreshToken = await generateRefreshToken();
|
|
449
|
+
const end = sessionEnd(claims.auth_time, claims.client_id);
|
|
450
|
+
const refreshTokenExpiresAt = new Date(
|
|
451
|
+
Math.min(
|
|
452
|
+
Date.now() + sessionPolicy(claims.client_id).idleTimeout * 1e3,
|
|
453
|
+
end === void 0 ? Infinity : end * 1e3
|
|
454
|
+
)
|
|
455
|
+
);
|
|
456
|
+
await settings.dataProvider.storeRefreshToken({
|
|
457
|
+
...refreshToken,
|
|
458
|
+
userId: claims.sub,
|
|
459
|
+
jwtId: access.jwtId,
|
|
460
|
+
familyId,
|
|
461
|
+
expiresAt: refreshTokenExpiresAt
|
|
462
|
+
});
|
|
463
|
+
return {
|
|
464
|
+
accessToken: access.accessToken,
|
|
465
|
+
refreshToken: refreshToken.token,
|
|
466
|
+
accessTokenExpiresAt: access.expiresAt,
|
|
467
|
+
refreshTokenExpiresAt,
|
|
468
|
+
sessionExpiresAt: end === void 0 ? void 0 : new Date(end * 1e3)
|
|
17
469
|
};
|
|
470
|
+
};
|
|
471
|
+
const checkReuse = async (stored) => {
|
|
472
|
+
const usedAt = stored.usedAt?.getTime();
|
|
473
|
+
if (usedAt !== void 0 && Date.now() - usedAt < settings.refreshTokenReuseInterval * 1e3) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const familyId = stored.familyId ?? void 0;
|
|
477
|
+
if (familyId) {
|
|
478
|
+
await settings.dataProvider.revokeTokenFamily(familyId);
|
|
479
|
+
} else {
|
|
480
|
+
await settings.dataProvider.revokeTokens(stored.userId);
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
await settings.onRefreshTokenReuse?.({
|
|
484
|
+
userId: stored.userId,
|
|
485
|
+
familyId,
|
|
486
|
+
jwtId: stored.jwtId
|
|
487
|
+
});
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
throw new AuthError("REFRESH_TOKEN_REUSED");
|
|
491
|
+
};
|
|
492
|
+
const isClientAccepted = (clientId) => {
|
|
493
|
+
const { clients } = settings;
|
|
494
|
+
if (!clients) return true;
|
|
495
|
+
return clientId === void 0 ? !clients.required : clients.allowed.includes(clientId);
|
|
496
|
+
};
|
|
497
|
+
const sessionClaims = ({ userId, username, clientId }) => {
|
|
498
|
+
if (!isClientAccepted(clientId)) {
|
|
499
|
+
throw new TypeError(
|
|
500
|
+
clientId === void 0 ? "A clientId is required by the clients setting." : `Unknown client "${clientId}": add it to clients.allowed.`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
sub: userId,
|
|
505
|
+
username,
|
|
506
|
+
auth_time: nowInSeconds(),
|
|
507
|
+
client_id: clientId
|
|
508
|
+
};
|
|
509
|
+
};
|
|
510
|
+
const generateAccessToken = async (subject) => signAccessToken(sessionClaims(subject));
|
|
511
|
+
let routesPath;
|
|
512
|
+
let cors;
|
|
513
|
+
const isTrustedOrigin = (req) => (origin) => {
|
|
514
|
+
if (input.cookies?.trustedOrigins !== void 0 || !cors?.credentials) {
|
|
515
|
+
return settings.cookies.trustedOrigins.includes(origin);
|
|
516
|
+
}
|
|
517
|
+
return cors.isOriginAllowed(req);
|
|
518
|
+
};
|
|
519
|
+
const bindRoutesPath = (path) => {
|
|
520
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
521
|
+
throw new TypeError(
|
|
522
|
+
`Invalid routesPath: expected a path starting with "/", received ${JSON.stringify(path)}.`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
if (routesPath !== void 0 && routesPath !== path) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`This auth service already serves routes under "${routesPath}": create one auth service per app to serve them under "${path}".`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
routesPath = path;
|
|
531
|
+
if (input.cookies?.refreshTokenPath === void 0) {
|
|
532
|
+
settings.cookies = resolveCookies({
|
|
533
|
+
...input.cookies,
|
|
534
|
+
refreshTokenPath: path
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const cookieOptions = () => {
|
|
539
|
+
const { httpOnly, secure, sameSite, path, domain } = settings.cookies;
|
|
540
|
+
return { httpOnly, secure, sameSite, path, domain };
|
|
541
|
+
};
|
|
542
|
+
return {
|
|
543
|
+
settings,
|
|
544
|
+
isCookieAuthenticationAllowed: (req) => isCookieAuthenticationAllowed(req, isTrustedOrigin(req)),
|
|
545
|
+
init: async (options = {}) => {
|
|
546
|
+
if (options.routesPath !== void 0) bindRoutesPath(options.routesPath);
|
|
547
|
+
if (options.cors !== void 0) cors = options.cors;
|
|
548
|
+
await Promise.all([getKeys(), getDummyHash()]);
|
|
549
|
+
},
|
|
550
|
+
generateAccessToken,
|
|
551
|
+
generateRefreshToken,
|
|
552
|
+
async generateTokensForUser(subject) {
|
|
553
|
+
return issueTokens(sessionClaims(subject), crypto.randomUUID());
|
|
554
|
+
},
|
|
555
|
+
async refreshTokens({ accessToken, refreshToken }) {
|
|
556
|
+
const { verificationKeys } = await getKeys();
|
|
557
|
+
let claims;
|
|
558
|
+
let legacy;
|
|
559
|
+
try {
|
|
560
|
+
const { payload, protectedHeader } = await verifyWithKeys(
|
|
561
|
+
accessToken,
|
|
562
|
+
verificationKeys,
|
|
563
|
+
(key) => compactVerify(accessToken, key, { algorithms: [ALGORITHM] })
|
|
564
|
+
);
|
|
565
|
+
claims = JSON.parse(new TextDecoder().decode(payload));
|
|
566
|
+
legacy = protectedHeader.typ !== ACCESS_TOKEN_TYPE;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
throw new AuthError("INVALID_REFRESH_TOKEN", { cause: error });
|
|
569
|
+
}
|
|
570
|
+
const userId = typeof claims.sub === "string" ? claims.sub : "";
|
|
571
|
+
const clientId = typeof claims.client_id === "string" ? claims.client_id : (
|
|
572
|
+
// V1 tokens carried the client in `aud`.
|
|
573
|
+
legacy && typeof claims.aud === "string" && settings.clients?.allowed.includes(claims.aud) ? claims.aud : void 0
|
|
574
|
+
);
|
|
575
|
+
const refreshTokenHash = await sha256(refreshToken);
|
|
576
|
+
const stored = await settings.dataProvider.findRefreshToken(refreshTokenHash);
|
|
577
|
+
if (!userId || !stored || stored.userId !== userId || stored.jwtId !== claims.jti || stored.revokedAt || // Client removed from the configuration: the session ends.
|
|
578
|
+
!isClientAccepted(clientId)) {
|
|
579
|
+
throw new AuthError("INVALID_REFRESH_TOKEN");
|
|
580
|
+
}
|
|
581
|
+
if (stored.isUsed) await checkReuse(stored);
|
|
582
|
+
const claimedAuthTime = typeof claims.auth_time === "number" ? claims.auth_time : void 0;
|
|
583
|
+
if (claimedAuthTime === void 0 && sessionPolicy(clientId).absoluteTimeout !== void 0) {
|
|
584
|
+
throw new AuthError("EXPIRED_REFRESH_TOKEN");
|
|
585
|
+
}
|
|
586
|
+
const authTime = claimedAuthTime ?? nowInSeconds();
|
|
587
|
+
const end = sessionEnd(authTime, clientId);
|
|
588
|
+
if (stored.expiresAt.getTime() <= Date.now() || end !== void 0 && end * 1e3 <= Date.now()) {
|
|
589
|
+
throw new AuthError("EXPIRED_REFRESH_TOKEN");
|
|
590
|
+
}
|
|
591
|
+
const user = await settings.dataProvider.findUserById(userId);
|
|
592
|
+
if (!user) {
|
|
593
|
+
if (stored.familyId) {
|
|
594
|
+
await settings.dataProvider.revokeTokenFamily(stored.familyId);
|
|
595
|
+
} else {
|
|
596
|
+
await settings.dataProvider.revokeTokens(userId);
|
|
597
|
+
}
|
|
598
|
+
throw new AuthError("INVALID_REFRESH_TOKEN");
|
|
599
|
+
}
|
|
600
|
+
const issued = await issueTokens(
|
|
601
|
+
{
|
|
602
|
+
sub: userId,
|
|
603
|
+
username: typeof claims.username === "string" ? claims.username : void 0,
|
|
604
|
+
auth_time: authTime,
|
|
605
|
+
client_id: clientId
|
|
606
|
+
},
|
|
607
|
+
// Tokens stored without family start one, derived from the token
|
|
608
|
+
// so that concurrent rotations land in the same family.
|
|
609
|
+
stored.familyId || stored.jwtId
|
|
610
|
+
);
|
|
611
|
+
if (!stored.isUsed) {
|
|
612
|
+
await settings.dataProvider.markRefreshTokenAsUsed(
|
|
613
|
+
refreshTokenHash,
|
|
614
|
+
/* @__PURE__ */ new Date()
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return { userId, user, ...issued };
|
|
618
|
+
},
|
|
619
|
+
async verifyAccessToken(accessToken) {
|
|
620
|
+
const { verificationKeys } = await getKeys();
|
|
621
|
+
try {
|
|
622
|
+
const { payload } = await verifyWithKeys(
|
|
623
|
+
accessToken,
|
|
624
|
+
verificationKeys,
|
|
625
|
+
(key) => jwtVerify(accessToken, key, {
|
|
626
|
+
algorithms: [ALGORITHM],
|
|
627
|
+
typ: ACCESS_TOKEN_TYPE,
|
|
628
|
+
issuer: settings.issuer,
|
|
629
|
+
audience: settings.audience
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
const session = toSession(payload, []);
|
|
633
|
+
if (!isClientAccepted(session.clientId)) {
|
|
634
|
+
throw new AuthError("INVALID_ACCESS_TOKEN");
|
|
635
|
+
}
|
|
636
|
+
const { isSessionRevoked } = settings.dataProvider;
|
|
637
|
+
if (session.sessionId !== void 0 && isSessionRevoked && await isSessionRevoked.call(
|
|
638
|
+
settings.dataProvider,
|
|
639
|
+
session.sessionId
|
|
640
|
+
)) {
|
|
641
|
+
throw new AuthError("SESSION_REVOKED");
|
|
642
|
+
}
|
|
643
|
+
return session;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (error instanceof AuthError) throw error;
|
|
646
|
+
throw new AuthError("INVALID_ACCESS_TOKEN", { cause: error });
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
async revokeSession(refreshToken, { scope = "current" } = {}) {
|
|
650
|
+
const { dataProvider } = settings;
|
|
651
|
+
const stored = await dataProvider.findRefreshToken(
|
|
652
|
+
await sha256(refreshToken)
|
|
653
|
+
);
|
|
654
|
+
if (!stored) return void 0;
|
|
655
|
+
const sessionId = stored.familyId || void 0;
|
|
656
|
+
if (scope === "all") {
|
|
657
|
+
if (stored.revokedAt || stored.isUsed || stored.expiresAt.getTime() <= Date.now()) {
|
|
658
|
+
return void 0;
|
|
659
|
+
}
|
|
660
|
+
await dataProvider.revokeTokens(stored.userId);
|
|
661
|
+
} else if (sessionId) {
|
|
662
|
+
await dataProvider.revokeTokenFamily(sessionId);
|
|
663
|
+
} else {
|
|
664
|
+
await dataProvider.revokeTokens(stored.userId);
|
|
665
|
+
}
|
|
666
|
+
return { userId: stored.userId, sessionId };
|
|
667
|
+
},
|
|
668
|
+
async getJwks() {
|
|
669
|
+
const { verificationKeys } = await getKeys();
|
|
670
|
+
return {
|
|
671
|
+
keys: verificationKeys.map(({ kid, jwk }) => ({
|
|
672
|
+
...jwk,
|
|
673
|
+
kid,
|
|
674
|
+
alg: ALGORITHM,
|
|
675
|
+
use: "sig"
|
|
676
|
+
}))
|
|
677
|
+
};
|
|
678
|
+
},
|
|
679
|
+
getSessionExpiresAt(session) {
|
|
680
|
+
const authTime = session.claims.auth_time;
|
|
681
|
+
if (typeof authTime !== "number") return void 0;
|
|
682
|
+
const end = sessionEnd(authTime, session.clientId);
|
|
683
|
+
return end === void 0 ? void 0 : new Date(end * 1e3);
|
|
684
|
+
},
|
|
685
|
+
getUserRoles: (user) => settings.getUserRoles(user),
|
|
686
|
+
setAuthCookies(res, tokens, expiresInSeconds) {
|
|
687
|
+
const cookies = settings.cookies;
|
|
688
|
+
if (!cookies.enabled) return;
|
|
689
|
+
const expiresAt = tokens.refreshTokenExpiresAt?.getTime();
|
|
690
|
+
const options = {
|
|
691
|
+
...cookieOptions(),
|
|
692
|
+
maxAge: expiresInSeconds ?? (expiresAt === void 0 ? settings.session.idleTimeout : Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3)))
|
|
693
|
+
};
|
|
694
|
+
res.setCookie(cookies.refreshTokenName, tokens.refreshToken, {
|
|
695
|
+
...options,
|
|
696
|
+
path: cookies.refreshTokenPath,
|
|
697
|
+
sameSite: cookies.refreshTokenSameSite
|
|
698
|
+
});
|
|
699
|
+
if (cookies.accessTokenInCookie) {
|
|
700
|
+
res.setCookie(cookies.accessTokenName, tokens.accessToken, options);
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
clearAuthCookies(res) {
|
|
704
|
+
const cookies = settings.cookies;
|
|
705
|
+
if (!cookies.enabled) return;
|
|
706
|
+
res.clearCookie(cookies.refreshTokenName, {
|
|
707
|
+
...cookieOptions(),
|
|
708
|
+
path: cookies.refreshTokenPath
|
|
709
|
+
});
|
|
710
|
+
if (cookies.accessTokenInCookie) {
|
|
711
|
+
res.clearCookie(cookies.accessTokenName, cookieOptions());
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
hashPassword: (password, saltRounds = settings.passwordHashRounds) => generatePasswordHash(password, saltRounds),
|
|
715
|
+
passwordNeedsRehash: (passwordHash) => BCRYPT_HASH.test(passwordHash) && bcrypt.getRounds(passwordHash) !== settings.passwordHashRounds,
|
|
716
|
+
// No length check: the password was verified, and a longer V1 password
|
|
717
|
+
// is truncated the same way.
|
|
718
|
+
rehashPassword: (password) => bcrypt.hash(password, settings.passwordHashRounds),
|
|
719
|
+
async verifyPassword(password, passwordHash) {
|
|
720
|
+
if (typeof passwordHash === "string" && BCRYPT_HASH.test(passwordHash)) {
|
|
721
|
+
return bcrypt.compare(password, passwordHash);
|
|
722
|
+
}
|
|
723
|
+
await bcrypt.compare(password, await getDummyHash());
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
};
|
|
728
|
+
var MAX_PASSWORD_BYTES = 72;
|
|
729
|
+
var isPasswordTooLong = (password) => bcrypt.truncates(password);
|
|
730
|
+
var generatePasswordHash = async (password, saltRounds = 12) => {
|
|
731
|
+
if (isPasswordTooLong(password)) {
|
|
732
|
+
throw new RangeError(
|
|
733
|
+
`Password too long: bcrypt reads ${MAX_PASSWORD_BYTES} bytes at most (UTF-8). Check it with isPasswordTooLong().`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return bcrypt.hash(password, saltRounds);
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/middlewares.ts
|
|
740
|
+
import {
|
|
741
|
+
defineMiddleware,
|
|
742
|
+
forbiddenError,
|
|
743
|
+
unauthorizedError
|
|
744
|
+
} from "@dotpress/core";
|
|
745
|
+
var SECURED = { security: ["bearerAuth"] };
|
|
746
|
+
var requireAuth = defineMiddleware(
|
|
747
|
+
(ctx) => ctx.user && ctx.auth ? void 0 : unauthorizedError(),
|
|
748
|
+
SECURED
|
|
749
|
+
);
|
|
750
|
+
var requireRole = (...roles) => {
|
|
751
|
+
if (roles.length === 0) {
|
|
752
|
+
throw new TypeError("requireRole() expects at least one role.");
|
|
753
|
+
}
|
|
754
|
+
return defineMiddleware((ctx) => {
|
|
755
|
+
if (!ctx.user || !ctx.auth) return unauthorizedError();
|
|
756
|
+
const granted = ctx.auth.roles;
|
|
757
|
+
return roles.some((role) => granted.includes(role)) ? void 0 : forbiddenError();
|
|
758
|
+
}, SECURED);
|
|
759
|
+
};
|
|
760
|
+
var requireClient = (...clientIds) => {
|
|
761
|
+
if (clientIds.length === 0) {
|
|
762
|
+
throw new TypeError("requireClient() expects at least one client.");
|
|
763
|
+
}
|
|
764
|
+
return defineMiddleware((ctx) => {
|
|
765
|
+
if (!ctx.user || !ctx.auth) return unauthorizedError();
|
|
766
|
+
const { clientId } = ctx.auth;
|
|
767
|
+
return clientId !== void 0 && clientIds.includes(clientId) ? void 0 : forbiddenError();
|
|
768
|
+
}, SECURED);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// src/plugin.ts
|
|
772
|
+
import {
|
|
773
|
+
definePlugin,
|
|
774
|
+
forbiddenError as forbiddenError2,
|
|
775
|
+
internalError,
|
|
776
|
+
tooManyRequestsError,
|
|
777
|
+
unauthorizedError as unauthorizedError2
|
|
778
|
+
} from "@dotpress/core";
|
|
779
|
+
var AUTH_SECURITY_SCHEMES = {
|
|
780
|
+
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" }
|
|
781
|
+
};
|
|
782
|
+
var LOCAL_IPS = /* @__PURE__ */ new Set([
|
|
783
|
+
"127.0.0.1",
|
|
784
|
+
"localhost",
|
|
785
|
+
"::1",
|
|
786
|
+
"::ffff",
|
|
787
|
+
"::ffff:127.0.0.1"
|
|
788
|
+
]);
|
|
789
|
+
var clientIp = (req) => req.ip && !LOCAL_IPS.has(req.ip) ? req.ip : "";
|
|
790
|
+
var BEARER = /^bearer +(\S+) *$/i;
|
|
791
|
+
var bodyField = (body, key) => {
|
|
792
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
793
|
+
const value = body[key];
|
|
794
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
795
|
+
};
|
|
796
|
+
var findAccessToken = (req, cookies) => {
|
|
797
|
+
const bearer = BEARER.exec(req.header("authorization") ?? "");
|
|
798
|
+
if (bearer) return { token: bearer[1], source: "header" };
|
|
799
|
+
if (cookies.enabled && cookies.accessTokenInCookie) {
|
|
800
|
+
const cookie = req.cookies[cookies.accessTokenName];
|
|
801
|
+
if (cookie) return { token: cookie, source: "cookie" };
|
|
802
|
+
}
|
|
803
|
+
return void 0;
|
|
804
|
+
};
|
|
805
|
+
var extractAccessToken = (req, cookies) => findAccessToken(req, cookies)?.token;
|
|
806
|
+
var loginThrottleKeys = async (throttle, username, ipAddress) => {
|
|
807
|
+
const normalized = username.trim().toLowerCase();
|
|
808
|
+
const { usernameAndIp, username: perUsername } = throttle.limits;
|
|
809
|
+
return {
|
|
810
|
+
usernameAndIp: usernameAndIp && {
|
|
811
|
+
key: `dotpress:login:username-ip:${await sha256(`${normalized}
|
|
812
|
+
${ipAddress}`)}`,
|
|
813
|
+
limit: usernameAndIp
|
|
814
|
+
},
|
|
815
|
+
username: perUsername && {
|
|
816
|
+
key: `dotpress:login:username:${await sha256(normalized)}`,
|
|
817
|
+
limit: perUsername
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
};
|
|
821
|
+
var crossOriginError = () => forbiddenError2("CROSS_ORIGIN_REQUEST");
|
|
822
|
+
var extractTokens = (req, body, cookies) => {
|
|
823
|
+
const cookieRefreshToken = req.cookies[cookies.refreshTokenName];
|
|
824
|
+
if (cookieRefreshToken) {
|
|
825
|
+
const accessToken2 = cookies.accessTokenInCookie ? req.cookies[cookies.accessTokenName] : bodyField(body, "accessToken");
|
|
826
|
+
return accessToken2 ? { accessToken: accessToken2, refreshToken: cookieRefreshToken, mode: "cookie" } : void 0;
|
|
827
|
+
}
|
|
828
|
+
const accessToken = bodyField(body, "accessToken");
|
|
829
|
+
const refreshToken = bodyField(body, "refreshToken");
|
|
830
|
+
return accessToken && refreshToken ? { accessToken, refreshToken, mode: "body" } : void 0;
|
|
831
|
+
};
|
|
832
|
+
var refreshErrorResponse = (error, fallback) => {
|
|
833
|
+
if (error instanceof AuthError) {
|
|
834
|
+
if (error.code === "EXPIRED_REFRESH_TOKEN") {
|
|
835
|
+
return unauthorizedError2("EXPIRED_REFRESH_TOKEN");
|
|
836
|
+
}
|
|
837
|
+
return unauthorizedError2("INVALID_REFRESH_TOKEN");
|
|
838
|
+
}
|
|
839
|
+
return internalError(fallback);
|
|
840
|
+
};
|
|
841
|
+
var registerRoutes = (group, auth) => {
|
|
842
|
+
const { settings } = auth;
|
|
843
|
+
const { dataProvider, cookies } = settings;
|
|
844
|
+
const expiration = (accessTokenExpiresAt, sessionExpiresAt) => ({
|
|
845
|
+
expiresIn: Math.max(
|
|
846
|
+
0,
|
|
847
|
+
Math.ceil((accessTokenExpiresAt.getTime() - Date.now()) / 1e3)
|
|
848
|
+
),
|
|
849
|
+
expiresAt: accessTokenExpiresAt.toISOString(),
|
|
850
|
+
...sessionExpiresAt && {
|
|
851
|
+
sessionExpiresAt: sessionExpiresAt.toISOString()
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
const issuedExpiration = (tokens) => expiration(tokens.accessTokenExpiresAt, tokens.sessionExpiresAt);
|
|
855
|
+
const bodyTokens = (tokens, inCookies) => {
|
|
856
|
+
if (!inCookies) {
|
|
857
|
+
return {
|
|
858
|
+
accessToken: tokens.accessToken,
|
|
859
|
+
refreshToken: tokens.refreshToken
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return cookies.accessTokenInCookie ? {} : { accessToken: tokens.accessToken };
|
|
863
|
+
};
|
|
864
|
+
const { clients } = settings;
|
|
865
|
+
const loginFailed = async (event) => {
|
|
866
|
+
await settings.onLoginFailed?.(event);
|
|
867
|
+
return unauthorizedError2("INVALID_LOGIN");
|
|
868
|
+
};
|
|
869
|
+
const meta = { tags: ["auth"] };
|
|
870
|
+
const crossOrigin = "Cross-origin request in cookie mode (`CROSS_ORIGIN_REQUEST`, see cookies.trustedOrigins).";
|
|
871
|
+
group.defineRoute({
|
|
872
|
+
method: "post",
|
|
873
|
+
path: "/login",
|
|
874
|
+
meta: {
|
|
875
|
+
...meta,
|
|
876
|
+
summary: "Log in with a username and a password",
|
|
877
|
+
responses: {
|
|
878
|
+
401: "Invalid credentials (`INVALID_LOGIN`).",
|
|
879
|
+
429: "Too many failed attempts (`TOO_MANY_LOGIN_ATTEMPTS`), with a `Retry-After` header."
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
schema: (z) => ({
|
|
883
|
+
body: z.object({
|
|
884
|
+
username: z.string(),
|
|
885
|
+
password: z.string(),
|
|
886
|
+
// One of the allowed clients when the clients setting is defined,
|
|
887
|
+
// otherwise any string, only reported in the login events.
|
|
888
|
+
clientId: !clients ? z.string().optional() : clients.required ? z.enum(clients.allowed) : z.enum(clients.allowed).optional(),
|
|
889
|
+
mode: z.enum(["body", "cookie"]).optional()
|
|
890
|
+
}),
|
|
891
|
+
response: z.object({
|
|
892
|
+
userId: z.string(),
|
|
893
|
+
accessToken: z.string().optional(),
|
|
894
|
+
refreshToken: z.string().optional(),
|
|
895
|
+
expiresIn: z.number(),
|
|
896
|
+
expiresAt: z.string(),
|
|
897
|
+
sessionExpiresAt: z.string().optional()
|
|
898
|
+
})
|
|
899
|
+
}),
|
|
900
|
+
handler: async ({ req, res, logger }) => {
|
|
901
|
+
const { username, password, clientId, mode } = req.body;
|
|
902
|
+
const event = {
|
|
903
|
+
username,
|
|
904
|
+
clientId: clientId ?? "",
|
|
905
|
+
ipAddress: clientIp(req)
|
|
906
|
+
};
|
|
907
|
+
const throttle = settings.loginThrottle;
|
|
908
|
+
const counters = throttle && await loginThrottleKeys(throttle, username, event.ipAddress);
|
|
909
|
+
if (throttle && counters) {
|
|
910
|
+
let retryAfter = 0;
|
|
911
|
+
for (const counter of [counters.usernameAndIp, counters.username]) {
|
|
912
|
+
if (!counter) continue;
|
|
913
|
+
const state = await throttle.store.increment(
|
|
914
|
+
counter.key,
|
|
915
|
+
counter.limit.window
|
|
916
|
+
);
|
|
917
|
+
if (state.count > counter.limit.max) {
|
|
918
|
+
retryAfter = Math.max(
|
|
919
|
+
retryAfter,
|
|
920
|
+
Math.ceil((state.resetAt.getTime() - Date.now()) / 1e3),
|
|
921
|
+
1
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (retryAfter > 0) {
|
|
926
|
+
await settings.onLoginFailed?.({
|
|
927
|
+
...event,
|
|
928
|
+
errorType: "too_many_attempts"
|
|
929
|
+
});
|
|
930
|
+
res.setHeader("retry-after", String(retryAfter));
|
|
931
|
+
return tooManyRequestsError("TOO_MANY_LOGIN_ATTEMPTS");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const rejected = await settings.beforeLogin?.({ ...event, req });
|
|
935
|
+
if (rejected) return rejected;
|
|
936
|
+
const user = await dataProvider.findUserIdentifiers(username);
|
|
937
|
+
const passwordValid = await auth.verifyPassword(
|
|
938
|
+
password,
|
|
939
|
+
user?.passwordHash
|
|
940
|
+
);
|
|
941
|
+
if (!user) {
|
|
942
|
+
return loginFailed({ ...event, errorType: "invalid_username" });
|
|
943
|
+
}
|
|
944
|
+
if (!passwordValid) {
|
|
945
|
+
return loginFailed({ ...event, errorType: "invalid_password" });
|
|
946
|
+
}
|
|
947
|
+
if (clientId !== void 0 && clients?.isUserRoleAllowed && !clients.isUserRoleAllowed(user.role ?? "", clientId)) {
|
|
948
|
+
return loginFailed({ ...event, errorType: "invalid_role" });
|
|
949
|
+
}
|
|
950
|
+
const tokens = await auth.generateTokensForUser({
|
|
951
|
+
userId: user.userId,
|
|
952
|
+
username,
|
|
953
|
+
// Without clients setting, the clientId is only reported in the
|
|
954
|
+
// login events: an unchecked value is not written in the token.
|
|
955
|
+
clientId: clients ? clientId : void 0
|
|
956
|
+
});
|
|
957
|
+
if (throttle && counters?.usernameAndIp) {
|
|
958
|
+
await throttle.store.reset(counters.usernameAndIp.key);
|
|
959
|
+
}
|
|
960
|
+
await settings.onLoginSuccess?.({ ...event, userId: user.userId });
|
|
961
|
+
if (dataProvider.updatePasswordHash && user.passwordHash && auth.passwordNeedsRehash(user.passwordHash)) {
|
|
962
|
+
try {
|
|
963
|
+
await dataProvider.updatePasswordHash(
|
|
964
|
+
user.userId,
|
|
965
|
+
await auth.rehashPassword(password)
|
|
966
|
+
);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
logger.warn(
|
|
969
|
+
error instanceof Error ? error : { error },
|
|
970
|
+
"Cannot update the password hash"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const inCookies = mode === "cookie" && cookies.enabled;
|
|
975
|
+
if (inCookies) auth.setAuthCookies(res, tokens);
|
|
976
|
+
return {
|
|
977
|
+
userId: user.userId,
|
|
978
|
+
...bodyTokens(tokens, inCookies),
|
|
979
|
+
...issuedExpiration(tokens)
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
group.defineRoute({
|
|
984
|
+
method: "post",
|
|
985
|
+
path: "/refresh",
|
|
986
|
+
meta: {
|
|
987
|
+
...meta,
|
|
988
|
+
summary: "Rotate a token pair",
|
|
989
|
+
responses: {
|
|
990
|
+
401: "Missing, invalid, expired or reused tokens.",
|
|
991
|
+
403: crossOrigin
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
schema: (z) => ({
|
|
995
|
+
body: z.object({
|
|
996
|
+
accessToken: z.string().optional(),
|
|
997
|
+
refreshToken: z.string().optional()
|
|
998
|
+
}).optional(),
|
|
999
|
+
response: z.object({
|
|
1000
|
+
userId: z.string(),
|
|
1001
|
+
accessToken: z.string().optional(),
|
|
1002
|
+
refreshToken: z.string().optional(),
|
|
1003
|
+
expiresIn: z.number(),
|
|
1004
|
+
expiresAt: z.string(),
|
|
1005
|
+
sessionExpiresAt: z.string().optional()
|
|
1006
|
+
})
|
|
1007
|
+
}),
|
|
1008
|
+
handler: async ({ req, res }) => {
|
|
1009
|
+
const extracted = extractTokens(req, req.body, cookies);
|
|
1010
|
+
if (!extracted) return unauthorizedError2("NO_TOKENS_PROVIDED");
|
|
1011
|
+
if (extracted.mode === "cookie" && !await auth.isCookieAuthenticationAllowed(req)) {
|
|
1012
|
+
return crossOriginError();
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
const { userId, ...tokens } = await auth.refreshTokens(extracted);
|
|
1016
|
+
const inCookies = extracted.mode === "cookie" && cookies.enabled;
|
|
1017
|
+
if (inCookies) auth.setAuthCookies(res, tokens);
|
|
1018
|
+
return {
|
|
1019
|
+
userId,
|
|
1020
|
+
...bodyTokens(tokens, inCookies),
|
|
1021
|
+
...issuedExpiration(tokens)
|
|
1022
|
+
};
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
return refreshErrorResponse(error, "Can not refresh tokens.");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
group.defineRoute({
|
|
1029
|
+
path: "/me",
|
|
1030
|
+
meta: { ...meta, summary: "Authenticated user" },
|
|
1031
|
+
middlewares: [requireAuth],
|
|
1032
|
+
handler: ({ user }) => user
|
|
1033
|
+
});
|
|
1034
|
+
group.defineRoute({
|
|
1035
|
+
method: "post",
|
|
1036
|
+
path: "/restore",
|
|
1037
|
+
meta: {
|
|
1038
|
+
...meta,
|
|
1039
|
+
summary: "Restore a session from the auth cookies",
|
|
1040
|
+
responses: {
|
|
1041
|
+
401: "Missing or invalid auth cookies.",
|
|
1042
|
+
403: crossOrigin
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
schema: (z) => ({
|
|
1046
|
+
response: z.object({
|
|
1047
|
+
userId: z.string(),
|
|
1048
|
+
accessToken: z.string().optional(),
|
|
1049
|
+
refreshToken: z.string().optional(),
|
|
1050
|
+
expiresIn: z.number(),
|
|
1051
|
+
expiresAt: z.string(),
|
|
1052
|
+
sessionExpiresAt: z.string().optional(),
|
|
1053
|
+
user: z.unknown()
|
|
1054
|
+
})
|
|
1055
|
+
}),
|
|
1056
|
+
handler: async ({ req, res }) => {
|
|
1057
|
+
if (!cookies.enabled) return unauthorizedError2("COOKIES_NOT_ENABLED");
|
|
1058
|
+
if (!cookies.accessTokenInCookie) {
|
|
1059
|
+
return unauthorizedError2("ACCESS_TOKEN_NOT_IN_COOKIE");
|
|
1060
|
+
}
|
|
1061
|
+
const refreshToken = req.cookies[cookies.refreshTokenName];
|
|
1062
|
+
const accessToken = req.cookies[cookies.accessTokenName];
|
|
1063
|
+
if (!refreshToken) return unauthorizedError2("NO_REFRESH_TOKEN");
|
|
1064
|
+
if (!accessToken) return unauthorizedError2("NO_ACCESS_TOKEN");
|
|
1065
|
+
if (!await auth.isCookieAuthenticationAllowed(req)) {
|
|
1066
|
+
return crossOriginError();
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const session = await auth.verifyAccessToken(accessToken);
|
|
1070
|
+
const { userId } = session;
|
|
1071
|
+
const user = await dataProvider.findUserById(userId);
|
|
1072
|
+
if (user) {
|
|
1073
|
+
return {
|
|
1074
|
+
userId,
|
|
1075
|
+
...expiration(
|
|
1076
|
+
new Date(Number(session.claims.exp) * 1e3),
|
|
1077
|
+
auth.getSessionExpiresAt(session)
|
|
1078
|
+
),
|
|
1079
|
+
user
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
} catch {
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const { userId, user, ...tokens } = await auth.refreshTokens({
|
|
1086
|
+
accessToken,
|
|
1087
|
+
refreshToken
|
|
1088
|
+
});
|
|
1089
|
+
auth.setAuthCookies(res, tokens);
|
|
1090
|
+
return { userId, ...issuedExpiration(tokens), user };
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
return refreshErrorResponse(error, "Cannot restore session.");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
group.defineRoute({
|
|
1097
|
+
method: "post",
|
|
1098
|
+
path: "/logout",
|
|
1099
|
+
meta: {
|
|
1100
|
+
...meta,
|
|
1101
|
+
summary: "Revoke the current session, or every session of the user",
|
|
1102
|
+
responses: {
|
|
1103
|
+
401: '`scope: "all"` without a valid refresh token or access token.',
|
|
1104
|
+
403: crossOrigin
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
schema: (z) => ({
|
|
1108
|
+
body: z.object({
|
|
1109
|
+
refreshToken: z.string().optional(),
|
|
1110
|
+
scope: z.enum(["current", "all"]).optional()
|
|
1111
|
+
}).optional()
|
|
1112
|
+
}),
|
|
1113
|
+
// The session is proven by the refresh token (cookie, then body), or
|
|
1114
|
+
// by the access token: an expired access token does not prevent a
|
|
1115
|
+
// logout.
|
|
1116
|
+
handler: async ({ req, res, auth: session }) => {
|
|
1117
|
+
const scope = req.body?.scope ?? "current";
|
|
1118
|
+
const cookieRefreshToken = cookies.enabled ? req.cookies[cookies.refreshTokenName] : void 0;
|
|
1119
|
+
if (cookieRefreshToken && !await auth.isCookieAuthenticationAllowed(req)) {
|
|
1120
|
+
return crossOriginError();
|
|
1121
|
+
}
|
|
1122
|
+
const refreshToken = cookieRefreshToken || req.body?.refreshToken;
|
|
1123
|
+
const revoked = refreshToken ? await auth.revokeSession(refreshToken, { scope }) : void 0;
|
|
1124
|
+
if (!revoked && session) {
|
|
1125
|
+
if (scope === "all") {
|
|
1126
|
+
await dataProvider.revokeTokens(session.userId);
|
|
1127
|
+
} else if (session.sessionId) {
|
|
1128
|
+
await dataProvider.revokeTokenFamily(session.sessionId);
|
|
1129
|
+
}
|
|
1130
|
+
} else if (!revoked && scope === "all") {
|
|
1131
|
+
return unauthorizedError2();
|
|
1132
|
+
}
|
|
1133
|
+
auth.clearAuthCookies(res);
|
|
1134
|
+
return { success: true };
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
};
|
|
1138
|
+
var normalizePrefix = (prefix) => `/${prefix.split("/").filter(Boolean).join("/")}`;
|
|
1139
|
+
var authPlugin = (auth, options = {}) => {
|
|
1140
|
+
const prefix = options.prefix ?? "/auth";
|
|
1141
|
+
return definePlugin({
|
|
1142
|
+
name: "auth",
|
|
1143
|
+
auth,
|
|
1144
|
+
securitySchemes: AUTH_SECURITY_SCHEMES,
|
|
1145
|
+
async setup(api) {
|
|
1146
|
+
await auth.init({
|
|
1147
|
+
...options.routes === false ? {} : { routesPath: api.publicPath(normalizePrefix(prefix)) },
|
|
1148
|
+
...api.cors ? { cors: api.cors } : {}
|
|
1149
|
+
});
|
|
1150
|
+
api.decorateContext("user", () => void 0);
|
|
1151
|
+
api.decorateContext("auth", () => void 0);
|
|
1152
|
+
const rejectedTokens = /* @__PURE__ */ new WeakSet();
|
|
1153
|
+
const authenticate = async (ctx) => {
|
|
1154
|
+
const { cookies } = auth.settings;
|
|
1155
|
+
const found = findAccessToken(ctx.req, cookies);
|
|
1156
|
+
if (!found) return;
|
|
1157
|
+
const { token } = found;
|
|
1158
|
+
if (found.source === "cookie" && !await auth.isCookieAuthenticationAllowed(ctx.req)) {
|
|
1159
|
+
ctx.logger.warn(
|
|
1160
|
+
{ origin: ctx.req.header("origin") },
|
|
1161
|
+
"Access token cookie ignored: cross-origin request (see cookies.trustedOrigins)"
|
|
1162
|
+
);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1166
|
+
const session = await auth.verifyAccessToken(token);
|
|
1167
|
+
const user = await auth.settings.dataProvider.findUserById(
|
|
1168
|
+
session.userId
|
|
1169
|
+
);
|
|
1170
|
+
if (!user) {
|
|
1171
|
+
if (found.source === "header") rejectedTokens.add(ctx);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
ctx.user = user;
|
|
1175
|
+
ctx.auth = { ...session, roles: auth.getUserRoles(user) };
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
if (found.source === "header") rejectedTokens.add(ctx);
|
|
1178
|
+
ctx.logger.warn(
|
|
1179
|
+
error instanceof Error ? error : { error },
|
|
1180
|
+
"Invalid access token"
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
api.addGlobalMiddleware(authenticate);
|
|
1185
|
+
api.hooks.onResponse(({ response, ctx }) => {
|
|
1186
|
+
if (response.status !== 401) return;
|
|
1187
|
+
const has = Object.keys(response.headers).some(
|
|
1188
|
+
(name) => name.toLowerCase() === "www-authenticate"
|
|
1189
|
+
);
|
|
1190
|
+
if (has) return;
|
|
1191
|
+
return {
|
|
1192
|
+
...response,
|
|
1193
|
+
headers: {
|
|
1194
|
+
...response.headers,
|
|
1195
|
+
"www-authenticate": ctx && rejectedTokens.has(ctx) ? 'Bearer error="invalid_token"' : "Bearer"
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
});
|
|
1199
|
+
if (options.jwks) {
|
|
1200
|
+
api.addRoute({
|
|
1201
|
+
path: "/.well-known/jwks.json",
|
|
1202
|
+
meta: { tags: ["auth"], summary: "Public keys of the access tokens" },
|
|
1203
|
+
handler: async ({ res }) => {
|
|
1204
|
+
res.setHeader("cache-control", "public, max-age=300");
|
|
1205
|
+
return auth.getJwks();
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (options.routes !== false) {
|
|
1210
|
+
registerRoutes(api.addRouteGroup(prefix), auth);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
18
1214
|
};
|
|
19
|
-
export
|
|
20
|
-
|
|
1215
|
+
export {
|
|
1216
|
+
AUTH_SECURITY_SCHEMES,
|
|
1217
|
+
AuthError,
|
|
1218
|
+
MAX_PASSWORD_BYTES,
|
|
1219
|
+
authPlugin,
|
|
1220
|
+
createAuth,
|
|
1221
|
+
extractAccessToken,
|
|
1222
|
+
generatePasswordHash,
|
|
1223
|
+
isPasswordTooLong,
|
|
1224
|
+
memoryRateLimitStore,
|
|
1225
|
+
requireAuth,
|
|
1226
|
+
requireClient,
|
|
1227
|
+
requireRole
|
|
21
1228
|
};
|
|
22
|
-
|
|
1229
|
+
//# sourceMappingURL=index.js.map
|