@soapjs/soap-auth 0.3.2 → 0.4.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.
Files changed (77) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/build/__tests__/soap-auth.test.js +94 -0
  3. package/build/errors.d.ts +1 -1
  4. package/build/errors.js +2 -2
  5. package/build/index.d.ts +1 -0
  6. package/build/index.js +1 -0
  7. package/build/services/__tests__/password.service.test.js +25 -18
  8. package/build/services/__tests__/totp.service.test.d.ts +1 -0
  9. package/build/services/__tests__/totp.service.test.js +120 -0
  10. package/build/services/auth-throttle.service.d.ts +2 -2
  11. package/build/services/auth-throttle.service.js +2 -2
  12. package/build/services/index.d.ts +1 -0
  13. package/build/services/index.js +1 -0
  14. package/build/services/password.service.d.ts +7 -5
  15. package/build/services/password.service.js +76 -18
  16. package/build/services/totp.service.d.ts +16 -0
  17. package/build/services/totp.service.js +96 -0
  18. package/build/session/__tests__/session-handler.test.js +22 -14
  19. package/build/session/session-handler.d.ts +1 -0
  20. package/build/session/session-handler.js +39 -6
  21. package/build/soap-auth.d.ts +13 -6
  22. package/build/soap-auth.js +132 -5
  23. package/build/strategies/__tests__/base-auth.strategy.test.d.ts +3 -2
  24. package/build/strategies/__tests__/base-auth.strategy.test.js +1 -0
  25. package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +1 -0
  26. package/build/strategies/__tests__/credential-auth.strategy.test.js +18 -17
  27. package/build/strategies/__tests__/token-auth.strategy.test.d.ts +1 -0
  28. package/build/strategies/__tests__/token-auth.strategy.test.js +1 -0
  29. package/build/strategies/api-key/api-key.strategy.d.ts +4 -4
  30. package/build/strategies/api-key/api-key.strategy.js +3 -2
  31. package/build/strategies/base-auth.strategy.d.ts +5 -4
  32. package/build/strategies/basic/basic.strategy.d.ts +2 -1
  33. package/build/strategies/basic/basic.strategy.js +1 -0
  34. package/build/strategies/credential-auth.strategy.d.ts +7 -7
  35. package/build/strategies/credential-auth.strategy.js +9 -14
  36. package/build/strategies/index.d.ts +1 -0
  37. package/build/strategies/index.js +1 -0
  38. package/build/strategies/jwt/__tests__/jwt.strategy.test.js +2 -2
  39. package/build/strategies/jwt/jwt.strategy.d.ts +3 -1
  40. package/build/strategies/jwt/jwt.strategy.js +35 -6
  41. package/build/strategies/jwt/jwt.tools.js +8 -8
  42. package/build/strategies/local/__tests__/local.strategy.test.js +8 -2
  43. package/build/strategies/local/local.strategy.d.ts +4 -1
  44. package/build/strategies/local/local.strategy.js +81 -0
  45. package/build/strategies/oauth2/__tests__/oauth2.strategy.test.d.ts +1 -0
  46. package/build/strategies/oauth2/__tests__/oauth2.strategy.test.js +239 -0
  47. package/build/strategies/oauth2/hybrid.oauth2.strategy.d.ts +3 -3
  48. package/build/strategies/oauth2/hybrid.oauth2.strategy.js +1 -6
  49. package/build/strategies/oauth2/oauth2.errors.d.ts +1 -0
  50. package/build/strategies/oauth2/oauth2.errors.js +4 -0
  51. package/build/strategies/oauth2/oauth2.strategy.d.ts +6 -4
  52. package/build/strategies/oauth2/oauth2.strategy.js +114 -46
  53. package/build/strategies/oauth2/oauth2.tools.js +2 -2
  54. package/build/strategies/oauth2/oauth2.types.d.ts +2 -2
  55. package/build/strategies/oauth2/providers/__tests__/social-providers.test.d.ts +1 -0
  56. package/build/strategies/oauth2/providers/__tests__/social-providers.test.js +201 -0
  57. package/build/strategies/oauth2/providers/facebook.strategy.d.ts +11 -0
  58. package/build/strategies/oauth2/providers/facebook.strategy.js +58 -0
  59. package/build/strategies/oauth2/providers/github.strategy.d.ts +11 -0
  60. package/build/strategies/oauth2/providers/github.strategy.js +56 -0
  61. package/build/strategies/oauth2/providers/google.strategy.d.ts +11 -0
  62. package/build/strategies/oauth2/providers/google.strategy.js +52 -0
  63. package/build/strategies/oauth2/providers/http-oauth2.strategy.d.ts +16 -0
  64. package/build/strategies/oauth2/providers/http-oauth2.strategy.js +49 -0
  65. package/build/strategies/oauth2/providers/index.d.ts +5 -0
  66. package/build/strategies/oauth2/providers/index.js +21 -0
  67. package/build/strategies/oauth2/providers/provider.types.d.ts +7 -0
  68. package/build/strategies/oauth2/providers/provider.types.js +2 -0
  69. package/build/strategies/token-auth.strategy.d.ts +4 -4
  70. package/build/strategies/token-auth.strategy.js +2 -3
  71. package/build/tools/tools.js +1 -2
  72. package/build/types.d.ts +31 -32
  73. package/build/utils/__tests__/validation.test.d.ts +1 -0
  74. package/build/utils/__tests__/validation.test.js +181 -0
  75. package/build/utils/validation.d.ts +23 -0
  76. package/build/utils/validation.js +139 -0
  77. package/package.json +8 -7
