@restingowlorg/owlauth 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +307 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +16 -0
- package/dist/core/auth.manager.d.ts +6 -0
- package/dist/core/auth.manager.js +21 -0
- package/dist/core/auth.service.init.d.ts +2 -0
- package/dist/core/auth.service.init.js +26 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -0
- package/dist/infra/databases/mongo/adapter.d.ts +6 -0
- package/dist/infra/databases/mongo/adapter.js +24 -0
- package/dist/infra/databases/mongo/db.d.ts +5 -0
- package/dist/infra/databases/mongo/db.js +43 -0
- package/dist/infra/databases/mongo/mongo.d.ts +5 -0
- package/dist/infra/databases/mongo/mongo.js +11 -0
- package/dist/infra/databases/postgresql/adapter.d.ts +6 -0
- package/dist/infra/databases/postgresql/adapter.js +26 -0
- package/dist/infra/databases/postgresql/db.d.ts +5 -0
- package/dist/infra/databases/postgresql/db.js +50 -0
- package/dist/infra/databases/postgresql/helpers.d.ts +8 -0
- package/dist/infra/databases/postgresql/helpers.js +55 -0
- package/dist/infra/databases/postgresql/postgres.d.ts +5 -0
- package/dist/infra/databases/postgresql/postgres.js +11 -0
- package/dist/infra/databases/postgresql/schema.d.ts +6 -0
- package/dist/infra/databases/postgresql/schema.js +9 -0
- package/dist/infra/security/bcrypt.adapter.d.ts +9 -0
- package/dist/infra/security/bcrypt.adapter.js +62 -0
- package/dist/infra/security/bcrypt.adapter.test.d.ts +1 -0
- package/dist/infra/security/bcrypt.adapter.test.js +67 -0
- package/dist/infra/security/pwned-passwords.d.ts +5 -0
- package/dist/infra/security/pwned-passwords.js +45 -0
- package/dist/infra/security/pwned-passwords.test.d.ts +1 -0
- package/dist/infra/security/pwned-passwords.test.js +62 -0
- package/dist/infra/security/security-audit-logger.d.ts +11 -0
- package/dist/infra/security/security-audit-logger.js +90 -0
- package/dist/repositories/contracts.d.ts +21 -0
- package/dist/repositories/contracts.js +2 -0
- package/dist/repositories/mongo/magicLink.repo.d.ts +26 -0
- package/dist/repositories/mongo/magicLink.repo.js +106 -0
- package/dist/repositories/mongo/user.repo.d.ts +16 -0
- package/dist/repositories/mongo/user.repo.js +84 -0
- package/dist/repositories/postgresql/magic.link.repo.d.ts +21 -0
- package/dist/repositories/postgresql/magic.link.repo.js +97 -0
- package/dist/repositories/postgresql/user.repo.d.ts +14 -0
- package/dist/repositories/postgresql/user.repo.js +50 -0
- package/dist/services/auth.service.d.ts +22 -0
- package/dist/services/auth.service.js +362 -0
- package/dist/services/auth.service.test.d.ts +1 -0
- package/dist/services/auth.service.test.js +297 -0
- package/dist/services/magic-link.service.d.ts +22 -0
- package/dist/services/magic-link.service.js +196 -0
- package/dist/services/magic-link.service.test.d.ts +1 -0
- package/dist/services/magic-link.service.test.js +230 -0
- package/dist/strategies/CredentialsStrategy.d.ts +4 -0
- package/dist/strategies/CredentialsStrategy.js +32 -0
- package/dist/strategies/CredentialsStrategy.test.d.ts +1 -0
- package/dist/strategies/CredentialsStrategy.test.js +29 -0
- package/dist/strategies/MagicLinkStrategy.d.ts +4 -0
- package/dist/strategies/MagicLinkStrategy.js +21 -0
- package/dist/strategies/MagicLinkStrategy.test.d.ts +1 -0
- package/dist/strategies/MagicLinkStrategy.test.js +38 -0
- package/dist/types/index.d.ts +224 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/check-blocked-passwords.d.ts +1 -0
- package/dist/utils/check-blocked-passwords.js +10 -0
- package/dist/utils/check-blocked-passwords.test.d.ts +1 -0
- package/dist/utils/check-blocked-passwords.test.js +27 -0
- package/package.json +102 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateSchema = validateSchema;
|
|
4
|
+
exports.validateTable = validateTable;
|
|
5
|
+
exports.validateColumns = validateColumns;
|
|
6
|
+
exports.validateForeignKey = validateForeignKey;
|
|
7
|
+
/**
|
|
8
|
+
* Validation Helpers for PostgreSQL
|
|
9
|
+
*/
|
|
10
|
+
async function validateSchema(pool, schema) {
|
|
11
|
+
const res = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = $1`, [schema]);
|
|
12
|
+
if (!res.rowCount)
|
|
13
|
+
throw new Error(`[Auth:validateSchema] Schema '${schema}' does not exist`);
|
|
14
|
+
}
|
|
15
|
+
async function validateTable(pool, qualifiedTable) {
|
|
16
|
+
const res = await pool.query(`SELECT to_regclass($1) AS table_exists`, [qualifiedTable]);
|
|
17
|
+
if (!res.rows[0].table_exists)
|
|
18
|
+
throw new Error(`[Auth:validateTable] Table '${qualifiedTable}' does not exist`);
|
|
19
|
+
}
|
|
20
|
+
async function validateColumns(pool, schema, table, requiredColumns) {
|
|
21
|
+
const res = await pool.query(`
|
|
22
|
+
SELECT column_name
|
|
23
|
+
FROM information_schema.columns
|
|
24
|
+
WHERE table_name = $1 AND table_schema = $2
|
|
25
|
+
`, [table, schema]);
|
|
26
|
+
const existingColumns = res.rows.map((r) => r.column_name);
|
|
27
|
+
for (const col of requiredColumns) {
|
|
28
|
+
if (!existingColumns.includes(col)) {
|
|
29
|
+
throw new Error(`[Auth:validateColumns] Table '${schema}.${table}' missing required column '${col}'`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function validateForeignKey(pool, schema, table, refSchema, refTable, col, refCol) {
|
|
34
|
+
const res = await pool.query(`
|
|
35
|
+
SELECT
|
|
36
|
+
ccu.table_name AS referenced_table,
|
|
37
|
+
ccu.column_name AS referenced_column,
|
|
38
|
+
ccu.table_schema AS referenced_schema
|
|
39
|
+
FROM information_schema.table_constraints AS tc
|
|
40
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
41
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
42
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
43
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
44
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
45
|
+
AND tc.table_name = $1
|
|
46
|
+
AND tc.table_schema = $2
|
|
47
|
+
AND kcu.column_name = $3
|
|
48
|
+
`, [table, schema, col]);
|
|
49
|
+
const valid = res.rows.some((r) => r.referenced_table === refTable &&
|
|
50
|
+
r.referenced_schema === refSchema &&
|
|
51
|
+
r.referenced_column === refCol);
|
|
52
|
+
if (!valid) {
|
|
53
|
+
throw new Error(`[Auth:validateForeignKey] Table '${schema}.${table}' must have a foreign key '${col}' referencing '${refSchema}.${refTable}.${refCol}'`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { PostgresAdapter } from "./adapter";
|
|
2
|
+
export { initPostgres } from "./db";
|
|
3
|
+
export { PostgresUserRepository } from "../../../repositories/postgresql/user.repo";
|
|
4
|
+
export { PostgresMagicLinkRepository } from "../../../repositories/postgresql/magic.link.repo";
|
|
5
|
+
export { MagicLinkRow, TableColumn, ColumnRow, FKRow, TableExistsRow, ColumnInfoRow, PrimaryKeyRow } from "../../../types/index";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresMagicLinkRepository = exports.PostgresUserRepository = exports.initPostgres = exports.PostgresAdapter = void 0;
|
|
4
|
+
var adapter_1 = require("./adapter");
|
|
5
|
+
Object.defineProperty(exports, "PostgresAdapter", { enumerable: true, get: function () { return adapter_1.PostgresAdapter; } });
|
|
6
|
+
var db_1 = require("./db");
|
|
7
|
+
Object.defineProperty(exports, "initPostgres", { enumerable: true, get: function () { return db_1.initPostgres; } });
|
|
8
|
+
var user_repo_1 = require("../../../repositories/postgresql/user.repo");
|
|
9
|
+
Object.defineProperty(exports, "PostgresUserRepository", { enumerable: true, get: function () { return user_repo_1.PostgresUserRepository; } });
|
|
10
|
+
var magic_link_repo_1 = require("../../../repositories/postgresql/magic.link.repo");
|
|
11
|
+
Object.defineProperty(exports, "PostgresMagicLinkRepository", { enumerable: true, get: function () { return magic_link_repo_1.PostgresMagicLinkRepository; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresMagicLinkSchema = exports.PostgresUserSchema = void 0;
|
|
4
|
+
exports.PostgresUserSchema = {
|
|
5
|
+
requiredColumns: ["id", "email", "username", "password"]
|
|
6
|
+
};
|
|
7
|
+
exports.PostgresMagicLinkSchema = {
|
|
8
|
+
requiredColumns: ["id", "user_id", "token", "expires_at", "used_at", "created_at"]
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ICryptoAdapter } from "../../types";
|
|
2
|
+
export declare class BcryptAdapter implements ICryptoAdapter {
|
|
3
|
+
private readonly SALT_ROUNDS;
|
|
4
|
+
hashPassword(password: string): Promise<string>;
|
|
5
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
6
|
+
generateToken(length?: number): string;
|
|
7
|
+
hashToken(token: string): Promise<string>;
|
|
8
|
+
verifyToken(token: string, hash: string): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.BcryptAdapter = void 0;
|
|
37
|
+
const bcrypt = __importStar(require("bcryptjs"));
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const config_1 = require("../../config");
|
|
40
|
+
class BcryptAdapter {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.SALT_ROUNDS = config_1.SECURITY_CONFIG.SALT_ROUNDS;
|
|
43
|
+
}
|
|
44
|
+
// ---------------- Password Helpers ----------------
|
|
45
|
+
async hashPassword(password) {
|
|
46
|
+
return bcrypt.hash(password, this.SALT_ROUNDS);
|
|
47
|
+
}
|
|
48
|
+
async verifyPassword(password, hash) {
|
|
49
|
+
return bcrypt.compare(password, hash);
|
|
50
|
+
}
|
|
51
|
+
// ---------------- Magic Link Helpers ----------------
|
|
52
|
+
generateToken(length = 32) {
|
|
53
|
+
return crypto.randomBytes(length).toString("hex");
|
|
54
|
+
}
|
|
55
|
+
async hashToken(token) {
|
|
56
|
+
return bcrypt.hash(token, this.SALT_ROUNDS);
|
|
57
|
+
}
|
|
58
|
+
async verifyToken(token, hash) {
|
|
59
|
+
return bcrypt.compare(token, hash);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.BcryptAdapter = BcryptAdapter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const bcrypt_adapter_1 = require("./bcrypt.adapter");
|
|
37
|
+
const bcrypt = __importStar(require("bcryptjs"));
|
|
38
|
+
jest.mock("bcryptjs", () => ({
|
|
39
|
+
hash: jest.fn(),
|
|
40
|
+
compare: jest.fn()
|
|
41
|
+
}));
|
|
42
|
+
describe("BcryptAdapter", () => {
|
|
43
|
+
let adapter;
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
adapter = new bcrypt_adapter_1.BcryptAdapter();
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
it("should correctly hash a password", async () => {
|
|
49
|
+
bcrypt.hash.mockResolvedValue("hashed_pwd");
|
|
50
|
+
const result = await adapter.hashPassword("my_password");
|
|
51
|
+
expect(result).toBe("hashed_pwd");
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
53
|
+
expect(bcrypt.hash).toHaveBeenCalledWith("my_password", 10);
|
|
54
|
+
});
|
|
55
|
+
it("should correctly verify a password", async () => {
|
|
56
|
+
bcrypt.compare.mockResolvedValue(true);
|
|
57
|
+
const result = await adapter.verifyPassword("my_password", "hashed_pwd");
|
|
58
|
+
expect(result).toBe(true);
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
60
|
+
expect(bcrypt.compare).toHaveBeenCalledWith("my_password", "hashed_pwd");
|
|
61
|
+
});
|
|
62
|
+
it("should return false if verification fails", async () => {
|
|
63
|
+
bcrypt.compare.mockResolvedValue(false);
|
|
64
|
+
const result = await adapter.verifyPassword("wrong_password", "hashed_pwd");
|
|
65
|
+
expect(result).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isBreachedPassword = isBreachedPassword;
|
|
4
|
+
const js_sha1_1 = require("js-sha1");
|
|
5
|
+
const security_audit_logger_1 = require("./security-audit-logger");
|
|
6
|
+
const config_1 = require("../../config");
|
|
7
|
+
async function isBreachedPassword(password) {
|
|
8
|
+
const hash = (0, js_sha1_1.sha1)(password).toUpperCase();
|
|
9
|
+
const prefix = hash.slice(0, 5);
|
|
10
|
+
const suffix = hash.slice(5);
|
|
11
|
+
let timeout;
|
|
12
|
+
try {
|
|
13
|
+
// 3-second timeout fallback to prevent hanging requests
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
timeout = setTimeout(() => controller.abort(), 3000);
|
|
16
|
+
timeout.unref(); // Allow process to exit even if timer is active
|
|
17
|
+
const response = await fetch(`${config_1.SECURITY_CONFIG.PWNED_API_URL}/${prefix}`, {
|
|
18
|
+
signal: controller.signal
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const error = new Error(`PwnedPasswords API returned status ${response.status}`);
|
|
22
|
+
security_audit_logger_1.auditLogger.warn(`${error.message}. Fallback to caller handling.`);
|
|
23
|
+
return { detected: false, error };
|
|
24
|
+
}
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
const lines = text.split("\n");
|
|
27
|
+
const detected = lines.some((line) => line.split(":")[0] === suffix);
|
|
28
|
+
return { detected };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
32
|
+
if (err.name === "AbortError") {
|
|
33
|
+
security_audit_logger_1.auditLogger.warn("PwnedPasswords check timed out. Fallback to caller handling.");
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
security_audit_logger_1.auditLogger.warn(`Failed to check breached password. Error: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
return { detected: false, error: err };
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (timeout) {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const pwned_passwords_1 = require("./pwned-passwords");
|
|
4
|
+
const security_audit_logger_1 = require("./security-audit-logger");
|
|
5
|
+
const js_sha1_1 = require("js-sha1");
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
jest.mock("./security-audit-logger", () => ({
|
|
8
|
+
auditLogger: {
|
|
9
|
+
warn: jest.fn()
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("js-sha1", () => ({
|
|
13
|
+
sha1: jest.fn()
|
|
14
|
+
}));
|
|
15
|
+
describe("isBreachedPassword", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
global.fetch = jest.fn();
|
|
19
|
+
js_sha1_1.sha1.mockReturnValue("DEFAULT_HASH_FOR_TESTS");
|
|
20
|
+
});
|
|
21
|
+
it("should return detected true if password suffix is found in the API response", async () => {
|
|
22
|
+
const fakeHash = "0123456789ABCDEF0123456789ABCDEF01234567";
|
|
23
|
+
js_sha1_1.sha1.mockReturnValue(fakeHash);
|
|
24
|
+
const suffix = "56789ABCDEF0123456789ABCDEF01234567";
|
|
25
|
+
const mockResponse = `${suffix}:10\nOTHERHASH:5\n`;
|
|
26
|
+
global.fetch.mockResolvedValue({
|
|
27
|
+
ok: true,
|
|
28
|
+
text: () => Promise.resolve(mockResponse)
|
|
29
|
+
});
|
|
30
|
+
const result = await (0, pwned_passwords_1.isBreachedPassword)("any_password");
|
|
31
|
+
expect(result.detected).toBe(true);
|
|
32
|
+
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining("01234"), expect.any(Object));
|
|
33
|
+
});
|
|
34
|
+
it("should return detected false if password suffix is NOT found", async () => {
|
|
35
|
+
js_sha1_1.sha1.mockReturnValue("0123456789ABCDEF0123456789ABCDEF01234567");
|
|
36
|
+
global.fetch.mockResolvedValue({
|
|
37
|
+
ok: true,
|
|
38
|
+
text: () => Promise.resolve("OTHERHASH:10\n")
|
|
39
|
+
});
|
|
40
|
+
const result = await (0, pwned_passwords_1.isBreachedPassword)("secure_password");
|
|
41
|
+
expect(result.detected).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it("should handle API error and return error object", async () => {
|
|
44
|
+
global.fetch.mockResolvedValue({
|
|
45
|
+
ok: false,
|
|
46
|
+
status: 500
|
|
47
|
+
});
|
|
48
|
+
const result = await (0, pwned_passwords_1.isBreachedPassword)("test");
|
|
49
|
+
expect(result.detected).toBe(false);
|
|
50
|
+
expect(result.error).toBeDefined();
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
52
|
+
expect(security_audit_logger_1.auditLogger.warn).toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
it("should handle fetch timeout/error", async () => {
|
|
55
|
+
global.fetch.mockRejectedValue(new Error("Network Error"));
|
|
56
|
+
const result = await (0, pwned_passwords_1.isBreachedPassword)("test");
|
|
57
|
+
expect(result.detected).toBe(false);
|
|
58
|
+
expect(result.error).toBeDefined();
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
60
|
+
expect(security_audit_logger_1.auditLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Failed to check breached password"));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IAuditLogger, SecurityEvent } from "../../types/index";
|
|
2
|
+
export declare class SecurityAuditLogger implements IAuditLogger {
|
|
3
|
+
private readonly prefix;
|
|
4
|
+
private customMaskingKeys;
|
|
5
|
+
setCustomMaskingKeys(keys: string[]): void;
|
|
6
|
+
info(message: string, context?: unknown, correlationId?: string): void;
|
|
7
|
+
warn(message: string, context?: unknown, correlationId?: string): void;
|
|
8
|
+
error(message: string, error: unknown, context?: unknown, correlationId?: string): void;
|
|
9
|
+
audit(event: SecurityEvent): void;
|
|
10
|
+
}
|
|
11
|
+
export declare const auditLogger: SecurityAuditLogger;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditLogger = exports.SecurityAuditLogger = void 0;
|
|
4
|
+
/* eslint-disable no-console */
|
|
5
|
+
const config_1 = require("../../config");
|
|
6
|
+
/**
|
|
7
|
+
* Robustly masks sensitive data in objects/arrays.
|
|
8
|
+
* Handles circular references and recursively masks nested objects.
|
|
9
|
+
*/
|
|
10
|
+
function maskSensitiveData(data, customKeys, seen = new WeakSet()) {
|
|
11
|
+
if (!data || typeof data !== "object")
|
|
12
|
+
return data;
|
|
13
|
+
const dataObj = data;
|
|
14
|
+
if (seen.has(dataObj))
|
|
15
|
+
return "[Circular]";
|
|
16
|
+
// Only add non-primitive objects to the seen set
|
|
17
|
+
seen.add(dataObj);
|
|
18
|
+
if (Array.isArray(data)) {
|
|
19
|
+
const masked = [...data];
|
|
20
|
+
for (let i = 0; i < masked.length; i++) {
|
|
21
|
+
masked[i] = maskSensitiveData(masked[i], customKeys, seen);
|
|
22
|
+
}
|
|
23
|
+
return masked;
|
|
24
|
+
}
|
|
25
|
+
const masked = data instanceof Error
|
|
26
|
+
? {
|
|
27
|
+
name: data.name,
|
|
28
|
+
message: data.message,
|
|
29
|
+
stack: data.stack,
|
|
30
|
+
...dataObj
|
|
31
|
+
}
|
|
32
|
+
: { ...dataObj };
|
|
33
|
+
const allSensitiveKeys = [
|
|
34
|
+
...config_1.SECURITY_CONFIG.SENSITIVE_KEYS,
|
|
35
|
+
...customKeys
|
|
36
|
+
].map((k) => k.toLowerCase());
|
|
37
|
+
// Mask sensitive data
|
|
38
|
+
for (const key in masked) {
|
|
39
|
+
const value = masked[key];
|
|
40
|
+
if (allSensitiveKeys.includes(key.toLowerCase())) {
|
|
41
|
+
masked[key] = "********";
|
|
42
|
+
}
|
|
43
|
+
else if (value && typeof value === "object") {
|
|
44
|
+
masked[key] = maskSensitiveData(value, customKeys, seen);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Restore Error object structure
|
|
48
|
+
if (data instanceof Error) {
|
|
49
|
+
const err = new Error(masked.message);
|
|
50
|
+
err.name = masked.name;
|
|
51
|
+
err.stack = masked.stack;
|
|
52
|
+
for (const key in masked) {
|
|
53
|
+
if (!["name", "message", "stack"].includes(key)) {
|
|
54
|
+
err[key] = masked[key];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return err;
|
|
58
|
+
}
|
|
59
|
+
return masked;
|
|
60
|
+
}
|
|
61
|
+
class SecurityAuditLogger {
|
|
62
|
+
constructor() {
|
|
63
|
+
this.prefix = config_1.SECURITY_CONFIG.LOGGER_PREFIX;
|
|
64
|
+
this.customMaskingKeys = [];
|
|
65
|
+
}
|
|
66
|
+
setCustomMaskingKeys(keys) {
|
|
67
|
+
this.customMaskingKeys = keys;
|
|
68
|
+
}
|
|
69
|
+
info(message, context, correlationId) {
|
|
70
|
+
console.info(`${this.prefix} [INFO]${correlationId ? ` [${correlationId}]` : ""} - ${message}`, context ? maskSensitiveData(context, this.customMaskingKeys) : "");
|
|
71
|
+
}
|
|
72
|
+
warn(message, context, correlationId) {
|
|
73
|
+
console.warn(`${this.prefix} [WARN]${correlationId ? ` [${correlationId}]` : ""} - ${message}`, context ? maskSensitiveData(context, this.customMaskingKeys) : "");
|
|
74
|
+
}
|
|
75
|
+
error(message, error, context, correlationId) {
|
|
76
|
+
console.error(`${this.prefix} [ERROR]${correlationId ? ` [${correlationId}]` : ""} - ${message}`, maskSensitiveData(error, this.customMaskingKeys), context ? maskSensitiveData(context, this.customMaskingKeys) : "");
|
|
77
|
+
}
|
|
78
|
+
audit(event) {
|
|
79
|
+
const { type, userId, email, correlationId, ...rest } = event;
|
|
80
|
+
console.log(`${this.prefix} [AUDIT]${correlationId ? ` [${correlationId}]` : ""} - ${type}`, maskSensitiveData({
|
|
81
|
+
userId,
|
|
82
|
+
email,
|
|
83
|
+
...rest,
|
|
84
|
+
timestamp: new Date().toISOString()
|
|
85
|
+
}, this.customMaskingKeys));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.SecurityAuditLogger = SecurityAuditLogger;
|
|
89
|
+
exports.auditLogger = new SecurityAuditLogger();
|
|
90
|
+
/* eslint-enable no-console */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { User, MagicLinkRow, MagicLinkToken, CreateUserInput, SafeUser } from "../types";
|
|
2
|
+
export interface UserRepository {
|
|
3
|
+
create(input: CreateUserInput): Promise<SafeUser>;
|
|
4
|
+
findByEmail(email: string): Promise<User | null>;
|
|
5
|
+
findById(id: string | number): Promise<User | null>;
|
|
6
|
+
findByUsername?(username: string): Promise<User | null>;
|
|
7
|
+
updatePassword(userId: string | number, passwordHash: string): Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
export interface MagicLinkRepository {
|
|
10
|
+
create(data: {
|
|
11
|
+
userId: string | number;
|
|
12
|
+
tokenHash: string;
|
|
13
|
+
expiresAt: Date;
|
|
14
|
+
}): Promise<MagicLinkToken>;
|
|
15
|
+
findAll(): Promise<MagicLinkRow[]>;
|
|
16
|
+
findByTokenHash(tokenHash: string): Promise<MagicLinkToken | null>;
|
|
17
|
+
findById(id: string | number): Promise<MagicLinkToken | null>;
|
|
18
|
+
consume(id: string | number): Promise<boolean>;
|
|
19
|
+
invalidateByUserId(userId: string | number): Promise<boolean>;
|
|
20
|
+
deleteByUserId(userId: string | number): Promise<boolean>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Collection } from "mongodb";
|
|
2
|
+
import { MagicLinkToken, MagicLinkRow, IMongoMagicLinkDoc } from "../../types";
|
|
3
|
+
import { MagicLinkRepository } from "../contracts";
|
|
4
|
+
export declare class MongoMagicLinkRepo implements MagicLinkRepository {
|
|
5
|
+
private collection;
|
|
6
|
+
constructor(collection: Collection<IMongoMagicLinkDoc>);
|
|
7
|
+
/** Create a new magic link token */
|
|
8
|
+
create(token: {
|
|
9
|
+
userId: string | number;
|
|
10
|
+
tokenHash: string;
|
|
11
|
+
expiresAt: Date;
|
|
12
|
+
usedAt?: Date | null;
|
|
13
|
+
}): Promise<MagicLinkToken>;
|
|
14
|
+
/** Find token by its hash */
|
|
15
|
+
findByTokenHash(tokenHash: string): Promise<MagicLinkToken | null>;
|
|
16
|
+
/** Find token by its ID */
|
|
17
|
+
findById(id: string | number): Promise<MagicLinkToken | null>;
|
|
18
|
+
/** Mark a token as used */
|
|
19
|
+
consume(id: string | number): Promise<boolean>;
|
|
20
|
+
/** Delete existing tokens for a user */
|
|
21
|
+
deleteByUserId(userId: string | number): Promise<boolean>;
|
|
22
|
+
/** Invalidate existing tokens for a user */
|
|
23
|
+
invalidateByUserId(userId: string | number): Promise<boolean>;
|
|
24
|
+
/** Return all active tokens as MagicLinkRow (for contracts) */
|
|
25
|
+
findAll(): Promise<MagicLinkRow[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoMagicLinkRepo = void 0;
|
|
4
|
+
const mongodb_1 = require("mongodb");
|
|
5
|
+
class MongoMagicLinkRepo {
|
|
6
|
+
constructor(collection) {
|
|
7
|
+
this.collection = collection;
|
|
8
|
+
}
|
|
9
|
+
/** Create a new magic link token */
|
|
10
|
+
async create(token) {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const now = new Date();
|
|
13
|
+
// Build the doc
|
|
14
|
+
const doc = {
|
|
15
|
+
user_id: new mongodb_1.ObjectId(token.userId.toString()),
|
|
16
|
+
token: token.tokenHash,
|
|
17
|
+
expires_at: token.expiresAt,
|
|
18
|
+
used_at: (_a = token.usedAt) !== null && _a !== void 0 ? _a : null,
|
|
19
|
+
created_at: now,
|
|
20
|
+
updated_at: now
|
|
21
|
+
};
|
|
22
|
+
const result = await this.collection.insertOne(doc);
|
|
23
|
+
return {
|
|
24
|
+
id: result.insertedId.toString(),
|
|
25
|
+
userId: token.userId,
|
|
26
|
+
tokenHash: token.tokenHash,
|
|
27
|
+
expiresAt: token.expiresAt,
|
|
28
|
+
usedAt: (_b = token.usedAt) !== null && _b !== void 0 ? _b : null,
|
|
29
|
+
createdAt: now
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Find token by its hash */
|
|
33
|
+
async findByTokenHash(tokenHash) {
|
|
34
|
+
var _a;
|
|
35
|
+
const doc = await this.collection.findOne({ token: tokenHash });
|
|
36
|
+
if (!doc)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
id: doc._id.toString(),
|
|
40
|
+
userId: doc.user_id.toString(),
|
|
41
|
+
tokenHash: doc.token,
|
|
42
|
+
expiresAt: doc.expires_at,
|
|
43
|
+
usedAt: (_a = doc.used_at) !== null && _a !== void 0 ? _a : null,
|
|
44
|
+
createdAt: doc.created_at
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Find token by its ID */
|
|
48
|
+
async findById(id) {
|
|
49
|
+
var _a;
|
|
50
|
+
let objectId;
|
|
51
|
+
try {
|
|
52
|
+
objectId = new mongodb_1.ObjectId(id.toString());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const doc = await this.collection.findOne({ _id: objectId });
|
|
58
|
+
if (!doc)
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
id: doc._id.toString(),
|
|
62
|
+
userId: doc.user_id.toString(),
|
|
63
|
+
tokenHash: doc.token,
|
|
64
|
+
expiresAt: doc.expires_at,
|
|
65
|
+
usedAt: (_a = doc.used_at) !== null && _a !== void 0 ? _a : null,
|
|
66
|
+
createdAt: doc.created_at
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Mark a token as used */
|
|
70
|
+
async consume(id) {
|
|
71
|
+
const result = await this.collection.updateOne({ _id: new mongodb_1.ObjectId(id.toString()), used_at: null }, { $set: { used_at: new Date(), updated_at: new Date() } });
|
|
72
|
+
return result.modifiedCount > 0;
|
|
73
|
+
}
|
|
74
|
+
/** Delete existing tokens for a user */
|
|
75
|
+
async deleteByUserId(userId) {
|
|
76
|
+
const result = await this.collection.deleteMany({ user_id: new mongodb_1.ObjectId(userId.toString()) });
|
|
77
|
+
return result.deletedCount > 0;
|
|
78
|
+
}
|
|
79
|
+
/** Invalidate existing tokens for a user */
|
|
80
|
+
async invalidateByUserId(userId) {
|
|
81
|
+
const filter = { user_id: new mongodb_1.ObjectId(userId.toString()), used_at: null };
|
|
82
|
+
await this.collection.updateMany(filter, {
|
|
83
|
+
$set: { used_at: new Date(), updated_at: new Date() }
|
|
84
|
+
});
|
|
85
|
+
// Confirm no active tokens remain
|
|
86
|
+
const remainingCount = await this.collection.countDocuments(filter);
|
|
87
|
+
return remainingCount === 0;
|
|
88
|
+
}
|
|
89
|
+
/** Return all active tokens as MagicLinkRow (for contracts) */
|
|
90
|
+
async findAll() {
|
|
91
|
+
const now = new Date();
|
|
92
|
+
const docs = await this.collection.find({ expires_at: { $gt: now }, used_at: null }).toArray();
|
|
93
|
+
return docs.map((doc) => {
|
|
94
|
+
var _a;
|
|
95
|
+
return ({
|
|
96
|
+
id: doc._id.toString(),
|
|
97
|
+
user_id: doc.user_id.toString(),
|
|
98
|
+
token: doc.token,
|
|
99
|
+
expires_at: doc.expires_at,
|
|
100
|
+
used_at: (_a = doc.used_at) !== null && _a !== void 0 ? _a : null,
|
|
101
|
+
created_at: doc.created_at
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.MongoMagicLinkRepo = MongoMagicLinkRepo;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CreateUserInput, IMongoUserDoc } from "../../types";
|
|
2
|
+
import { User } from "../../types/index";
|
|
3
|
+
import { UserRepository } from "../contracts";
|
|
4
|
+
import { Collection } from "mongodb";
|
|
5
|
+
/**
|
|
6
|
+
* MongoDB implementation of UserRepository
|
|
7
|
+
*/
|
|
8
|
+
export declare class MongoUserRepo implements UserRepository {
|
|
9
|
+
private collection;
|
|
10
|
+
constructor(collection: Collection<IMongoUserDoc>);
|
|
11
|
+
create(input: CreateUserInput): Promise<User>;
|
|
12
|
+
findByEmail(email: string): Promise<User | null>;
|
|
13
|
+
findById(id: string): Promise<User | null>;
|
|
14
|
+
findByUsername(username: string): Promise<User | null>;
|
|
15
|
+
updatePassword(userId: string | number, passwordHash: string): Promise<boolean>;
|
|
16
|
+
}
|