@bevel-software/platform-core-backend 0.1.0 → 0.2.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/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +13 -0
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +35 -5
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts +45 -1
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +66 -2
- package/dist/core-config.js.map +1 -1
- package/dist/modules/admin/admin-access.service.d.ts +17 -1
- package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
- package/dist/modules/admin/admin-access.service.js +14 -1
- package/dist/modules/admin/admin-access.service.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +17 -0
- package/dist/modules/auth/account.routes.d.ts.map +1 -0
- package/dist/modules/auth/account.routes.js +47 -0
- package/dist/modules/auth/account.routes.js.map +1 -0
- package/dist/modules/auth/auth.routes.d.ts +13 -7
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +51 -3
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +44 -8
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +129 -32
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts +51 -0
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -0
- package/dist/modules/auth/oidc-auth-provider.js +189 -0
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -0
- package/dist/modules/auth/password-hash.d.ts +12 -0
- package/dist/modules/auth/password-hash.d.ts.map +1 -0
- package/dist/modules/auth/password-hash.js +67 -0
- package/dist/modules/auth/password-hash.js.map +1 -0
- package/dist/modules/auth/rate-limit.d.ts +32 -0
- package/dist/modules/auth/rate-limit.d.ts.map +1 -0
- package/dist/modules/auth/rate-limit.js +49 -0
- package/dist/modules/auth/rate-limit.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +17 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +6 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +10 -0
- package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.js +12 -0
- package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
- package/migrations/0001_user_password_hash.sql +4 -0
- package/migrations/meta/_journal.json +20 -13
- package/package.json +2 -2
- package/src/core/create-core-server.ts +352 -338
- package/src/core/create-core-services.ts +542 -501
- package/src/core-config.ts +65 -2
- package/src/modules/admin/admin-access.service.ts +10 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +98 -0
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -0
- package/src/modules/auth/__tests__/auth.service.test.ts +217 -0
- package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +174 -0
- package/src/modules/auth/__tests__/password-hash.test.ts +53 -0
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -0
- package/src/modules/auth/account.routes.ts +59 -0
- package/src/modules/auth/auth.routes.ts +68 -10
- package/src/modules/auth/auth.service.ts +151 -32
- package/src/modules/auth/oidc-auth-provider.ts +229 -0
- package/src/modules/auth/password-hash.ts +80 -0
- package/src/modules/auth/rate-limit.ts +45 -0
- package/src/modules/database/core-schema.ts +437 -431
- package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +25 -1
- package/src/modules/tool-auth/internal-token.service.ts +13 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.routes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.routes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAmB,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;CACrE;AAED,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,OAAO,CAAC,cAAc,EACtC,SAAS,GAAE,kBAAkB,EAAO,EACpC,oBAAoB,UAAO,GAC1B,OAAO,CAAC,MAAM,CAmHhB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { AUTH_COOKIE_NAME } from './auth.middleware.js'; // also imports Express Request augmentation
|
|
3
|
+
import { FixedWindowRateLimiter } from './rate-limit.js';
|
|
3
4
|
/**
|
|
4
5
|
* Max age of the JWT cookie (seconds). Matches the JWT's own `expiresIn:
|
|
5
6
|
* '7d'` so the cookie expires when the token does — browsers stop
|
|
@@ -9,7 +10,19 @@ import { AUTH_COOKIE_NAME } from './auth.middleware.js'; // also imports Express
|
|
|
9
10
|
export const AUTH_COOKIE_MAX_AGE_S = 7 * 24 * 60 * 60;
|
|
10
11
|
export function createAuthRoutes(authService, authMiddleware, providers = [], passwordLoginEnabled = true) {
|
|
11
12
|
const router = express.Router();
|
|
12
|
-
//
|
|
13
|
+
// Brute-force guards on the password endpoints (scrypt also makes each
|
|
14
|
+
// attempt expensive server-side, so these double as DoS protection):
|
|
15
|
+
// - login: a tight per-(ip, email) budget plus a wider per-ip budget so a
|
|
16
|
+
// single address can't spray many emails; the pair key resets on a
|
|
17
|
+
// successful login.
|
|
18
|
+
// - change-password: per authenticated account (a stolen session must not
|
|
19
|
+
// get unlimited guesses at the current password).
|
|
20
|
+
const loginPairLimiter = new FixedWindowRateLimiter(10, 15 * 60_000);
|
|
21
|
+
const loginIpLimiter = new FixedWindowRateLimiter(50, 15 * 60_000);
|
|
22
|
+
const changePasswordLimiter = new FixedWindowRateLimiter(10, 15 * 60_000);
|
|
23
|
+
const clientIp = (req) => req.ip ?? req.socket.remoteAddress ?? 'unknown';
|
|
24
|
+
// POST /api/auth/login — email + password login (unprotected): the env
|
|
25
|
+
// bootstrap admin or a per-user account password (see AuthService).
|
|
13
26
|
router.post('/auth/login', async (req, res) => {
|
|
14
27
|
if (!passwordLoginEnabled) {
|
|
15
28
|
res.status(403).json({ error: 'Password login is disabled' });
|
|
@@ -20,8 +33,18 @@ export function createAuthRoutes(authService, authMiddleware, providers = [], pa
|
|
|
20
33
|
res.status(400).json({ error: 'email and password are required' });
|
|
21
34
|
return;
|
|
22
35
|
}
|
|
36
|
+
const ip = clientIp(req);
|
|
37
|
+
const pairKey = `${ip}|${email.trim().toLowerCase()}`;
|
|
38
|
+
if (!loginPairLimiter.consume(pairKey) || !loginIpLimiter.consume(ip)) {
|
|
39
|
+
res.status(429).json({ error: 'Too many attempts. Try again later.' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
23
42
|
try {
|
|
24
43
|
const result = await authService.loginWithPassword(email, password);
|
|
44
|
+
// A real login clears the pair budget — a user who fumbled a few
|
|
45
|
+
// attempts isn't rate-limited on their next visit. (The wider per-ip
|
|
46
|
+
// window intentionally keeps counting.)
|
|
47
|
+
loginPairLimiter.reset(pairKey);
|
|
25
48
|
// Also set the JWT as an HttpOnly cookie so transports that can't
|
|
26
49
|
// carry headers (EventSource for SSE, image tags) authenticate
|
|
27
50
|
// automatically. JSON callers continue to use the Bearer header
|
|
@@ -43,15 +66,40 @@ export function createAuthRoutes(authService, authMiddleware, providers = [], pa
|
|
|
43
66
|
}
|
|
44
67
|
});
|
|
45
68
|
// GET /api/auth/providers — unprotected capability probe so the login screen
|
|
46
|
-
// knows which login methods to render
|
|
69
|
+
// knows which login methods to render: the password form, plus one button
|
|
70
|
+
// per SSO provider (label + the path that starts its flow).
|
|
47
71
|
router.get('/auth/providers', (_req, res) => {
|
|
48
72
|
res.json({
|
|
49
73
|
password: passwordLoginEnabled,
|
|
50
|
-
|
|
74
|
+
sso: providers.map((p) => ({ key: p.key, label: p.label, startPath: p.startPath })),
|
|
51
75
|
});
|
|
52
76
|
});
|
|
53
77
|
for (const provider of providers)
|
|
54
78
|
provider.mountRoutes(router, authService);
|
|
79
|
+
// POST /api/auth/change-password — self-service (protected). Requires the
|
|
80
|
+
// current password whenever one is set; an SSO-only account sets its first
|
|
81
|
+
// password without one.
|
|
82
|
+
router.post('/auth/change-password', authMiddleware, async (req, res) => {
|
|
83
|
+
const { currentPassword, newPassword } = req.body;
|
|
84
|
+
if (!newPassword) {
|
|
85
|
+
res.status(400).json({ error: 'newPassword is required' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!changePasswordLimiter.consume(req.userId)) {
|
|
89
|
+
res.status(429).json({ error: 'Too many attempts. Try again later.' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await authService.changePassword(req.userId, currentPassword, newPassword);
|
|
94
|
+
res.json({ ok: true });
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
98
|
+
// Policy violations and a wrong current password are caller errors, not
|
|
99
|
+
// server faults — and the message is safe to show verbatim.
|
|
100
|
+
res.status(400).json({ error: msg });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
55
103
|
// GET /api/auth/me — validate stored JWT, return user info (protected)
|
|
56
104
|
router.get('/auth/me', authMiddleware, async (req, res) => {
|
|
57
105
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.routes.js","sourceRoot":"","sources":["../../../src/modules/auth/auth.routes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC,CAAC,4CAA4C;
|
|
1
|
+
{"version":3,"file":"auth.routes.js","sourceRoot":"","sources":["../../../src/modules/auth/auth.routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,22 +5,59 @@ export declare class AuthService {
|
|
|
5
5
|
private readonly db;
|
|
6
6
|
private readonly config;
|
|
7
7
|
constructor(db: Database, config: CoreConfig);
|
|
8
|
+
/**
|
|
9
|
+
* Password login. Two credential sources, in order:
|
|
10
|
+
*
|
|
11
|
+
* 1. The env bootstrap admin (`ADMIN_EMAIL` + `ADMIN_PASSWORD`), checked
|
|
12
|
+
* directly against the environment — never stored, so unsetting either
|
|
13
|
+
* variable disables it immediately. Lets a fresh deployment sign in
|
|
14
|
+
* before any account exists.
|
|
15
|
+
* 2. A per-user account: the `users` row's scrypt `password_hash`
|
|
16
|
+
* (created by an admin from Roles & Members, or set by the user on
|
|
17
|
+
* their Account page). Accounts that only ever signed in via SSO have
|
|
18
|
+
* no hash and are refused here.
|
|
19
|
+
*
|
|
20
|
+
* A generic "Invalid credentials" error for every failure mode — never
|
|
21
|
+
* reveal whether the email exists or has a password.
|
|
22
|
+
*/
|
|
8
23
|
loginWithPassword(email: string, password: string): Promise<{
|
|
9
24
|
token: string;
|
|
10
25
|
user: AuthUser;
|
|
11
26
|
}>;
|
|
12
27
|
/**
|
|
13
|
-
* Establish a session for a user
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the same account/id.
|
|
28
|
+
* Establish a session for a user whose identity an SSO provider (the
|
|
29
|
+
* generic OIDC plugin, or the enterprise "Sign in with Microsoft") already
|
|
30
|
+
* verified. Same upsert-by-email + JWT path as password login. Email is the
|
|
31
|
+
* idempotency key, so a user who first used password login and later signs
|
|
32
|
+
* in via SSO (same email) keeps the same account/id.
|
|
19
33
|
*/
|
|
20
|
-
|
|
34
|
+
loginWithSso(email: string, name: string): Promise<{
|
|
21
35
|
token: string;
|
|
22
36
|
user: AuthUser;
|
|
23
37
|
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Admin-driven account provisioning (Roles & Members → Accounts). Upserts
|
|
40
|
+
* the user by email and sets their password — re-provisioning an existing
|
|
41
|
+
* account (e.g. one that first arrived via SSO, or a reset for a locked-out
|
|
42
|
+
* user) is deliberate admin behavior, not an error.
|
|
43
|
+
*/
|
|
44
|
+
createAccount(email: string, name: string | undefined, password: string): Promise<AuthUser>;
|
|
45
|
+
/**
|
|
46
|
+
* Self-service password change (the Account page). The current password is
|
|
47
|
+
* required whenever one is set; an SSO-only account (no hash yet) may set
|
|
48
|
+
* its first password without one. The env bootstrap-admin credential is not
|
|
49
|
+
* affected — it lives in the environment, not in this row.
|
|
50
|
+
*/
|
|
51
|
+
changePassword(userId: string, currentPassword: string | undefined, newPassword: string): Promise<void>;
|
|
52
|
+
/** Accounts overview for the admin management screen (never exposes hashes). */
|
|
53
|
+
listAccounts(): Promise<Array<{
|
|
54
|
+
id: string;
|
|
55
|
+
email: string;
|
|
56
|
+
name: string;
|
|
57
|
+
hasPassword: boolean;
|
|
58
|
+
createdAt: Date;
|
|
59
|
+
}>>;
|
|
60
|
+
private assertPasswordPolicy;
|
|
24
61
|
private signToken;
|
|
25
62
|
/**
|
|
26
63
|
* Get-or-create a user record by email, without issuing a session token.
|
|
@@ -71,6 +108,5 @@ export declare class AuthService {
|
|
|
71
108
|
* regex-validated) email.
|
|
72
109
|
*/
|
|
73
110
|
private assertAllowedDomain;
|
|
74
|
-
private checkPassword;
|
|
75
111
|
}
|
|
76
112
|
//# sourceMappingURL=auth.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAsChE,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,UAAU;IAGrC;;;;;;;;;;;;;;OAcG;IACG,iBAAiB,CACrB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;IAsC7C;;;;;;OAMG;IACG,YAAY,CAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;IAW7C;;;;;OAKG;IACG,aAAa,CACjB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC;IA2BpB;;;;;OAKG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IAgBhB,gFAAgF;IAC1E,YAAY,IAAI,OAAO,CAC3B,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,CAAC,CAC1F;IAWD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,SAAS;IAIjB;;;;;;;OAOG;IACG,kBAAkB,CACtB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;YAQhD,iBAAiB;IAS/B,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAQ7D;;;;;;;;;OASG;IACG,oBAAoB,CACxB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAY9D,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAiB3D;;;;;;;OAOG;IACH,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAQ5C;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAM5B"}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
2
1
|
import jwt from 'jsonwebtoken';
|
|
3
2
|
import { eq } from 'drizzle-orm';
|
|
4
3
|
import { users } from '../database/schema.js';
|
|
5
4
|
import { hashEmail } from '../../shared/hash-email.js';
|
|
5
|
+
import { hashPassword, verifyPassword, timingSafeStringEqual, MIN_PASSWORD_LENGTH, } from './password-hash.js';
|
|
6
6
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
7
|
+
/**
|
|
8
|
+
* Decoy hash verified when the email is unknown or has no password set, so
|
|
9
|
+
* those paths cost the same scrypt work as a real wrong-password attempt —
|
|
10
|
+
* without it, response timing would reveal which emails have accounts.
|
|
11
|
+
* Lazily computed once; the compared password never matches it (verification
|
|
12
|
+
* result is discarded — those paths always refuse).
|
|
13
|
+
*/
|
|
14
|
+
let decoyHashPromise = null;
|
|
15
|
+
function decoyHash() {
|
|
16
|
+
decoyHashPromise ??= hashPassword('bevel-decoy-password-never-matches');
|
|
17
|
+
return decoyHashPromise;
|
|
18
|
+
}
|
|
7
19
|
function toAuthUser(user) {
|
|
8
20
|
return {
|
|
9
21
|
id: user.id,
|
|
@@ -19,38 +31,140 @@ export class AuthService {
|
|
|
19
31
|
this.db = db;
|
|
20
32
|
this.config = config;
|
|
21
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Password login. Two credential sources, in order:
|
|
36
|
+
*
|
|
37
|
+
* 1. The env bootstrap admin (`ADMIN_EMAIL` + `ADMIN_PASSWORD`), checked
|
|
38
|
+
* directly against the environment — never stored, so unsetting either
|
|
39
|
+
* variable disables it immediately. Lets a fresh deployment sign in
|
|
40
|
+
* before any account exists.
|
|
41
|
+
* 2. A per-user account: the `users` row's scrypt `password_hash`
|
|
42
|
+
* (created by an admin from Roles & Members, or set by the user on
|
|
43
|
+
* their Account page). Accounts that only ever signed in via SSO have
|
|
44
|
+
* no hash and are refused here.
|
|
45
|
+
*
|
|
46
|
+
* A generic "Invalid credentials" error for every failure mode — never
|
|
47
|
+
* reveal whether the email exists or has a password.
|
|
48
|
+
*/
|
|
22
49
|
async loginWithPassword(email, password) {
|
|
23
50
|
const normalizedEmail = (email ?? '').trim().toLowerCase();
|
|
24
51
|
if (!EMAIL_REGEX.test(normalizedEmail)) {
|
|
25
|
-
throw new Error('Invalid
|
|
52
|
+
throw new Error('Invalid credentials');
|
|
26
53
|
}
|
|
27
54
|
this.assertAllowedDomain(normalizedEmail);
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
const provided = password ?? '';
|
|
56
|
+
const isEnvAdmin = this.config.adminEmail.length > 0 &&
|
|
57
|
+
this.config.adminPassword.length > 0 &&
|
|
58
|
+
normalizedEmail === this.config.adminEmail &&
|
|
59
|
+
timingSafeStringEqual(provided, this.config.adminPassword);
|
|
60
|
+
if (isEnvAdmin) {
|
|
61
|
+
const defaultName = normalizedEmail.split('@')[0] || normalizedEmail;
|
|
62
|
+
const user = await this.upsertUserByEmail(normalizedEmail, defaultName);
|
|
63
|
+
return { token: this.signToken(user.id, user.email), user: toAuthUser(user) };
|
|
64
|
+
}
|
|
65
|
+
const [user] = await this.db
|
|
66
|
+
.select()
|
|
67
|
+
.from(users)
|
|
68
|
+
.where(eq(users.email, normalizedEmail))
|
|
69
|
+
.limit(1);
|
|
70
|
+
// Always run one scrypt verification — against the stored hash when there
|
|
71
|
+
// is one, against the decoy otherwise — so unknown emails and
|
|
72
|
+
// password-less (SSO-only) accounts take the same time as a wrong
|
|
73
|
+
// password and can't be enumerated via response timing.
|
|
74
|
+
const stored = user?.passwordHash ?? (await decoyHash());
|
|
75
|
+
const matches = await verifyPassword(provided, stored);
|
|
76
|
+
if (!user?.passwordHash || !matches) {
|
|
77
|
+
throw new Error('Invalid credentials');
|
|
30
78
|
}
|
|
31
|
-
// Derive a default display name from the local-part of the email
|
|
32
|
-
const defaultName = normalizedEmail.split('@')[0] || normalizedEmail;
|
|
33
|
-
const user = await this.upsertUserByEmail(normalizedEmail, defaultName);
|
|
34
79
|
return { token: this.signToken(user.id, user.email), user: toAuthUser(user) };
|
|
35
80
|
}
|
|
36
81
|
/**
|
|
37
|
-
* Establish a session for a user
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* the same account/id.
|
|
82
|
+
* Establish a session for a user whose identity an SSO provider (the
|
|
83
|
+
* generic OIDC plugin, or the enterprise "Sign in with Microsoft") already
|
|
84
|
+
* verified. Same upsert-by-email + JWT path as password login. Email is the
|
|
85
|
+
* idempotency key, so a user who first used password login and later signs
|
|
86
|
+
* in via SSO (same email) keeps the same account/id.
|
|
43
87
|
*/
|
|
44
|
-
async
|
|
88
|
+
async loginWithSso(email, name) {
|
|
45
89
|
const normalizedEmail = (email ?? '').trim().toLowerCase();
|
|
46
90
|
if (!EMAIL_REGEX.test(normalizedEmail)) {
|
|
47
|
-
throw new Error('
|
|
91
|
+
throw new Error('Sign-in returned an invalid email');
|
|
48
92
|
}
|
|
49
93
|
this.assertAllowedDomain(normalizedEmail);
|
|
50
94
|
const displayName = (name ?? '').trim() || normalizedEmail.split('@')[0] || normalizedEmail;
|
|
51
95
|
const user = await this.upsertUserByEmail(normalizedEmail, displayName);
|
|
52
96
|
return { token: this.signToken(user.id, user.email), user: toAuthUser(user) };
|
|
53
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Admin-driven account provisioning (Roles & Members → Accounts). Upserts
|
|
100
|
+
* the user by email and sets their password — re-provisioning an existing
|
|
101
|
+
* account (e.g. one that first arrived via SSO, or a reset for a locked-out
|
|
102
|
+
* user) is deliberate admin behavior, not an error.
|
|
103
|
+
*/
|
|
104
|
+
async createAccount(email, name, password) {
|
|
105
|
+
const normalizedEmail = (email ?? '').trim().toLowerCase();
|
|
106
|
+
if (!EMAIL_REGEX.test(normalizedEmail)) {
|
|
107
|
+
throw new Error('Invalid email');
|
|
108
|
+
}
|
|
109
|
+
this.assertAllowedDomain(normalizedEmail);
|
|
110
|
+
this.assertPasswordPolicy(password);
|
|
111
|
+
const suppliedName = (name ?? '').trim();
|
|
112
|
+
const displayName = suppliedName || normalizedEmail.split('@')[0] || normalizedEmail;
|
|
113
|
+
const passwordHash = await hashPassword(password);
|
|
114
|
+
// One atomic upsert. On conflict (re-provisioning an existing account) an
|
|
115
|
+
// EXPLICITLY supplied name is persisted too; a blank name keeps the
|
|
116
|
+
// existing display name rather than clobbering it with the local-part
|
|
117
|
+
// fallback. `returning()` yields the authoritative row either way.
|
|
118
|
+
const [row] = await this.db
|
|
119
|
+
.insert(users)
|
|
120
|
+
.values({ email: normalizedEmail, name: displayName, passwordHash })
|
|
121
|
+
.onConflictDoUpdate({
|
|
122
|
+
target: users.email,
|
|
123
|
+
set: suppliedName
|
|
124
|
+
? { passwordHash, name: suppliedName, updatedAt: new Date() }
|
|
125
|
+
: { passwordHash, updatedAt: new Date() },
|
|
126
|
+
})
|
|
127
|
+
.returning();
|
|
128
|
+
return toAuthUser(row);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Self-service password change (the Account page). The current password is
|
|
132
|
+
* required whenever one is set; an SSO-only account (no hash yet) may set
|
|
133
|
+
* its first password without one. The env bootstrap-admin credential is not
|
|
134
|
+
* affected — it lives in the environment, not in this row.
|
|
135
|
+
*/
|
|
136
|
+
async changePassword(userId, currentPassword, newPassword) {
|
|
137
|
+
this.assertPasswordPolicy(newPassword);
|
|
138
|
+
const [user] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
|
|
139
|
+
if (!user)
|
|
140
|
+
throw new Error('User not found');
|
|
141
|
+
if (user.passwordHash) {
|
|
142
|
+
if (!currentPassword || !(await verifyPassword(currentPassword, user.passwordHash))) {
|
|
143
|
+
throw new Error('Current password is incorrect');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const passwordHash = await hashPassword(newPassword);
|
|
147
|
+
await this.db
|
|
148
|
+
.update(users)
|
|
149
|
+
.set({ passwordHash, updatedAt: new Date() })
|
|
150
|
+
.where(eq(users.id, userId));
|
|
151
|
+
}
|
|
152
|
+
/** Accounts overview for the admin management screen (never exposes hashes). */
|
|
153
|
+
async listAccounts() {
|
|
154
|
+
const rows = await this.db.select().from(users).orderBy(users.email);
|
|
155
|
+
return rows.map((row) => ({
|
|
156
|
+
id: row.id,
|
|
157
|
+
email: row.email,
|
|
158
|
+
name: row.name,
|
|
159
|
+
hasPassword: row.passwordHash != null,
|
|
160
|
+
createdAt: row.createdAt,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
assertPasswordPolicy(password) {
|
|
164
|
+
if ((password ?? '').length < MIN_PASSWORD_LENGTH) {
|
|
165
|
+
throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
54
168
|
signToken(userId, email) {
|
|
55
169
|
return jwt.sign({ userId, email }, this.config.jwtSecret, { expiresIn: '7d' });
|
|
56
170
|
}
|
|
@@ -147,22 +261,5 @@ export class AuthService {
|
|
|
147
261
|
throw new Error('Email domain is not allowed');
|
|
148
262
|
}
|
|
149
263
|
}
|
|
150
|
-
checkPassword(provided) {
|
|
151
|
-
const expected = this.config.testPassword;
|
|
152
|
-
// Reject if no password is configured on the server
|
|
153
|
-
if (!expected)
|
|
154
|
-
return false;
|
|
155
|
-
const a = Buffer.from(provided, 'utf8');
|
|
156
|
-
const b = Buffer.from(expected, 'utf8');
|
|
157
|
-
// Pad both buffers to the same length so timingSafeEqual runs unconditionally —
|
|
158
|
-
// returning early on length mismatch would leak the expected password's length via timing.
|
|
159
|
-
const len = Math.max(a.length, b.length);
|
|
160
|
-
const aPadded = Buffer.alloc(len);
|
|
161
|
-
const bPadded = Buffer.alloc(len);
|
|
162
|
-
a.copy(aPadded);
|
|
163
|
-
b.copy(bPadded);
|
|
164
|
-
const equal = timingSafeEqual(aPadded, bPadded);
|
|
165
|
-
return equal && a.length === b.length;
|
|
166
|
-
}
|
|
167
264
|
}
|
|
168
265
|
//# sourceMappingURL=auth.service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type express from 'express';
|
|
2
|
+
import type { AuthProviderPlugin } from './auth.routes.js';
|
|
3
|
+
import type { AuthService } from './auth.service.js';
|
|
4
|
+
export interface OidcAuthProviderOptions {
|
|
5
|
+
/** Issuer base URL; `<issuer>/.well-known/openid-configuration` must exist. */
|
|
6
|
+
issuerUrl: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
clientSecret: string;
|
|
9
|
+
/** Space-separated; must include `openid` (and `email` for most providers). */
|
|
10
|
+
scopes: string;
|
|
11
|
+
/** Login-button label shown by the login screen. */
|
|
12
|
+
label: string;
|
|
13
|
+
publicBackendUrl: string;
|
|
14
|
+
publicFrontendUrl: string;
|
|
15
|
+
/** See MicrosoftAuthDeps.cookieSecure — scheme-derived, not NODE_ENV. */
|
|
16
|
+
cookieSecure: boolean;
|
|
17
|
+
/** Injectable for tests; defaults to global fetch. */
|
|
18
|
+
fetchImpl?: typeof fetch;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generic OIDC single sign-on as an {@link AuthProviderPlugin} — works with
|
|
22
|
+
* any spec-compliant provider (Entra, Okta, Auth0, Keycloak, Google, …),
|
|
23
|
+
* configured purely from the environment (see CoreConfig `oidc*`).
|
|
24
|
+
*
|
|
25
|
+
* Flow: authorization-code with PKCE (S256) as a confidential client.
|
|
26
|
+
* Identity claims come from the `userinfo` endpoint called with the freshly
|
|
27
|
+
* exchanged access token — the token arrived over TLS directly from the
|
|
28
|
+
* issuer's token endpoint, so no local id_token signature verification (and
|
|
29
|
+
* no JWKS handling) is needed to trust it.
|
|
30
|
+
*
|
|
31
|
+
* Register `<PUBLIC_BACKEND_URL>/api/auth/oidc/callback` as the redirect URI
|
|
32
|
+
* with the provider.
|
|
33
|
+
*/
|
|
34
|
+
export declare class OidcAuthProvider implements AuthProviderPlugin {
|
|
35
|
+
private readonly opts;
|
|
36
|
+
readonly key = "oidc";
|
|
37
|
+
readonly label: string;
|
|
38
|
+
readonly startPath = "/api/auth/oidc/login";
|
|
39
|
+
private readonly fetchImpl;
|
|
40
|
+
private discovery;
|
|
41
|
+
constructor(opts: OidcAuthProviderOptions);
|
|
42
|
+
/**
|
|
43
|
+
* Fetch + cache the issuer's discovery document. Resolved lazily (not at
|
|
44
|
+
* boot) so a temporarily unreachable issuer delays the first login instead
|
|
45
|
+
* of failing the whole deployment; a failed attempt is not cached.
|
|
46
|
+
*/
|
|
47
|
+
private discover;
|
|
48
|
+
private redirectUri;
|
|
49
|
+
mountRoutes(router: express.Router, authService: AuthService): void;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=oidc-auth-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-auth-provider.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/oidc-auth-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAcrD,MAAM,WAAW,uBAAuB;IACtC,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,YAAY,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAoBD;;;;;;;;;;;;;GAaG;AACH,qBAAa,gBAAiB,YAAW,kBAAkB;IAQ7C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPjC,QAAQ,CAAC,GAAG,UAAU;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,0BAA0B;IAE5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,SAAS,CAA8B;gBAElB,IAAI,EAAE,uBAAuB;IAK1D;;;;OAIG;YACW,QAAQ;IAiBtB,OAAO,CAAC,WAAW;IAInB,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI;CAyHpE"}
|