@@ -0,0 +1,20 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run *)",
5
+ "Bash(npx tsc *)",
6
+ "Bash(npm audit *)",
7
+ "Bash(npm uninstall *)",
8
+ "Bash(npm install *)",
9
+ "Bash(echo \"=== SOAP-EXPRESS src ===\" && find /Users/rad/git/soapjs/soap-express/src -type f -name \"*.ts\" | grep -v __tests__ | grep -v \".test.\" | sort && echo \"\" && echo \"=== SOAP-EXPRESS package.json ===\" && cat /Users/rad/git/soapjs/soap-express/package.json)",
10
+ "Bash(echo \"=== SOAP src \\(http + common\\) ===\" && find /Users/rad/git/soapjs/soap/src -type f -name \"*.ts\" | grep -v __tests__ | grep -v \".test.\" | grep -E \"\\(http|common|config\\)\" | sort)",
11
+ "Bash(cat /Users/rad/git/soapjs/soap/package.json | grep '\"version\"' | head -1)",
12
+ "Bash(grep -rln \"AuthStrategy\\\\|\\\\.configure\\(\\\\|\\\\.middleware\\(\\\\|serializeUser\\\\|AuthConfig\\\\b\" src --include=\"*.test.ts\")",
13
+ "Bash(grep -rln \"AuthStrategy\\\\|AuthConfig\" src/**/__tests__)",
14
+ "Bash(npm pack *)",
15
+ "Read(//Users/rad/git/soapjs/**)",
16
+ "Bash(npm view *)",
17
+ "Bash(grep -v \"^$\")"
18
+ ]
19
+ }
20
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const globals_1 = require("@jest/globals");
4
4
  const soap_auth_1 = require("../soap-auth");
5
+ const memory_session_store_1 = require("../session/memory.session-store");
5
6
  describe("SoapAuth", () => {
6
7
  let soapAuth;
7
8
  const mockLogger = { error: globals_1.jest.fn(), info: globals_1.jest.fn() };
@@ -40,3 +41,96 @@ describe("SoapAuth", () => {
40
41
  expect(soapAuth.listStrategies("http")).toEqual(["jwt", "oauth"]);
41
42
  });
42
43
  });
