@cogenta/auth 0.1.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/dist/audit.d.ts +17 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +113 -0
- package/dist/audit.js.map +1 -0
- package/dist/credentials.d.ts +33 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +95 -0
- package/dist/credentials.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/login.d.ts +66 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +300 -0
- package/dist/login.js.map +1 -0
- package/dist/mfa.d.ts +4 -0
- package/dist/mfa.d.ts.map +1 -0
- package/dist/mfa.js +28 -0
- package/dist/mfa.js.map +1 -0
- package/dist/password.d.ts +11 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +108 -0
- package/dist/password.js.map +1 -0
- package/dist/rate-limit.d.ts +10 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +47 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/sessions.d.ts +20 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +82 -0
- package/dist/sessions.js.map +1 -0
- package/dist/store.d.ts +30 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +27 -0
- package/dist/store.js.map +1 -0
- package/dist/tables.d.ts +17 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +103 -0
- package/dist/tables.js.map +1 -0
- package/dist/totp.d.ts +22 -0
- package/dist/totp.d.ts.map +1 -0
- package/dist/totp.js +115 -0
- package/dist/totp.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/users.d.ts +12 -0
- package/dist/users.d.ts.map +1 -0
- package/dist/users.js +69 -0
- package/dist/users.js.map +1 -0
- package/dist/webauthn.d.ts +40 -0
- package/dist/webauthn.d.ts.map +1 -0
- package/dist/webauthn.js +81 -0
- package/dist/webauthn.js.map +1 -0
- package/package.json +44 -0
package/dist/login.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { CogentaError } from '@cogenta/core';
|
|
3
|
+
import { createCredentialStore } from './credentials.js';
|
|
4
|
+
import { requiresMfa } from './mfa.js';
|
|
5
|
+
import { createRateLimiter } from './rate-limit.js';
|
|
6
|
+
import { createSessionStore } from './sessions.js';
|
|
7
|
+
import { generateTotpSecret, totpUri, verifyTotp } from './totp.js';
|
|
8
|
+
import { createUserStore } from './users.js';
|
|
9
|
+
import { beginWebAuthnAuthentication, beginWebAuthnRegistration, completeWebAuthnAuthentication, completeWebAuthnRegistration, } from './webauthn.js';
|
|
10
|
+
const TICKET_TTL_MS = 5 * 60 * 1000;
|
|
11
|
+
/**
|
|
12
|
+
* A short-lived proof that a previous step already happened, carried between
|
|
13
|
+
* that step and whatever completes it.
|
|
14
|
+
*
|
|
15
|
+
* The same shape as a preview grant (`@cogenta/api`'s `previewCovers`): an
|
|
16
|
+
* opaque HMAC-signed ticket rather than server-side state, so the next step
|
|
17
|
+
* cannot be completed for a user (or a ceremony) whose earlier step never
|
|
18
|
+
* happened — the ticket is the only thing that says it did, and it cannot be
|
|
19
|
+
* forged without the signing key. A WebAuthn challenge rides in the same
|
|
20
|
+
* ticket rather than a separate store: `webauthn.ts`'s own doc says challenge
|
|
21
|
+
* storage is deliberately this layer's job, not a database table for
|
|
22
|
+
* something single-use that lives seconds.
|
|
23
|
+
*
|
|
24
|
+
* `purpose` is part of what gets signed, not a separate check layered on
|
|
25
|
+
* top: a login ticket and a first-time TOTP-setup ticket (or a WebAuthn
|
|
26
|
+
* registration ticket and a WebAuthn login ticket) must not be
|
|
27
|
+
* interchangeable, and folding the purpose into the signature is what makes
|
|
28
|
+
* swapping one for another a signature mismatch rather than a bug to
|
|
29
|
+
* remember not to introduce.
|
|
30
|
+
*/
|
|
31
|
+
function signTicket(key, payload) {
|
|
32
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
33
|
+
const signature = createHmac('sha256', key).update(encoded).digest('base64url');
|
|
34
|
+
return `${encoded}.${signature}`;
|
|
35
|
+
}
|
|
36
|
+
function verifyTicket(key, purpose, ticket, now) {
|
|
37
|
+
const [payload, signature] = ticket.split('.');
|
|
38
|
+
if (payload === undefined || signature === undefined)
|
|
39
|
+
return null;
|
|
40
|
+
const expected = createHmac('sha256', key).update(payload).digest('base64url');
|
|
41
|
+
const a = Buffer.from(signature);
|
|
42
|
+
const b = Buffer.from(expected);
|
|
43
|
+
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString());
|
|
47
|
+
if (parsed.purpose !== purpose)
|
|
48
|
+
return null;
|
|
49
|
+
if (parsed.userId !== null && typeof parsed.userId !== 'string')
|
|
50
|
+
return null;
|
|
51
|
+
if (typeof parsed.expiresAt !== 'number' || parsed.expiresAt <= now)
|
|
52
|
+
return null;
|
|
53
|
+
if (parsed.challenge !== undefined && typeof parsed.challenge !== 'string')
|
|
54
|
+
return null;
|
|
55
|
+
return { userId: parsed.userId, challenge: parsed.challenge };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function invalidTicket() {
|
|
62
|
+
return new CogentaError({
|
|
63
|
+
code: 'AUTH_SESSION_INVALID',
|
|
64
|
+
message: 'This sign-in attempt has expired or is invalid.',
|
|
65
|
+
hint: 'Start over from the password step.',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function webauthnNotConfigured() {
|
|
69
|
+
return new CogentaError({
|
|
70
|
+
code: 'AUTH_WEBAUTHN_FAILED',
|
|
71
|
+
message: 'Passkeys are not configured for this site.',
|
|
72
|
+
hint: 'Set relyingPartyName, relyingPartyId and origin when creating the auth service.',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function createAuthService(options) {
|
|
76
|
+
const { db, signingKey, collections, webauthn: webauthnConfig } = options;
|
|
77
|
+
const issuer = options.issuer ?? 'Cogenta';
|
|
78
|
+
const now = options.now ?? Date.now;
|
|
79
|
+
const users = createUserStore(db, now);
|
|
80
|
+
const credentials = createCredentialStore(db, now);
|
|
81
|
+
const sessions = createSessionStore(db, now);
|
|
82
|
+
const rateLimit = createRateLimiter(db, now);
|
|
83
|
+
async function issueSession(user) {
|
|
84
|
+
const session = await sessions.create(user.id);
|
|
85
|
+
return { status: 'session', session, user };
|
|
86
|
+
}
|
|
87
|
+
async function mfaChallenge(user) {
|
|
88
|
+
const kinds = await credentials.kinds(user.id);
|
|
89
|
+
const available = [];
|
|
90
|
+
if (kinds.includes('totp'))
|
|
91
|
+
available.push('totp');
|
|
92
|
+
if (kinds.includes('webauthn'))
|
|
93
|
+
available.push('webauthn');
|
|
94
|
+
if (available.length === 0) {
|
|
95
|
+
// A role that requires MFA and a user who never set one up: this is
|
|
96
|
+
// the one place that enrolment can start, with a ticket scoped to
|
|
97
|
+
// exactly that — it proves the password step happened, and nothing
|
|
98
|
+
// more, the same as the ordinary MFA ticket proves the same thing for
|
|
99
|
+
// completing a second factor that already exists.
|
|
100
|
+
return {
|
|
101
|
+
status: 'totp_setup_required',
|
|
102
|
+
ticket: signTicket(signingKey, {
|
|
103
|
+
purpose: 'totp_setup',
|
|
104
|
+
userId: user.id,
|
|
105
|
+
expiresAt: now() + TICKET_TTL_MS,
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
status: 'mfa_required',
|
|
111
|
+
ticket: signTicket(signingKey, {
|
|
112
|
+
purpose: 'login',
|
|
113
|
+
userId: user.id,
|
|
114
|
+
expiresAt: now() + TICKET_TTL_MS,
|
|
115
|
+
}),
|
|
116
|
+
availableFactors: available,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
passwordLogin: async (email, password) => {
|
|
121
|
+
const subject = email.trim().toLowerCase();
|
|
122
|
+
await rateLimit.check(subject);
|
|
123
|
+
const user = await users.byEmail(subject);
|
|
124
|
+
const valid = user !== null && (await credentials.verifyPassword(user.id, password));
|
|
125
|
+
if (!valid || user === null || user.status !== 'active') {
|
|
126
|
+
await rateLimit.record(subject);
|
|
127
|
+
throw new CogentaError({
|
|
128
|
+
code: 'AUTH_INVALID_CREDENTIALS',
|
|
129
|
+
message: 'Incorrect email or password.',
|
|
130
|
+
hint: 'Check the email and password. Repeated failures are rate-limited.',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
await rateLimit.clear(subject);
|
|
134
|
+
return requiresMfa(user.roles, collections) ? mfaChallenge(user) : issueSession(user);
|
|
135
|
+
},
|
|
136
|
+
totpLogin: async (ticket, token) => {
|
|
137
|
+
const verified = verifyTicket(signingKey, 'login', ticket, now());
|
|
138
|
+
if (verified === null || verified.userId === null)
|
|
139
|
+
throw invalidTicket();
|
|
140
|
+
const userId = verified.userId;
|
|
141
|
+
await rateLimit.check(`mfa:${userId}`);
|
|
142
|
+
const stored = await credentials.totpSecret(userId);
|
|
143
|
+
const ok = stored !== null &&
|
|
144
|
+
stored.verified &&
|
|
145
|
+
verifyTotp(token, stored.secret, { now: Math.floor(now() / 1000) });
|
|
146
|
+
if (!ok) {
|
|
147
|
+
await rateLimit.record(`mfa:${userId}`);
|
|
148
|
+
throw new CogentaError({
|
|
149
|
+
code: 'AUTH_INVALID_CREDENTIALS',
|
|
150
|
+
message: 'Incorrect verification code.',
|
|
151
|
+
hint: 'Check the code from your authenticator app. Codes are valid for 30 seconds.',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const user = await users.byId(userId);
|
|
155
|
+
if (user === null) {
|
|
156
|
+
throw new CogentaError({
|
|
157
|
+
code: 'AUTH_USER_NOT_FOUND',
|
|
158
|
+
message: 'This account no longer exists.',
|
|
159
|
+
hint: 'It may have been deleted between the password step and this one. Sign in again.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
await rateLimit.clear(`mfa:${userId}`);
|
|
163
|
+
return issueSession(user);
|
|
164
|
+
},
|
|
165
|
+
sessionForVerifiedUser: async (userId) => {
|
|
166
|
+
const user = await users.byId(userId);
|
|
167
|
+
if (user === null || user.status !== 'active') {
|
|
168
|
+
throw new CogentaError({
|
|
169
|
+
code: 'AUTH_USER_NOT_FOUND',
|
|
170
|
+
message: 'This account no longer exists or is disabled.',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return issueSession(user);
|
|
174
|
+
},
|
|
175
|
+
beginTotpSetup: async (ticket) => {
|
|
176
|
+
const verified = verifyTicket(signingKey, 'totp_setup', ticket, now());
|
|
177
|
+
if (verified === null || verified.userId === null)
|
|
178
|
+
throw invalidTicket();
|
|
179
|
+
const user = await users.byId(verified.userId);
|
|
180
|
+
if (user === null) {
|
|
181
|
+
throw new CogentaError({
|
|
182
|
+
code: 'AUTH_USER_NOT_FOUND',
|
|
183
|
+
message: 'This account no longer exists.',
|
|
184
|
+
hint: 'It may have been deleted between the password step and this one. Sign in again.',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// A fresh secret every time this is called, even for the same ticket:
|
|
188
|
+
// requesting the QR code again (a slow scan, a wrong app) must not
|
|
189
|
+
// let two different secrets both end up "current" — only the last one
|
|
190
|
+
// requested can ever be confirmed.
|
|
191
|
+
const secret = generateTotpSecret();
|
|
192
|
+
await credentials.setTotpSecret(user.id, secret);
|
|
193
|
+
return { secret, uri: totpUri(secret, issuer, user.email) };
|
|
194
|
+
},
|
|
195
|
+
confirmTotpSetup: async (ticket, token) => {
|
|
196
|
+
const verified = verifyTicket(signingKey, 'totp_setup', ticket, now());
|
|
197
|
+
if (verified === null || verified.userId === null)
|
|
198
|
+
throw invalidTicket();
|
|
199
|
+
const userId = verified.userId;
|
|
200
|
+
await rateLimit.check(`totp-setup:${userId}`);
|
|
201
|
+
const stored = await credentials.totpSecret(userId);
|
|
202
|
+
const ok = stored !== null && verifyTotp(token, stored.secret, { now: Math.floor(now() / 1000) });
|
|
203
|
+
if (!ok) {
|
|
204
|
+
await rateLimit.record(`totp-setup:${userId}`);
|
|
205
|
+
throw new CogentaError({
|
|
206
|
+
code: 'AUTH_INVALID_CREDENTIALS',
|
|
207
|
+
message: 'Incorrect verification code.',
|
|
208
|
+
hint: 'Check the code from your authenticator app. Codes are valid for 30 seconds.',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const user = await users.byId(userId);
|
|
212
|
+
if (user === null) {
|
|
213
|
+
throw new CogentaError({
|
|
214
|
+
code: 'AUTH_USER_NOT_FOUND',
|
|
215
|
+
message: 'This account no longer exists.',
|
|
216
|
+
hint: 'It may have been deleted since the password step. Sign in again.',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
await rateLimit.clear(`totp-setup:${userId}`);
|
|
220
|
+
await credentials.confirmTotp(userId);
|
|
221
|
+
return issueSession(user);
|
|
222
|
+
},
|
|
223
|
+
beginWebAuthnRegistration: async (userId) => {
|
|
224
|
+
if (webauthnConfig === undefined)
|
|
225
|
+
throw webauthnNotConfigured();
|
|
226
|
+
const user = await users.byId(userId);
|
|
227
|
+
if (user === null) {
|
|
228
|
+
throw new CogentaError({
|
|
229
|
+
code: 'AUTH_USER_NOT_FOUND',
|
|
230
|
+
message: 'This account no longer exists.',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const existing = await credentials.webAuthnCredentials(userId);
|
|
234
|
+
const { options, challenge } = await beginWebAuthnRegistration(webauthnConfig, userId, user.email, existing);
|
|
235
|
+
return {
|
|
236
|
+
options,
|
|
237
|
+
ticket: signTicket(signingKey, {
|
|
238
|
+
purpose: 'webauthn_register',
|
|
239
|
+
userId,
|
|
240
|
+
challenge,
|
|
241
|
+
expiresAt: now() + TICKET_TTL_MS,
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
completeWebAuthnRegistration: async (ticket, response, label) => {
|
|
246
|
+
if (webauthnConfig === undefined)
|
|
247
|
+
throw webauthnNotConfigured();
|
|
248
|
+
const verified = verifyTicket(signingKey, 'webauthn_register', ticket, now());
|
|
249
|
+
if (verified === null || verified.userId === null || verified.challenge === undefined) {
|
|
250
|
+
throw invalidTicket();
|
|
251
|
+
}
|
|
252
|
+
const credential = await completeWebAuthnRegistration(webauthnConfig, response, verified.challenge, label);
|
|
253
|
+
await credentials.addWebAuthnCredential(verified.userId, credential);
|
|
254
|
+
},
|
|
255
|
+
beginWebAuthnLogin: async () => {
|
|
256
|
+
if (webauthnConfig === undefined)
|
|
257
|
+
throw webauthnNotConfigured();
|
|
258
|
+
// No `allowCredentials`: this is the discoverable-credential (resident
|
|
259
|
+
// key) flow — the browser prompts for whichever passkey it holds for
|
|
260
|
+
// this site, and the assertion's own credential id says which account
|
|
261
|
+
// it is, rather than the server naming one up front.
|
|
262
|
+
const { options, challenge } = await beginWebAuthnAuthentication(webauthnConfig, []);
|
|
263
|
+
return {
|
|
264
|
+
options,
|
|
265
|
+
ticket: signTicket(signingKey, {
|
|
266
|
+
purpose: 'webauthn_login',
|
|
267
|
+
userId: null,
|
|
268
|
+
challenge,
|
|
269
|
+
expiresAt: now() + TICKET_TTL_MS,
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
completeWebAuthnLogin: async (ticket, response) => {
|
|
274
|
+
if (webauthnConfig === undefined)
|
|
275
|
+
throw webauthnNotConfigured();
|
|
276
|
+
const verified = verifyTicket(signingKey, 'webauthn_login', ticket, now());
|
|
277
|
+
if (verified === null || verified.challenge === undefined)
|
|
278
|
+
throw invalidTicket();
|
|
279
|
+
const found = await credentials.webAuthnCredentialByExternalId(response.id);
|
|
280
|
+
if (found === null) {
|
|
281
|
+
throw new CogentaError({
|
|
282
|
+
code: 'AUTH_WEBAUTHN_FAILED',
|
|
283
|
+
message: 'This passkey is not registered with any account.',
|
|
284
|
+
hint: 'Register it first from an already-signed-in session, or use a different sign-in method.',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const result = await completeWebAuthnAuthentication(webauthnConfig, response, verified.challenge, found.data);
|
|
288
|
+
await credentials.updateWebAuthnCounter(found.data.credentialId, result.newCounter);
|
|
289
|
+
const user = await users.byId(found.userId);
|
|
290
|
+
if (user === null || user.status !== 'active') {
|
|
291
|
+
throw new CogentaError({
|
|
292
|
+
code: 'AUTH_USER_NOT_FOUND',
|
|
293
|
+
message: 'This account no longer exists or is disabled.',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return issueSession(user);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"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"}
|
package/dist/mfa.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CollectionDefinition } from '@cogenta/schema';
|
|
2
|
+
export declare function sensitiveRoles(collections: readonly CollectionDefinition[]): ReadonlySet<string>;
|
|
3
|
+
export declare function requiresMfa(userRoles: readonly string[], collections: readonly CollectionDefinition[]): boolean;
|
|
4
|
+
//# sourceMappingURL=mfa.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa.d.ts","sourceRoot":"","sources":["../src/mfa.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAkB3D,wBAAgB,cAAc,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAMhG;AAED,wBAAgB,WAAW,CACzB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,WAAW,EAAE,SAAS,oBAAoB,EAAE,GAC3C,OAAO,CAGT"}
|
package/dist/mfa.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a role must clear a second factor before it can act.
|
|
3
|
+
*
|
|
4
|
+
* The spec names `content.publish` and `site.config_write` — contract C's
|
|
5
|
+
* permission taxonomy, for an agent tool. L2 has no tool permissions yet; the
|
|
6
|
+
* faithful reading for a **collection**-scoped role (contract A) is the
|
|
7
|
+
* closest equivalent action: a role that can `publish` on any collection, and
|
|
8
|
+
* the `admin` role always, since site configuration has no dedicated model in
|
|
9
|
+
* L2 and `admin` is where that power concentrates until it does.
|
|
10
|
+
*
|
|
11
|
+
* This is not configurable per site, on purpose — the spec says "non
|
|
12
|
+
* contournable par configuration", and a setting that can be turned off is a
|
|
13
|
+
* setting that will be, by whoever is in the biggest hurry the day it matters.
|
|
14
|
+
*/
|
|
15
|
+
const ALWAYS_SENSITIVE_ROLES = new Set(['admin']);
|
|
16
|
+
export function sensitiveRoles(collections) {
|
|
17
|
+
const roles = new Set(ALWAYS_SENSITIVE_ROLES);
|
|
18
|
+
for (const collection of collections) {
|
|
19
|
+
for (const role of collection.permissions.publish ?? [])
|
|
20
|
+
roles.add(role);
|
|
21
|
+
}
|
|
22
|
+
return roles;
|
|
23
|
+
}
|
|
24
|
+
export function requiresMfa(userRoles, collections) {
|
|
25
|
+
const sensitive = sensitiveRoles(collections);
|
|
26
|
+
return userRoles.some((role) => sensitive.has(role));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=mfa.js.map
|
package/dist/mfa.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa.js","sourceRoot":"","sources":["../src/mfa.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;AAEjD,MAAM,UAAU,cAAc,CAAC,WAA4C;IACzE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,sBAAsB,CAAC,CAAA;IAC7C,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,WAAW,CAAC,OAAO,IAAI,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC1E,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,SAA4B,EAC5B,WAA4C;IAE5C,MAAM,SAAS,GAAG,cAAc,CAAC,WAAW,CAAC,CAAA;IAC7C,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;AACtD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
2
|
+
/**
|
|
3
|
+
* Verifies a password against a stored hash.
|
|
4
|
+
*
|
|
5
|
+
* Every failure path — wrong format, wrong length, wrong bytes — takes the
|
|
6
|
+
* same route to `false` in roughly the same time. A verify function whose
|
|
7
|
+
* error path is faster than its comparison path leaks, through timing, which
|
|
8
|
+
* kind of wrong the guess was.
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
11
|
+
//# sourceMappingURL=password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"AA8CA,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUpE;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYvF"}
|
package/dist/password.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { CogentaError } from '@cogenta/core';
|
|
3
|
+
// `util.promisify` cannot see the options-object overload of `scrypt`, only
|
|
4
|
+
// the no-options one — this wrapper is the three extra lines that buys back
|
|
5
|
+
// the tunable cost parameters without losing the async, non-blocking form.
|
|
6
|
+
function scrypt(password, salt, keyLength, params) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
// Node refuses to run scrypt above its default 32MB `maxmem` ceiling, and
|
|
9
|
+
// the OWASP-floor cost below (N=2^15, r=8) needs `128 * N * r` = exactly
|
|
10
|
+
// that many bytes before OpenSSL's own overhead — so the default clips
|
|
11
|
+
// it. Raise the ceiling instead of lowering the cost.
|
|
12
|
+
const maxmem = 128 * params.N * params.r * 2;
|
|
13
|
+
scryptCallback(password, salt, keyLength, { ...params, maxmem }, (error, derived) => {
|
|
14
|
+
if (error)
|
|
15
|
+
reject(error);
|
|
16
|
+
else
|
|
17
|
+
resolve(derived);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* scrypt from `node:crypto`, not a dependency.
|
|
23
|
+
*
|
|
24
|
+
* bcrypt and argon2 are both native modules — R10 forbids that without a WASM
|
|
25
|
+
* fallback, and neither ships one. scrypt is memory-hard, tunable, and has
|
|
26
|
+
* been in Node's standard library since 10.5, so there is nothing to add.
|
|
27
|
+
*/
|
|
28
|
+
const KEY_LENGTH = 64;
|
|
29
|
+
const SALT_LENGTH = 16;
|
|
30
|
+
// N=2^15 costs roughly 100ms on ordinary hardware. Doubling it doubles login
|
|
31
|
+
// latency for everyone to make one offline attacker's job twice as hard; this
|
|
32
|
+
// is the OWASP-recommended floor for scrypt, not a guess.
|
|
33
|
+
const SCRYPT_PARAMS = { N: 2 ** 15, r: 8, p: 1 };
|
|
34
|
+
const MAX_INPUT_LENGTH = 512;
|
|
35
|
+
export async function hashPassword(password) {
|
|
36
|
+
assertLength(password);
|
|
37
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
38
|
+
const derived = await scrypt(password.normalize('NFKC'), salt, KEY_LENGTH, SCRYPT_PARAMS);
|
|
39
|
+
// Parameters travel with the hash, the way bcrypt embeds its cost factor.
|
|
40
|
+
// Raising SCRYPT_PARAMS later must not invalidate every password already
|
|
41
|
+
// stored — it invalidates none, because each hash carries what made it.
|
|
42
|
+
const { N, r, p } = SCRYPT_PARAMS;
|
|
43
|
+
return `scrypt$${N}$${r}$${p}$${salt.toString('base64url')}$${derived.toString('base64url')}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verifies a password against a stored hash.
|
|
47
|
+
*
|
|
48
|
+
* Every failure path — wrong format, wrong length, wrong bytes — takes the
|
|
49
|
+
* same route to `false` in roughly the same time. A verify function whose
|
|
50
|
+
* error path is faster than its comparison path leaks, through timing, which
|
|
51
|
+
* kind of wrong the guess was.
|
|
52
|
+
*/
|
|
53
|
+
export async function verifyPassword(password, stored) {
|
|
54
|
+
const parsed = parseHash(stored);
|
|
55
|
+
if (parsed === null)
|
|
56
|
+
return false;
|
|
57
|
+
if (password.length === 0 || password.length > MAX_INPUT_LENGTH)
|
|
58
|
+
return false;
|
|
59
|
+
const derived = await scrypt(password.normalize('NFKC'), parsed.salt, parsed.expected.length, {
|
|
60
|
+
N: parsed.N,
|
|
61
|
+
r: parsed.r,
|
|
62
|
+
p: parsed.p,
|
|
63
|
+
});
|
|
64
|
+
return derived.length === parsed.expected.length && timingSafeEqual(derived, parsed.expected);
|
|
65
|
+
}
|
|
66
|
+
function parseHash(stored) {
|
|
67
|
+
const parts = stored.split('$');
|
|
68
|
+
if (parts.length !== 6 || parts[0] !== 'scrypt')
|
|
69
|
+
return null;
|
|
70
|
+
const N = Number(parts[1]);
|
|
71
|
+
const r = Number(parts[2]);
|
|
72
|
+
const p = Number(parts[3]);
|
|
73
|
+
if (![N, r, p].every((value) => Number.isInteger(value) && value > 0))
|
|
74
|
+
return null;
|
|
75
|
+
// An empty salt or hash field parses fine but must not verify: scrypt with
|
|
76
|
+
// a zero-length key returns a zero-length buffer, which trivially equals
|
|
77
|
+
// another zero-length buffer — an empty stored hash would then "match"
|
|
78
|
+
// any password.
|
|
79
|
+
if (parts[4] === '' || parts[5] === '')
|
|
80
|
+
return null;
|
|
81
|
+
try {
|
|
82
|
+
const salt = Buffer.from(parts[4] ?? '', 'base64url');
|
|
83
|
+
const expected = Buffer.from(parts[5] ?? '', 'base64url');
|
|
84
|
+
if (salt.length === 0 || expected.length === 0)
|
|
85
|
+
return null;
|
|
86
|
+
return { N, r, p, salt, expected };
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function assertLength(password) {
|
|
93
|
+
if (password.length === 0) {
|
|
94
|
+
throw new CogentaError({
|
|
95
|
+
code: 'AUTH_PASSWORD_INVALID',
|
|
96
|
+
message: 'A password must not be empty.',
|
|
97
|
+
hint: 'Ask for a password with at least a minimum length before hashing it.',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (password.length > MAX_INPUT_LENGTH) {
|
|
101
|
+
throw new CogentaError({
|
|
102
|
+
code: 'AUTH_PASSWORD_INVALID',
|
|
103
|
+
message: `A password longer than ${MAX_INPUT_LENGTH} characters is refused before hashing.`,
|
|
104
|
+
hint: 'scrypt has no practical upper bound, but an unbounded input is a denial-of-service knob — a large password costs CPU proportional to its length before it is even compared.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
export interface RateLimiter {
|
|
3
|
+
/** Throws `AUTH_RATE_LIMITED` if the subject is currently backed off. */
|
|
4
|
+
check(subject: string): Promise<void>;
|
|
5
|
+
record(subject: string): Promise<void>;
|
|
6
|
+
/** Called after a successful login: past failures stop counting against them. */
|
|
7
|
+
clear(subject: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRateLimiter(db: DatabaseHandle, now?: () => number): RateLimiter;
|
|
10
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAmBzF,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,iFAAiF;IACjF,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,WAAW,CAkC/F"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CogentaError, identifier, newId, sql } from '@cogenta/core';
|
|
2
|
+
import { TABLES } from './tables.js';
|
|
3
|
+
/**
|
|
4
|
+
* Backoff on login attempts, keyed by subject — the email tried, or the caller's
|
|
5
|
+
* IP when the email itself is what needs protecting from enumeration.
|
|
6
|
+
*
|
|
7
|
+
* Progressive, not a hard cap: five attempts inside a minute is a typo, thirty
|
|
8
|
+
* is a script. The delay is what makes a script expensive without locking a
|
|
9
|
+
* real person out of their own account after one bad guess.
|
|
10
|
+
*/
|
|
11
|
+
const WINDOW_MS = 15 * 60 * 1000;
|
|
12
|
+
const THRESHOLDS = [
|
|
13
|
+
{ attempts: 5, delayMs: 1_000 },
|
|
14
|
+
{ attempts: 10, delayMs: 10_000 },
|
|
15
|
+
{ attempts: 15, delayMs: 60_000 },
|
|
16
|
+
{ attempts: 20, delayMs: 15 * 60 * 1000 },
|
|
17
|
+
];
|
|
18
|
+
export function createRateLimiter(db, now = Date.now) {
|
|
19
|
+
const table = identifier(TABLES.loginAttempts, db.dialect);
|
|
20
|
+
async function countRecent(subject) {
|
|
21
|
+
const since = new Date(now() - WINDOW_MS).toISOString();
|
|
22
|
+
const result = await db.query(sql `select count(*) as n from ${table} where subject = ${subject} and at >= ${since}`);
|
|
23
|
+
return Number(result.rows[0]?.n ?? 0);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
check: async (subject) => {
|
|
27
|
+
const attempts = await countRecent(subject);
|
|
28
|
+
const threshold = [...THRESHOLDS].reverse().find((entry) => attempts >= entry.attempts);
|
|
29
|
+
if (threshold === undefined)
|
|
30
|
+
return;
|
|
31
|
+
throw new CogentaError({
|
|
32
|
+
code: 'AUTH_RATE_LIMITED',
|
|
33
|
+
message: 'Too many attempts. Try again later.',
|
|
34
|
+
hint: `Wait about ${Math.ceil(threshold.delayMs / 1000)} seconds before trying again, or use a passkey, which this limit does not slow down.`,
|
|
35
|
+
details: { retryAfterMs: threshold.delayMs },
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
record: async (subject) => {
|
|
39
|
+
await db.query(sql `
|
|
40
|
+
insert into ${table} (id, subject, at) values (${newId(now)}, ${subject}, ${new Date(now()).toISOString()})`);
|
|
41
|
+
},
|
|
42
|
+
clear: async (subject) => {
|
|
43
|
+
await db.query(sql `delete from ${table} where subject = ${subject}`);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC;;;;;;;GAOG;AACH,MAAM,SAAS,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAChC,MAAM,UAAU,GAAuE;IACrF,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE;IAC/B,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE;IACjC,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE;IACjC,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE;CAC1C,CAAA;AAUD,MAAM,UAAU,iBAAiB,CAAC,EAAkB,EAAE,GAAG,GAAiB,IAAI,CAAC,GAAG;IAChF,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAE1D,KAAK,UAAU,WAAW,CAAC,OAAe;QACxC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,CAAA;QACvD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAC3B,GAAG,CAAA,6BAA6B,KAAK,oBAAoB,OAAO,cAAc,KAAK,EAAE,CACtF,CAAA;QACD,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IACvC,CAAC;IAED,OAAO;QACL,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACvB,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;YAC3C,MAAM,SAAS,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAA;YACvF,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAM;YAEnC,MAAM,IAAI,YAAY,CAAC;gBACrB,IAAI,EAAE,mBAAmB;gBACzB,OAAO,EAAE,qCAAqC;gBAC9C,IAAI,EAAE,cAAc,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,sFAAsF;gBAC7I,OAAO,EAAE,EAAE,YAAY,EAAE,SAAS,CAAC,OAAO,EAAE;aAC7C,CAAC,CAAA;QACJ,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACxB,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;sBACF,KAAK,8BAA8B,KAAK,CAAC,GAAG,CAAC,KAAK,OAAO,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;QACjH,CAAC;QAED,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACvB,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA,eAAe,KAAK,oBAAoB,OAAO,EAAE,CAAC,CAAA;QACtE,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import type { IssuedSession, Session } from './types.js';
|
|
3
|
+
export interface SessionStore {
|
|
4
|
+
create(userId: string, options?: {
|
|
5
|
+
label?: string;
|
|
6
|
+
ttlMs?: number;
|
|
7
|
+
}): Promise<IssuedSession>;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves a bearer token to its session, or `null` if it is missing,
|
|
10
|
+
* expired or revoked. Touches `lastSeenAt` on success — a session is a
|
|
11
|
+
* sliding window, not a fixed one, so a person working for an hour is not
|
|
12
|
+
* signed out mid-task.
|
|
13
|
+
*/
|
|
14
|
+
resolve(token: string): Promise<Session | null>;
|
|
15
|
+
list(userId: string): Promise<readonly Session[]>;
|
|
16
|
+
revoke(sessionId: string): Promise<void>;
|
|
17
|
+
revokeAll(userId: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createSessionStore(db: DatabaseHandle, now?: () => number): SessionStore;
|
|
20
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AA4CxD,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAC5F;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IAC/C,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;IACjD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,YAAY,CA2DjG"}
|