44
+ describe("SoapAuth.create()", () => {
45
+ const mockLogger = { error: globals_1.jest.fn(), info: globals_1.jest.fn(), warn: globals_1.jest.fn() };
46
+ const apiKeyConfig = {
47
+ keyType: "session",
48
+ extractApiKey: (ctx) => ctx?.headers?.["x-api-key"],
49
+ retrieveUserByApiKey: async (_key) => ({ id: "1", email: "test@test.com" }),
50
+ };
51
+ const jwtConfig = {
52
+ accessToken: {
53
+ issuer: {
54
+ secretKey: "test-secret-key-for-tests",
55
+ options: { expiresIn: "1h" },
56
+ },
57
+ verifier: { options: {} },
58
+ },
59
+ routes: {
60
+ login: { path: "/auth/jwt/login", method: "POST" },
61
+ logout: { path: "/auth/jwt/logout", method: "POST" },
62
+ },
63
+ };
64
+ const localConfig = {
65
+ credentials: {
66
+ extractCredentials: (ctx) => ({
67
+ identifier: ctx?.body?.username,
68
+ password: ctx?.body?.password,
69
+ }),
70
+ verifyCredentials: async (_id, _pwd) => true,
71
+ },
72
+ user: { fetchUser: async (_id) => ({ id: "1", email: "u@test.com" }) },
73
+ routes: {
74
+ login: { path: "/auth/login", method: "POST" },
75
+ logout: { path: "/auth/logout", method: "POST" },
76
+ },
77
+ };
78
+ test("registers api-key strategy from config", async () => {
79
+ const auth = await soap_auth_1.SoapAuth.create({
80
+ http: { apiKey: apiKeyConfig },
81
+ logger: mockLogger,
82
+ });
83
+ expect(auth.hasStrategy("api-key", "http")).toBe(true);
84
+ });
85
+ test("registers jwt as standalone HTTP strategy from http.jwt", async () => {
86
+ const auth = await soap_auth_1.SoapAuth.create({
87
+ http: { jwt: jwtConfig },
88
+ logger: mockLogger,
89
+ });
90
+ expect(auth.hasStrategy("jwt", "http")).toBe(true);
91
+ });
92
+ test("registers local strategy from config", async () => {
93
+ const auth = await soap_auth_1.SoapAuth.create({
94
+ http: { local: localConfig },
95
+ logger: mockLogger,
96
+ });
97
+ expect(auth.hasStrategy("local", "http")).toBe(true);
98
+ });
99
+ test("registers multiple HTTP strategies simultaneously", async () => {
100
+ const auth = await soap_auth_1.SoapAuth.create({
101
+ http: { apiKey: apiKeyConfig, jwt: jwtConfig, local: localConfig },
102
+ logger: mockLogger,
103
+ });
104
+ expect(auth.hasStrategy("api-key", "http")).toBe(true);
105
+ expect(auth.hasStrategy("jwt", "http")).toBe(true);
106
+ expect(auth.hasStrategy("local", "http")).toBe(true);
107
+ });
108
+ test("registers custom strategies from config", async () => {
109
+ const customStrategy = { name: "custom", authenticate: globals_1.jest.fn() };
110
+ const auth = await soap_auth_1.SoapAuth.create({
111
+ http: { custom: { myStrategy: customStrategy } },
112
+ logger: mockLogger,
113
+ });
114
+ expect(auth.hasStrategy("myStrategy", "http")).toBe(true);
115
+ });
116
+ test("registers socket JWT strategy from config", async () => {
117
+ const auth = await soap_auth_1.SoapAuth.create({
118
+ socket: { jwt: jwtConfig },
119
+ logger: mockLogger,
120
+ });
121
+ expect(auth.hasStrategy("jwt", "socket")).toBe(true);
122
+ });
123
+ test("returns empty SoapAuth when no strategy configs provided", async () => {
124
+ const auth = await soap_auth_1.SoapAuth.create({ logger: mockLogger });
125
+ expect(auth.listStrategies("http")).toEqual([]);
126
+ });
127
+ test("uses session handler when session config provided", async () => {
128
+ const store = new memory_session_store_1.MemorySessionStore();
129
+ const auth = await soap_auth_1.SoapAuth.create({
130
+ session: { secret: "test-secret", store, getSessionId: () => "sid" },
131
+ http: { local: localConfig },
132
+ logger: mockLogger,
133
+ });
134
+ expect(auth.hasStrategy("local", "http")).toBe(true);
135
+ });
136
+ });
package/build/errors.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare class UserNotFoundError extends Error {
20
20
  constructor();
21
21
  }
22
22
  export declare class InvalidCredentialsError extends Error {
23
- constructor();
23
+ constructor(message?: string);
24
24
  }
25
25
  export declare class MissingTokenError extends Error {
26
26
  readonly tokenType: "Access" | "Refresh";
package/build/errors.js CHANGED
@@ -50,8 +50,8 @@ class UserNotFoundError extends Error {
50
50
  }
51
51
  exports.UserNotFoundError = UserNotFoundError;
52
52
  class InvalidCredentialsError extends Error {
53
- constructor() {
54
- super("Invalid credentials: Authentication failed.");
53
+ constructor(message) {
54
+ super(`Invalid credentials: ${message || 'Authentication failed.'}`);
55
55
  this.name = "InvalidCredentialsError";
56
56
  }
57
57
  }
package/build/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export * from "./tools";
5
5
  export * from "./errors";
6
6
  export * from "./types";
7
7
  export * from "./soap-auth";
8
+ export * from "./utils/validation";
package/build/index.js CHANGED
@@ -21,3 +21,4 @@ __exportStar(require("./tools"), exports);
21
21
  __exportStar(require("./errors"), exports);
22
22
  __exportStar(require("./types"), exports);
23
23
  __exportStar(require("./soap-auth"), exports);
24
+ __exportStar(require("./utils/validation"), exports);
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const password_service_1 = require("../password.service");
4
- const soap_1 = require("@soapjs/soap");
5
4
  const globals_1 = require("@jest/globals");
6
5
  describe("PasswordService", () => {
7
6
  let service;
@@ -13,10 +12,10 @@ describe("PasswordService", () => {
13
12
  globals_1.jest.clearAllMocks();
14
13
  mockConfig = {
15
14
  validatePassword: globals_1.jest.fn(),
16
- getLastPasswordChange: globals_1.jest.fn(),
15
+ getPasswordPasswordInfo: globals_1.jest.fn(),
17
16
  generateResetToken: globals_1.jest.fn(),
18
- sendResetEmail: globals_1.jest.fn(),
19
- validateResetToken: globals_1.jest.fn(),
17
+ sendPasswordResetEmail: globals_1.jest.fn(),
18
+ validatePasswordResetToken: globals_1.jest.fn(),
20
19
  updatePassword: globals_1.jest.fn(),
21
20
  passwordExpirationDays: 30,
22
21
  };
@@ -25,13 +24,13 @@ describe("PasswordService", () => {
25
24
  it("validatePassword calls config method", async () => {
26
25
  mockConfig.validatePassword.mockReturnValue(true);
27
26
  await expect(service.validatePassword(mockPassword)).resolves.toBe(true);
28
- expect(mockConfig.validatePassword).toHaveBeenCalledWith(mockPassword);
27
+ expect(mockConfig.validatePassword).toHaveBeenCalledWith(mockPassword, undefined);
29
28
  });
30
- it("getLastPasswordChange calls config method", async () => {
31
- const mockDate = new Date();
32
- mockConfig.getLastPasswordChange.mockReturnValue(mockDate);
33
- await expect(service.getLastPasswordChange(mockIdentifier)).resolves.toBe(mockDate);
34
- expect(mockConfig.getLastPasswordChange).toHaveBeenCalledWith(mockIdentifier);
29
+ it("getPasswordPasswordInfo calls config method", async () => {
30
+ const mockPasswordInfo = { type: "default" };
31
+ mockConfig.getPasswordPasswordInfo.mockReturnValue(mockPasswordInfo);
32
+ await expect(service.getPasswordPasswordInfo(mockIdentifier)).resolves.toBe(mockPasswordInfo);
33
+ expect(mockConfig.getPasswordPasswordInfo).toHaveBeenCalledWith(mockIdentifier);
35
34
  });
36
35
  it("generateResetToken calls config method", async () => {
37
36
  mockConfig.generateResetToken.mockResolvedValue("mock-token");
@@ -39,28 +38,36 @@ describe("PasswordService", () => {
39
38
  });
40
39
  it("sendResetEmail calls config method", async () => {
41
40
  await expect(service.sendResetEmail(mockIdentifier, "mock-token")).resolves.not.toThrow();
42
- expect(mockConfig.sendResetEmail).toHaveBeenCalledWith(mockIdentifier, "mock-token");
41
+ expect(mockConfig.sendPasswordResetEmail).toHaveBeenCalledWith(mockIdentifier, "mock-token");
43
42
  });
44
43
  it("validateResetToken calls config method", async () => {
45
- mockConfig.validateResetToken.mockResolvedValue(true);
46
- await expect(service.validateResetToken("mock-token")).resolves.toBe(true);
44
+ mockConfig.validatePasswordResetToken.mockResolvedValue(true);
45
+ await expect(service.validateResetToken("mock-token")).resolves.toBeUndefined();
47
46
  });
48
47
  it("updatePassword calls config method", async () => {
49
48
  await expect(service.updatePassword(mockIdentifier, mockPassword)).resolves.not.toThrow();
50
- expect(mockConfig.updatePassword).toHaveBeenCalledWith(mockIdentifier, mockPassword);
49
+ expect(mockConfig.updatePassword).toHaveBeenCalledWith(mockIdentifier, mockPassword, undefined);
51
50
  });
52
51
  it("isPasswordChangeRequired returns true if password is expired", async () => {
53
52
  const pastDate = new Date(Date.now() - 31 * 86400000);
54
- mockConfig.getLastPasswordChange.mockResolvedValue(pastDate);
53
+ mockConfig.getPasswordPasswordInfo.mockResolvedValue({
54
+ type: "temporary",
55
+ lastChangeDate: pastDate,
56
+ expiresIn: 30 * 86400000
57
+ });
55
58
  await expect(service.isPasswordChangeRequired(mockIdentifier)).resolves.toBe(true);
56
59
  });
57
60
  it("isPasswordChangeRequired returns false if password is within expiration period", async () => {
58
61
  const recentDate = new Date(Date.now() - 15 * 86400000);
59
- mockConfig.getLastPasswordChange.mockResolvedValue(recentDate);
62
+ mockConfig.getPasswordPasswordInfo.mockResolvedValue({
63
+ type: "temporary",
64
+ lastChangeDate: recentDate,
65
+ expiresIn: 30 * 86400000
66
+ });
60
67
  await expect(service.isPasswordChangeRequired(mockIdentifier)).resolves.toBe(false);
61
68
  });
62
- it("isPasswordChangeRequired throws NotImplementedError if getLastPasswordChange is not defined", async () => {
69
+ it("isPasswordChangeRequired returns false if getPasswordPasswordInfo is not defined", async () => {
63
70
  service = new password_service_1.PasswordService({ passwordExpirationDays: 30 }, mockLogger);
64
- await expect(service.isPasswordChangeRequired(mockIdentifier)).rejects.toThrow(soap_1.NotImplementedError);
71
+ await expect(service.isPasswordChangeRequired(mockIdentifier)).resolves.toBe(false);
65
72
  });
66
73
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const totp_service_1 = require("../totp.service");
4
+ describe("TotpService", () => {
5
+ const totp = new totp_service_1.TotpService();
6
+ describe("generateSecret", () => {
7
+ it("returns a non-empty base32 string", () => {
8
+ const secret = totp.generateSecret();
9
+ expect(secret).toMatch(/^[A-Z2-7]+$/);
10
+ expect(secret.length).toBeGreaterThan(0);
11
+ });
12
+ it("returns unique secrets", () => {
13
+ const a = totp.generateSecret();
14
+ const b = totp.generateSecret();
15
+ expect(a).not.toBe(b);
16
+ });
17
+ });
18
+ describe("generateOTP", () => {
19
+ it("returns a 6-digit string for a fixed timestamp", () => {
20
+ const secret = totp.generateSecret();
21
+ const code = totp.generateOTP(secret, 1000000000000);
22
+ expect(code).toMatch(/^\d{6}$/);
23
+ });
24
+ it("returns the same code within the same time step", () => {
25
+ const secret = totp.generateSecret();
26
+ const ts = 1000000000000;
27
+ expect(totp.generateOTP(secret, ts)).toBe(totp.generateOTP(secret, ts + 5000));
28
+ });
29
+ it("returns a different code in a different time step", () => {
30
+ const secret = totp.generateSecret();
31
+ const ts = 1000000000000;
32
+ const code1 = totp.generateOTP(secret, ts);
33
+ const code2 = totp.generateOTP(secret, ts + 30_000);
34
+ expect(code1).not.toBe(code2);
35
+ });
36
+ });
37
+ describe("verifyOTP", () => {
38
+ it("accepts a valid code at the current timestamp", () => {
39
+ const secret = totp.generateSecret();
40
+ const ts = Date.now();
41
+ const code = totp.generateOTP(secret, ts);
42
+ expect(totp.verifyOTP(secret, code, ts)).toBe(true);
43
+ });
44
+ it("accepts a code one step in the past (window = 1)", () => {
45
+ const secret = totp.generateSecret();
46
+ const ts = 1000000000000;
47
+ const pastCode = totp.generateOTP(secret, ts - 30_000);
48
+ expect(totp.verifyOTP(secret, pastCode, ts)).toBe(true);
49
+ });
50
+ it("accepts a code one step in the future (window = 1)", () => {
51
+ const secret = totp.generateSecret();
52
+ const ts = 1000000000000;
53
+ const futureCode = totp.generateOTP(secret, ts + 30_000);
54
+ expect(totp.verifyOTP(secret, futureCode, ts)).toBe(true);
55
+ });
56
+ it("rejects a code two steps old when window = 1", () => {
57
+ const secret = totp.generateSecret();
58
+ const ts = 1000000000000;
59
+ const oldCode = totp.generateOTP(secret, ts - 60_000);
60
+ expect(totp.verifyOTP(secret, oldCode, ts)).toBe(false);
61
+ });
62
+ it("rejects an invalid code", () => {
63
+ const secret = totp.generateSecret();
64
+ expect(totp.verifyOTP(secret, "000000", Date.now())).toBe(false);
65
+ });
66
+ it("rejects non-numeric input", () => {
67
+ const secret = totp.generateSecret();
68
+ expect(totp.verifyOTP(secret, "abcdef")).toBe(false);
69
+ });
70
+ it("rejects wrong-length input", () => {
71
+ const secret = totp.generateSecret();
72
+ expect(totp.verifyOTP(secret, "12345")).toBe(false);
73
+ });
74
+ });
75
+ describe("custom options", () => {
76
+ it("generates 8-digit codes when configured", () => {
77
+ const t8 = new totp_service_1.TotpService({ digits: 8 });
78
+ const secret = t8.generateSecret();
79
+ const code = t8.generateOTP(secret);
80
+ expect(code).toMatch(/^\d{8}$/);
81
+ });
82
+ it("accepts window = 0 — only the current step", () => {
83
+ const t0 = new totp_service_1.TotpService({ window: 0 });
84
+ const secret = t0.generateSecret();
85
+ const ts = 1000000000000;
86
+ const pastCode = t0.generateOTP(secret, ts - 30_000);
87
+ expect(t0.verifyOTP(secret, pastCode, ts)).toBe(false);
88
+ const current = t0.generateOTP(secret, ts);
89
+ expect(t0.verifyOTP(secret, current, ts)).toBe(true);
90
+ });
91
+ });
92
+ describe("generateQRUri", () => {
93
+ it("returns a valid otpauth URI", () => {
94
+ const secret = totp.generateSecret();
95
+ const uri = totp.generateQRUri(secret, "MyApp", "user@example.com");
96
+ expect(uri).toMatch(/^otpauth:\/\/totp\//);
97
+ expect(uri).toContain(secret);
98
+ expect(uri).toContain("issuer=MyApp");
99
+ });
100
+ });
101
+ describe("generateBackupCodes", () => {
102
+ it("returns 8 codes by default", () => {
103
+ const codes = totp.generateBackupCodes();
104
+ expect(codes).toHaveLength(8);
105
+ });
106
+ it("returns the requested count", () => {
107
+ const codes = totp.generateBackupCodes(10);
108
+ expect(codes).toHaveLength(10);
109
+ });
110
+ it("returns uppercase hex strings of 12 chars", () => {
111
+ const codes = totp.generateBackupCodes();
112
+ codes.forEach((c) => expect(c).toMatch(/^[0-9A-F]{12}$/));
113
+ });
114
+ it("returns unique codes", () => {
115
+ const codes = totp.generateBackupCodes(20);
116
+ const unique = new Set(codes);
117
+ expect(unique.size).toBe(20);
118
+ });
119
+ });
120
+ });
@@ -2,8 +2,8 @@ import * as Soap from "@soapjs/soap";
2
2
  import { AuthThrottleConfig } from "../types";
3
3
  export declare class AuthThrottleService {
4
4
  private config;
5
- private logger;
6
- constructor(config: AuthThrottleConfig, logger: Soap.Logger);
5
+ private logger?;
6
+ constructor(config: AuthThrottleConfig, logger?: Soap.Logger);
7
7
  checkFailedAttempts(identifier: string): Promise<void>;
8
8
  incrementFailedAttempts(account: any): Promise<void>;
9
9
  resetFailedAttempts(account: any): Promise<void>;
@@ -17,11 +17,11 @@ class AuthThrottleService {
17
17
  (await this.config.getFailedAttempts?.(identifier)) || 0;
18
18
  }
19
19
  catch (e) {
20
- this.logger.error("Check failed attempts:", e);
20
+ this.logger?.error("Check failed attempts:", e);
21
21
  }
22
22
  if (Number.isInteger(failedAttempts) &&
23
23
  failedAttempts >= this.config.maxFailedAttempts) {
24
- this.logger.warn(`User ${identifier} is temporarily locked out.`);
24
+ this.logger?.warn(`User ${identifier} is temporarily locked out.`);
25
25
  throw new errors_1.AccountLockedError();
26
26
  }
27
27
  }
@@ -6,3 +6,4 @@ export * from "./password.service";
6
6
  export * from "./pkce.service";
7
7
  export * from "./rate-limit.service";
8
8
  export * from "./role.service";
9
+ export * from "./totp.service";
@@ -22,3 +22,4 @@ __exportStar(require("./password.service"), exports);
22
22
  __exportStar(require("./pkce.service"), exports);
23
23
  __exportStar(require("./rate-limit.service"), exports);
24
24
  __exportStar(require("./role.service"), exports);
25
+ __exportStar(require("./totp.service"), exports);
@@ -1,14 +1,16 @@
1
1
  import * as Soap from "@soapjs/soap";
2
- import { PasswordPolicyConfig } from "../types";
2
+ import { NewPasswordOptions, PasswordInfo, PasswordPolicyConfig } from "../types";
3
3
  export declare class PasswordService {
4
4
  private config;
5
5
  private logger;
6
6
  constructor(config: PasswordPolicyConfig, logger: Soap.Logger);
7
- validatePassword(password: string): Promise<boolean>;
8
- getLastPasswordChange(identifier: string): Promise<Date>;
7
+ private validateConfig;
8
+ validatePassword(password: string, previousPassword?: string): Promise<void>;
9
+ getPasswordPasswordInfo(identifier: string): Promise<PasswordInfo>;
9
10
  generateResetToken(identifier: string): Promise<string>;
10
11
  sendResetEmail(identifier: string, token: string): Promise<void>;
11
- validateResetToken(token: string): Promise<boolean>;
12
- updatePassword(identifier: string, newPassword: string): Promise<void>;
12
+ validateResetToken(token: string): Promise<void>;
13
+ updatePassword(identifier: string, newPassword: string, passwordOptions?: NewPasswordOptions): Promise<void>;
13
14
  isPasswordChangeRequired(identifier: string): Promise<boolean>;
15
+ generatePassword(identifier: string, options: NewPasswordOptions): Promise<string>;
14
16
  }
@@ -22,57 +22,115 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
25
28
  Object.defineProperty(exports, "__esModule", { value: true });
26
29
  exports.PasswordService = void 0;
27
30
  const Soap = __importStar(require("@soapjs/soap"));
31
+ const bcrypt_1 = __importDefault(require("bcrypt"));
32
+ const crypto_1 = __importDefault(require("crypto"));
33
+ const validation_1 = require("../utils/validation");
34
+ const errors_1 = require("../errors");
28
35
  class PasswordService {
29
36
  config;
30
37
  logger;
31
38
  constructor(config, logger) {
32
39
  this.config = config;
33
40
  this.logger = logger;
41
+ this.validateConfig(config);
34
42
  }
35
- async validatePassword(password) {
36
- return this.config.validatePassword?.(password);
43
+ validateConfig(config) {
44
+ try {
45
+ validation_1.ValidationUtils.required(config, "config");
46
+ validation_1.ValidationUtils.object(config, "config");
47
+ }
48
+ catch (error) {
49
+ if (error instanceof validation_1.ValidationError) {
50
+ throw error;
51
+ }
52
+ throw new validation_1.ValidationError(`Invalid PasswordService configuration: ${error.message}`);
53
+ }
37
54
  }
38
- async getLastPasswordChange(identifier) {
39
- return this.config.getLastPasswordChange?.(identifier);
55
+ async validatePassword(password, previousPassword) {
56
+ validation_1.ValidationUtils.nonEmptyString(password, "password");
57
+ if (previousPassword !== undefined) {
58
+ validation_1.ValidationUtils.nonEmptyString(previousPassword, "previousPassword");
59
+ }
60
+ return this.config.validatePassword?.(password, previousPassword);
61
+ }
62
+ async getPasswordPasswordInfo(identifier) {
63
+ validation_1.ValidationUtils.nonEmptyString(identifier, "identifier");
64
+ return this.config.getPasswordPasswordInfo?.(identifier);
40
65
  }
41
66
  async generateResetToken(identifier) {
67
+ validation_1.ValidationUtils.nonEmptyString(identifier, "identifier");
42
68
  if (!this.config?.generateResetToken) {
43
69
  throw new Soap.NotImplementedError("generateResetToken");
44
70
  }
45
71
  return this.config.generateResetToken?.(identifier);
46
72
  }
47
73
  async sendResetEmail(identifier, token) {
48
- if (!this.config?.sendResetEmail) {
74
+ validation_1.ValidationUtils.nonEmptyString(identifier, "identifier");
75
+ validation_1.ValidationUtils.nonEmptyString(token, "token");
76
+ if (!this.config?.sendPasswordResetEmail) {
49
77
  throw new Soap.NotImplementedError("sendResetEmail");
50
78
  }
51
- return this.config.sendResetEmail?.(identifier, token);
79
+ return this.config.sendPasswordResetEmail?.(identifier, token);
52
80
  }
53
81
  async validateResetToken(token) {
54
- if (!this.config?.validateResetToken) {
82
+ validation_1.ValidationUtils.nonEmptyString(token, "token");
83
+ if (!this.config?.validatePasswordResetToken) {
55
84
  throw new Soap.NotImplementedError("validateResetToken");
56
85
  }
57
- return this.config.validateResetToken?.(token);
86
+ const isValid = await this.config.validatePasswordResetToken(token);
87
+ if (isValid === false) {
88
+ throw new errors_1.ExpiredResetTokenError();
89
+ }
58
90
  }
59
- async updatePassword(identifier, newPassword) {
91
+ async updatePassword(identifier, newPassword, passwordOptions) {
92
+ validation_1.ValidationUtils.nonEmptyString(identifier, "identifier");
93
+ validation_1.ValidationUtils.nonEmptyString(newPassword, "newPassword");
94
+ if (passwordOptions) {
95
+ validation_1.ValidationUtils.object(passwordOptions, "passwordOptions");
96
+ if (passwordOptions.type) {
97
+ validation_1.ValidationUtils.oneOf(passwordOptions.type, "passwordOptions.type", ["default", "one-time", "temporary"]);
98
+ }
99
+ }
60
100
  if (!this.config?.updatePassword) {
61
101
  throw new Soap.NotImplementedError("updatePassword");
62
102
  }
63
- return this.config.updatePassword?.(identifier, newPassword);
103
+ return this.config.updatePassword?.(identifier, newPassword, passwordOptions);
64
104
  }
65
105
  async isPasswordChangeRequired(identifier) {
66
- if (this.config.passwordExpirationDays) {
67
- if (!this.config.getLastPasswordChange) {
68
- throw new Soap.NotImplementedError("getLastPasswordChange");
69
- }
70
- const lastChanged = await this.config.getLastPasswordChange(identifier);
71
- return (lastChanged &&
72
- Date.now() - Number(lastChanged) >
73
- this.config.passwordExpirationDays * 86400000);
106
+ validation_1.ValidationUtils.nonEmptyString(identifier, "identifier");
107
+ const passwordInfo = await this.config.getPasswordPasswordInfo?.(identifier);
108
+ if (passwordInfo &&
109
+ (passwordInfo.type === "one-time" ||
110
+ (passwordInfo.type === "temporary" &&
111
+ Number.isInteger(passwordInfo.expiresIn) &&
112
+ Date.now() - passwordInfo.lastChangeDate.getTime() >
113
+ passwordInfo.expiresIn))) {
114
+ return true;
74
115
  }
75
116
  return false;
76
117
  }
118
+ async generatePassword(identifier, options) {
119
+ validation_1.ValidationUtils.nonEmptyString(identifier, "identifier");
120
+ validation_1.ValidationUtils.required(options, "options");
121
+ validation_1.ValidationUtils.object(options, "options");
122
+ validation_1.ValidationUtils.oneOf(options.type, "options.type", ["default", "one-time", "temporary"]);
123
+ let plaintext;
124
+ if (this.config.generatePassword) {
125
+ plaintext = await this.config.generatePassword(identifier, options);
126
+ }
127
+ else {
128
+ plaintext = crypto_1.default.randomBytes(12).toString("base64url");
129
+ }
130
+ const salt = await bcrypt_1.default.genSalt(10);
131
+ const hashedPassword = await bcrypt_1.default.hash(plaintext, salt);
132
+ await this.updatePassword(identifier, hashedPassword, options);
133
+ return plaintext;
134
+ }
77
135
  }
78
136
  exports.PasswordService = PasswordService;
@@ -0,0 +1,16 @@
1
+ export interface TotpOptions {
2
+ step?: number;
3
+ digits?: number;
4
+ window?: number;
5
+ }
6
+ export declare class TotpService {
7
+ private readonly step;
8
+ private readonly digits;
9
+ private readonly window;
10
+ constructor(options?: TotpOptions);
11
+ generateSecret(byteLength?: number): string;
12
+ generateOTP(secret: string, timestamp?: number): string;
13
+ verifyOTP(secret: string, code: string, timestamp?: number): boolean;
14
+ generateQRUri(secret: string, issuer: string, account: string): string;
15
+ generateBackupCodes(count?: number): string[];
16
+ }