@twin.org/api-auth-entity-storage-service 0.0.3-next.4 → 0.0.3-next.40

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 (104) hide show
  1. package/README.md +2 -2
  2. package/dist/es/entities/authenticationAuditEntry.js +101 -0
  3. package/dist/es/entities/authenticationAuditEntry.js.map +1 -0
  4. package/dist/es/entities/authenticationRateEntry.js +37 -0
  5. package/dist/es/entities/authenticationRateEntry.js.map +1 -0
  6. package/dist/es/entities/authenticationUser.js +17 -1
  7. package/dist/es/entities/authenticationUser.js.map +1 -1
  8. package/dist/es/index.js +11 -1
  9. package/dist/es/index.js.map +1 -1
  10. package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js.map +1 -1
  11. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js.map +1 -1
  12. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js +4 -0
  13. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js.map +1 -0
  14. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js +2 -0
  15. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js.map +1 -0
  16. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js +2 -0
  17. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js.map +1 -0
  18. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js +2 -0
  19. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js.map +1 -0
  20. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js +0 -2
  21. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js.map +1 -1
  22. package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js.map +1 -1
  23. package/dist/es/processors/authHeaderProcessor.js +62 -10
  24. package/dist/es/processors/authHeaderProcessor.js.map +1 -1
  25. package/dist/es/restEntryPoints.js +14 -0
  26. package/dist/es/restEntryPoints.js.map +1 -1
  27. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js +362 -0
  28. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js.map +1 -0
  29. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js +174 -0
  30. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js.map +1 -0
  31. package/dist/es/routes/entityStorageAuthenticationRoutes.js +20 -21
  32. package/dist/es/routes/entityStorageAuthenticationRoutes.js.map +1 -1
  33. package/dist/es/schema.js +4 -0
  34. package/dist/es/schema.js.map +1 -1
  35. package/dist/es/services/entityStorageAuthenticationAdminService.js +161 -55
  36. package/dist/es/services/entityStorageAuthenticationAdminService.js.map +1 -1
  37. package/dist/es/services/entityStorageAuthenticationAuditService.js +178 -0
  38. package/dist/es/services/entityStorageAuthenticationAuditService.js.map +1 -0
  39. package/dist/es/services/entityStorageAuthenticationRateService.js +202 -0
  40. package/dist/es/services/entityStorageAuthenticationRateService.js.map +1 -0
  41. package/dist/es/services/entityStorageAuthenticationService.js +200 -14
  42. package/dist/es/services/entityStorageAuthenticationService.js.map +1 -1
  43. package/dist/es/utils/passwordHelper.js +45 -16
  44. package/dist/es/utils/passwordHelper.js.map +1 -1
  45. package/dist/es/utils/tokenHelper.js +45 -21
  46. package/dist/es/utils/tokenHelper.js.map +1 -1
  47. package/dist/types/entities/authenticationAuditEntry.d.ts +49 -0
  48. package/dist/types/entities/authenticationRateEntry.d.ts +17 -0
  49. package/dist/types/entities/authenticationUser.d.ts +8 -0
  50. package/dist/types/index.d.ts +11 -1
  51. package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +14 -0
  52. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +5 -0
  53. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConfig.d.ts +9 -0
  54. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.d.ts +15 -0
  55. package/dist/types/models/IEntityStorageAuthenticationRateServiceConfig.d.ts +10 -0
  56. package/dist/types/models/IEntityStorageAuthenticationRateServiceConstructorOptions.d.ts +20 -0
  57. package/dist/types/models/IEntityStorageAuthenticationServiceConfig.d.ts +22 -1
  58. package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +17 -3
  59. package/dist/types/processors/authHeaderProcessor.d.ts +1 -1
  60. package/dist/types/routes/entityStorageAuthenticationAdminRoutes.d.ts +61 -0
  61. package/dist/types/routes/entityStorageAuthenticationAuditRoutes.d.ts +29 -0
  62. package/dist/types/services/entityStorageAuthenticationAdminService.d.ts +23 -6
  63. package/dist/types/services/entityStorageAuthenticationAuditService.d.ts +59 -0
  64. package/dist/types/services/entityStorageAuthenticationRateService.d.ts +60 -0
  65. package/dist/types/services/entityStorageAuthenticationService.d.ts +8 -3
  66. package/dist/types/utils/passwordHelper.d.ts +13 -5
  67. package/dist/types/utils/tokenHelper.d.ts +9 -2
  68. package/docs/changelog.md +658 -64
  69. package/docs/examples.md +178 -1
  70. package/docs/reference/classes/AuthHeaderProcessor.md +10 -10
  71. package/docs/reference/classes/AuthenticationAuditEntry.md +101 -0
  72. package/docs/reference/classes/AuthenticationRateEntry.md +37 -0
  73. package/docs/reference/classes/AuthenticationUser.md +21 -5
  74. package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +78 -18
  75. package/docs/reference/classes/EntityStorageAuthenticationAuditService.md +157 -0
  76. package/docs/reference/classes/EntityStorageAuthenticationRateService.md +227 -0
  77. package/docs/reference/classes/EntityStorageAuthenticationService.md +36 -16
  78. package/docs/reference/classes/PasswordHelper.md +37 -12
  79. package/docs/reference/classes/TokenHelper.md +44 -8
  80. package/docs/reference/functions/authenticationAdminCreateUser.md +31 -0
  81. package/docs/reference/functions/authenticationAdminGetUser.md +31 -0
  82. package/docs/reference/functions/authenticationAdminGetUserByIdentity.md +31 -0
  83. package/docs/reference/functions/authenticationAdminRemoveUser.md +31 -0
  84. package/docs/reference/functions/authenticationAdminUpdateUser.md +31 -0
  85. package/docs/reference/functions/authenticationAdminUpdateUserPassword.md +31 -0
  86. package/docs/reference/functions/authenticationAuditCreate.md +31 -0
  87. package/docs/reference/functions/authenticationAuditQuery.md +31 -0
  88. package/docs/reference/functions/generateRestRoutesAuthenticationAdmin.md +25 -0
  89. package/docs/reference/functions/generateRestRoutesAuthenticationAudit.md +25 -0
  90. package/docs/reference/index.md +20 -0
  91. package/docs/reference/interfaces/IAuthHeaderProcessorConfig.md +4 -4
  92. package/docs/reference/interfaces/IAuthHeaderProcessorConstructorOptions.md +40 -4
  93. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConfig.md +2 -2
  94. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConstructorOptions.md +18 -4
  95. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConfig.md +11 -0
  96. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConstructorOptions.md +25 -0
  97. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConfig.md +17 -0
  98. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md +39 -0
  99. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConfig.md +61 -5
  100. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConstructorOptions.md +46 -10
  101. package/docs/reference/variables/tagsAuthenticationAdmin.md +5 -0
  102. package/docs/reference/variables/tagsAuthenticationAudit.md +5 -0
  103. package/locales/en.json +17 -3
  104. package/package.json +8 -7
@@ -1,5 +1,7 @@
1
+ import { AuthAuditEvent } from "@twin.org/api-auth-entity-storage-models";
1
2
  import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
2
- import { ComponentFactory, Converter, GeneralError, Guards, Is, UnauthorizedError } from "@twin.org/core";
3
+ import { Coerce, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, UnauthorizedError } from "@twin.org/core";
4
+ import { PasswordGenerator, PasswordValidator } from "@twin.org/crypto";
3
5
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
4
6
  import { VaultConnectorFactory } from "@twin.org/vault-models";
5
7
  import { PasswordHelper } from "../utils/passwordHelper.js";
@@ -18,10 +20,45 @@ export class EntityStorageAuthenticationService {
18
20
  */
19
21
  static _DEFAULT_TTL_MINUTES = 60;
20
22
  /**
21
- * The user admin service.
23
+ * Default maximum login attempts in a rate window.
22
24
  * @internal
23
25
  */
24
- _authenticationAdminService;
26
+ static _DEFAULT_LOGIN_RATE_MAX_ATTEMPTS = 5;
27
+ /**
28
+ * Default login rate window in minutes.
29
+ * @internal
30
+ */
31
+ static _DEFAULT_LOGIN_RATE_WINDOW_MINUTES = 15;
32
+ /**
33
+ * Default maximum password change attempts in a rate window.
34
+ * @internal
35
+ */
36
+ static _DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS = 5;
37
+ /**
38
+ * Default password change rate window in minutes.
39
+ * @internal
40
+ */
41
+ static _DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES = 15;
42
+ /**
43
+ * Default maximum token refresh attempts in a rate window.
44
+ * @internal
45
+ */
46
+ static _DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS = 30;
47
+ /**
48
+ * Default token refresh rate window in minutes.
49
+ * @internal
50
+ */
51
+ static _DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES = 60;
52
+ /**
53
+ * The audit service.
54
+ * @internal
55
+ */
56
+ _authenticationAuditService;
57
+ /**
58
+ * The rate service.
59
+ * @internal
60
+ */
61
+ _authenticationRateService;
25
62
  /**
26
63
  * The entity storage for users.
27
64
  * @internal
@@ -32,6 +69,11 @@ export class EntityStorageAuthenticationService {
32
69
  * @internal
33
70
  */
34
71
  _vaultConnector;
72
+ /**
73
+ * The transformer component, used to resolve public origins for tenants and encrypt/decrypt tenant tokens.
74
+ * @internal
75
+ */
76
+ _urlTransformerService;
35
77
  /**
36
78
  * The name of the key to retrieve from the vault for signing JWT.
37
79
  * @internal
@@ -42,6 +84,31 @@ export class EntityStorageAuthenticationService {
42
84
  * @internal
43
85
  */
44
86
  _defaultTtlMinutes;
87
+ /**
88
+ * The minimum password length for validation.
89
+ * @internal
90
+ */
91
+ _minPasswordLength;
92
+ /**
93
+ * Rate limit configuration for login failures.
94
+ * @internal
95
+ */
96
+ _loginRateLimit;
97
+ /**
98
+ * Rate limit configuration for password changes.
99
+ * @internal
100
+ */
101
+ _passwordChangeRateLimit;
102
+ /**
103
+ * Rate limit configuration for token refresh.
104
+ * @internal
105
+ */
106
+ _tokenRefreshRateLimit;
107
+ /**
108
+ * The component to retrieve tenant information.
109
+ * @internal
110
+ */
111
+ _tenantAdminComponent;
45
112
  /**
46
113
  * The node identity.
47
114
  * @internal
@@ -54,10 +121,32 @@ export class EntityStorageAuthenticationService {
54
121
  constructor(options) {
55
122
  this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
56
123
  this._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
57
- this._authenticationAdminService = ComponentFactory.get(options?.authenticationAdminServiceType ?? "authentication-admin");
124
+ this._urlTransformerService = ComponentFactory.get(options?.urlTransformerComponentType ?? "url-transformer");
125
+ this._authenticationAuditService = ComponentFactory.getIfExists(options?.authenticationAuditServiceType ?? "authentication-audit");
126
+ this._authenticationRateService = ComponentFactory.get(options?.authenticationRateServiceType ?? "authentication-rate");
127
+ this._tenantAdminComponent = ComponentFactory.getIfExists(options?.tenantAdminComponentType ?? "tenant-admin");
58
128
  this._signingKeyName = options?.config?.signingKeyName ?? "auth-signing";
59
129
  this._defaultTtlMinutes =
60
130
  options?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;
131
+ this._minPasswordLength = options?.config?.minPasswordLength;
132
+ this._loginRateLimit = {
133
+ maxAttempts: options?.config?.loginRateLimit?.maxAttempts ??
134
+ EntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_MAX_ATTEMPTS,
135
+ windowMinutes: options?.config?.loginRateLimit?.windowMinutes ??
136
+ EntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_WINDOW_MINUTES
137
+ };
138
+ this._passwordChangeRateLimit = {
139
+ maxAttempts: options?.config?.passwordChangeRateLimit?.maxAttempts ??
140
+ EntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS,
141
+ windowMinutes: options?.config?.passwordChangeRateLimit?.windowMinutes ??
142
+ EntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES
143
+ };
144
+ this._tokenRefreshRateLimit = {
145
+ maxAttempts: options?.config?.tokenRefreshRateLimit?.maxAttempts ??
146
+ EntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS,
147
+ windowMinutes: options?.config?.tokenRefreshRateLimit?.windowMinutes ??
148
+ EntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES
149
+ };
61
150
  }
62
151
  /**
63
152
  * Returns the class name of the component.
@@ -75,6 +164,19 @@ export class EntityStorageAuthenticationService {
75
164
  const contextIds = await ContextIdStore.getContextIds();
76
165
  ContextIdHelper.guard(contextIds, ContextIdKeys.Node);
77
166
  this._nodeId = contextIds[ContextIdKeys.Node];
167
+ await this._authenticationRateService.registerAction("login", this._loginRateLimit);
168
+ await this._authenticationRateService.registerAction("password-change", this._passwordChangeRateLimit);
169
+ await this._authenticationRateService.registerAction("token-refresh", this._tokenRefreshRateLimit);
170
+ }
171
+ /**
172
+ * The component needs to be stopped when the node is closed.
173
+ * @param nodeLoggingComponentType The node logging component type.
174
+ * @returns Nothing.
175
+ */
176
+ async stop(nodeLoggingComponentType) {
177
+ await this._authenticationRateService.unregisterAction("login");
178
+ await this._authenticationRateService.unregisterAction("password-change");
179
+ await this._authenticationRateService.unregisterAction("token-refresh");
78
180
  }
79
181
  /**
80
182
  * Perform a login for the user.
@@ -85,23 +187,48 @@ export class EntityStorageAuthenticationService {
85
187
  async login(email, password) {
86
188
  Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "email", email);
87
189
  Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "password", password);
190
+ let loginUser;
191
+ let loginTenantId;
192
+ let tokenAndExpiry;
88
193
  try {
194
+ await this._authenticationRateService.check("login", email);
89
195
  const user = await this._userEntityStorage.get(email);
90
196
  if (!user) {
91
197
  throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "userNotFound");
92
198
  }
93
199
  const saltBytes = Converter.base64ToBytes(user.salt);
94
200
  const passwordBytes = Converter.utf8ToBytes(password);
95
- const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
96
- if (hashedPassword !== user.password) {
201
+ const hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);
202
+ if (!PasswordValidator.comparePasswordHashes(hashedPassword, user.password)) {
97
203
  throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "passwordMismatch");
98
204
  }
99
- const tokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, user.identity, user.organization, this._defaultTtlMinutes);
100
- return tokenAndExpiry;
205
+ // This might be undefined if the login is performed in a single tenant context
206
+ // it is verified during the token processing, tenant id will be matched against
207
+ // the context
208
+ const contextIds = await ContextIdStore.getContextIds();
209
+ loginTenantId = contextIds?.[ContextIdKeys.Tenant];
210
+ tokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, this._urlTransformerService, `${this._nodeId}/${this._signingKeyName}`, user.identity, user.organization, loginTenantId, this._defaultTtlMinutes, user.scope, user.passwordVersion ?? 0);
211
+ loginUser = user;
101
212
  }
102
213
  catch (error) {
214
+ await this._authenticationAuditService?.create({
215
+ actorId: email,
216
+ event: AuthAuditEvent.LoginFailure
217
+ });
103
218
  throw new UnauthorizedError(EntityStorageAuthenticationService.CLASS_NAME, "loginFailed", undefined, error);
104
219
  }
220
+ await this._authenticationRateService.clear("login", email);
221
+ await this._authenticationAuditService?.create({
222
+ actorId: email,
223
+ event: AuthAuditEvent.LoginSuccess,
224
+ data: {
225
+ userIdentity: loginUser.identity,
226
+ organizationIdentity: loginUser.organization,
227
+ tenantId: loginTenantId,
228
+ scope: loginUser.scope.split(",")
229
+ }
230
+ });
231
+ return tokenAndExpiry;
105
232
  }
106
233
  /**
107
234
  * Logout the current user.
@@ -109,7 +236,16 @@ export class EntityStorageAuthenticationService {
109
236
  * @returns Nothing.
110
237
  */
111
238
  async logout(token) {
112
- // Nothing to do here.
239
+ // Nothing to do here, as we are stateless.
240
+ // The cookie will be revoked by the REST route handling
241
+ const contextIds = await ContextIdStore.getContextIds();
242
+ const identifier = contextIds?.[ContextIdKeys.User];
243
+ if (Is.stringValue(identifier)) {
244
+ await this._authenticationAuditService?.create({
245
+ actorId: identifier,
246
+ event: AuthAuditEvent.Logout
247
+ });
248
+ }
113
249
  }
114
250
  /**
115
251
  * Refresh the token.
@@ -117,20 +253,70 @@ export class EntityStorageAuthenticationService {
117
253
  * @returns The refreshed token, if it uses a mechanism with public access.
118
254
  */
119
255
  async refresh(token) {
256
+ let refreshPasswordVersion;
257
+ let tenantId;
120
258
  // If the verify fails on the current token then it will throw an exception.
121
- const headerAndPayload = await TokenHelper.verify(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, token);
122
- const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, headerAndPayload.payload.sub ?? "", Is.stringValue(headerAndPayload.payload.org) ? headerAndPayload.payload.org : "", this._defaultTtlMinutes);
259
+ const headerAndPayload = await TokenHelper.verify(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, token, undefined, async (userIdentity, organizationIdentity, encryptedTenantId, passwordVersion) => {
260
+ const validParts = [];
261
+ const contextIds = (await ContextIdStore.getContextIds()) ?? {};
262
+ // If the token carries an encrypted tenant ID and the admin component is available,
263
+ // decrypt and resolve the tenant first so the user lookup runs in the correct partition.
264
+ if (Is.stringValue(encryptedTenantId)) {
265
+ tenantId = await this._urlTransformerService.decryptParam(encryptedTenantId);
266
+ if (Is.stringValue(tenantId)) {
267
+ const tenant = await this._tenantAdminComponent?.get(tenantId);
268
+ if (!Is.empty(tenant)) {
269
+ validParts.push("tenant");
270
+ contextIds[ContextIdKeys.Tenant] = tenantId;
271
+ }
272
+ }
273
+ }
274
+ // Wrap the user lookup in the request context so partitioned storage uses the correct tenant.
275
+ const user = await ContextIdStore.run(contextIds, async () => this._userEntityStorage.get(userIdentity, "identity"));
276
+ refreshPasswordVersion = user?.passwordVersion;
277
+ if (user?.identity === userIdentity &&
278
+ (passwordVersion ?? 0) === (refreshPasswordVersion ?? 0)) {
279
+ validParts.push("user");
280
+ }
281
+ if (user?.organization === organizationIdentity) {
282
+ validParts.push("organization");
283
+ }
284
+ return validParts;
285
+ });
286
+ const refreshSub = headerAndPayload.payload.sub ?? "";
287
+ await this._authenticationRateService.check("token-refresh", refreshSub);
288
+ const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, this._urlTransformerService, `${this._nodeId}/${this._signingKeyName}`, refreshSub, Is.stringValue(headerAndPayload.payload.org) ? headerAndPayload.payload.org : "", tenantId, this._defaultTtlMinutes, Coerce.string(headerAndPayload.payload?.scope), refreshPasswordVersion ?? 0);
289
+ const refreshScope = Coerce.string(headerAndPayload.payload?.scope) ?? "";
290
+ await this._authenticationAuditService?.create({
291
+ actorId: refreshSub,
292
+ event: AuthAuditEvent.TokenRefreshed,
293
+ data: {
294
+ organizationIdentity: Is.stringValue(headerAndPayload.payload.org)
295
+ ? headerAndPayload.payload.org
296
+ : "",
297
+ tenantId,
298
+ scope: refreshScope.split(",").filter(scope => scope.length > 0)
299
+ }
300
+ });
123
301
  return refreshTokenAndExpiry;
124
302
  }
125
303
  /**
126
304
  * Update the user's password.
127
- * @param email The email address of the user to update.
128
305
  * @param currentPassword The current password for the user.
129
306
  * @param newPassword The new password for the user.
130
307
  * @returns Nothing.
131
308
  */
132
- async updatePassword(email, currentPassword, newPassword) {
133
- return this._authenticationAdminService.updatePassword(email, newPassword, currentPassword);
309
+ async updatePassword(currentPassword, newPassword) {
310
+ const contextIds = await ContextIdStore.getContextIds();
311
+ ContextIdHelper.guard(contextIds, ContextIdKeys.User);
312
+ const userIdentity = contextIds[ContextIdKeys.User];
313
+ await this._authenticationRateService.check("password-change", userIdentity);
314
+ const user = await this._userEntityStorage.get(userIdentity, "identity");
315
+ if (!Is.object(user)) {
316
+ throw new NotFoundError(EntityStorageAuthenticationService.CLASS_NAME, "userNotFound", userIdentity);
317
+ }
318
+ await PasswordHelper.updatePassword(this._userEntityStorage, this._authenticationAuditService, user, newPassword, currentPassword, this._minPasswordLength);
319
+ await this._authenticationRateService.clear("password-change", userIdentity);
134
320
  }
135
321
  }
136
322
  //# sourceMappingURL=entityStorageAuthenticationService.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"entityStorageAuthenticationService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationService.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnF,OAAO,EACN,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,MAAM,EACN,EAAE,EACF,iBAAiB,EACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,6BAA6B,EAE7B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,qBAAqB,EAAwB,MAAM,wBAAwB,CAAC;AAGrF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD;;GAEG;AACH,MAAM,OAAO,kCAAkC;IAC9C;;OAEG;IACI,MAAM,CAAU,UAAU,wCAAwD;IAEzF;;;OAGG;IACK,MAAM,CAAU,oBAAoB,GAAW,EAAE,CAAC;IAE1D;;;OAGG;IACc,2BAA2B,CAAgC;IAE5E;;;OAGG;IACc,kBAAkB,CAA8C;IAEjF;;;OAGG;IACc,eAAe,CAAkB;IAElD;;;OAGG;IACc,eAAe,CAAS;IAEzC;;;OAGG;IACc,kBAAkB,CAAS;IAE5C;;;OAGG;IACK,OAAO,CAAU;IAEzB;;;OAGG;IACH,YAAY,OAA+D;QAC1E,IAAI,CAAC,kBAAkB,GAAG,6BAA6B,CAAC,GAAG,CAC1D,OAAO,EAAE,qBAAqB,IAAI,qBAAqB,CACvD,CAAC;QAEF,IAAI,CAAC,eAAe,GAAG,qBAAqB,CAAC,GAAG,CAAC,OAAO,EAAE,kBAAkB,IAAI,OAAO,CAAC,CAAC;QAEzF,IAAI,CAAC,2BAA2B,GAAG,gBAAgB,CAAC,GAAG,CACtD,OAAO,EAAE,8BAA8B,IAAI,sBAAsB,CACjE,CAAC;QAEF,IAAI,CAAC,eAAe,GAAG,OAAO,EAAE,MAAM,EAAE,cAAc,IAAI,cAAc,CAAC;QACzE,IAAI,CAAC,kBAAkB;YACtB,OAAO,EAAE,MAAM,EAAE,iBAAiB,IAAI,kCAAkC,CAAC,oBAAoB,CAAC;IAChG,CAAC;IAED;;;OAGG;IACI,SAAS;QACf,OAAO,kCAAkC,CAAC,UAAU,CAAC;IACtD,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,KAAK,CAAC,wBAAiC;QACnD,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,aAAa,EAAE,CAAC;QACxD,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,KAAK,CACjB,KAAa,EACb,QAAgB;QAKhB,MAAM,CAAC,WAAW,CAAC,kCAAkC,CAAC,UAAU,WAAiB,KAAK,CAAC,CAAC;QACxF,MAAM,CAAC,WAAW,CAAC,kCAAkC,CAAC,UAAU,cAAoB,QAAQ,CAAC,CAAC;QAE9F,IAAI,CAAC;YACJ,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACtD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACX,MAAM,IAAI,YAAY,CAAC,kCAAkC,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;YACvF,CAAC;YAED,MAAM,SAAS,GAAG,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrD,MAAM,aAAa,GAAG,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAEtD,MAAM,cAAc,GAAG,MAAM,cAAc,CAAC,YAAY,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;YAEnF,IAAI,cAAc,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACtC,MAAM,IAAI,YAAY,CAAC,kCAAkC,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;YAC3F,CAAC;YAED,MAAM,cAAc,GAAG,MAAM,WAAW,CAAC,WAAW,CACnD,IAAI,CAAC,eAAe,EACpB,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,eAAe,EAAE,EACzC,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,kBAAkB,CACvB,CAAC;YAEF,OAAO,cAAc,CAAC;QACvB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,iBAAiB,CAC1B,kCAAkC,CAAC,UAAU,EAC7C,aAAa,EACb,SAAS,EACT,KAAK,CACL,CAAC;QACH,CAAC;IACF,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,MAAM,CAAC,KAAc;QACjC,sBAAsB;IACvB,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,OAAO,CAAC,KAAc;QAIlC,4EAA4E;QAC5E,MAAM,gBAAgB,GAAG,MAAM,WAAW,CAAC,MAAM,CAChD,IAAI,CAAC,eAAe,EACpB,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,eAAe,EAAE,EACzC,KAAK,CACL,CAAC;QAEF,MAAM,qBAAqB,GAAG,MAAM,WAAW,CAAC,WAAW,CAC1D,IAAI,CAAC,eAAe,EACpB,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,eAAe,EAAE,EACzC,gBAAgB,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,EAClC,EAAE,CAAC,WAAW,CAAC,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAChF,IAAI,CAAC,kBAAkB,CACvB,CAAC;QAEF,OAAO,qBAAqB,CAAC;IAC9B,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,cAAc,CAC1B,KAAa,EACb,eAAuB,EACvB,WAAmB;QAEnB,OAAO,IAAI,CAAC,2BAA2B,CAAC,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,eAAe,CAAC,CAAC;IAC7F,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationAdminComponent,\n\tIAuthenticationComponent\n} from \"@twin.org/api-auth-entity-storage-models\";\nimport { ContextIdHelper, ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport {\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tUnauthorizedError\n} from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { VaultConnectorFactory, type IVaultConnector } from \"@twin.org/vault-models\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationServiceConstructorOptions.js\";\nimport { PasswordHelper } from \"../utils/passwordHelper.js\";\nimport { TokenHelper } from \"../utils/tokenHelper.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationService implements IAuthenticationComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationService>();\n\n\t/**\n\t * Default TTL in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TTL_MINUTES: number = 60;\n\n\t/**\n\t * The user admin service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAdminService: IAuthenticationAdminComponent;\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The vault for the keys.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector: IVaultConnector;\n\n\t/**\n\t * The name of the key to retrieve from the vault for signing JWT.\n\t * @internal\n\t */\n\tprivate readonly _signingKeyName: string;\n\n\t/**\n\t * The default TTL for the token.\n\t * @internal\n\t */\n\tprivate readonly _defaultTtlMinutes: number;\n\n\t/**\n\t * The node identity.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthentication.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? \"vault\");\n\n\t\tthis._authenticationAdminService = ComponentFactory.get<IAuthenticationAdminComponent>(\n\t\t\toptions?.authenticationAdminServiceType ?? \"authentication-admin\"\n\t\t);\n\n\t\tthis._signingKeyName = options?.config?.signingKeyName ?? \"auth-signing\";\n\t\tthis._defaultTtlMinutes =\n\t\t\toptions?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationService.CLASS_NAME;\n\t}\n\n\t/**\n\t * The service needs to be started when the application is initialized.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns Nothing.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.Node);\n\t\tthis._nodeId = contextIds[ContextIdKeys.Node];\n\t}\n\n\t/**\n\t * Perform a login for the user.\n\t * @param email The email address for the user.\n\t * @param password The password for the user.\n\t * @returns The authentication token for the user, if it uses a mechanism with public access.\n\t */\n\tpublic async login(\n\t\temail: string,\n\t\tpassword: string\n\t): Promise<{\n\t\ttoken?: string;\n\t\texpiry: number;\n\t}> {\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(password), password);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!user) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"userNotFound\");\n\t\t\t}\n\n\t\t\tconst saltBytes = Converter.base64ToBytes(user.salt);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(password);\n\n\t\t\tconst hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tif (hashedPassword !== user.password) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"passwordMismatch\");\n\t\t\t}\n\n\t\t\tconst tokenAndExpiry = await TokenHelper.createToken(\n\t\t\t\tthis._vaultConnector,\n\t\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\t\tuser.identity,\n\t\t\t\tuser.organization,\n\t\t\t\tthis._defaultTtlMinutes\n\t\t\t);\n\n\t\t\treturn tokenAndExpiry;\n\t\t} catch (error) {\n\t\t\tthrow new UnauthorizedError(\n\t\t\t\tEntityStorageAuthenticationService.CLASS_NAME,\n\t\t\t\t\"loginFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Logout the current user.\n\t * @param token The token to logout, if it uses a mechanism with public access.\n\t * @returns Nothing.\n\t */\n\tpublic async logout(token?: string): Promise<void> {\n\t\t// Nothing to do here.\n\t}\n\n\t/**\n\t * Refresh the token.\n\t * @param token The token to refresh, if it uses a mechanism with public access.\n\t * @returns The refreshed token, if it uses a mechanism with public access.\n\t */\n\tpublic async refresh(token?: string): Promise<{\n\t\ttoken: string;\n\t\texpiry: number;\n\t}> {\n\t\t// If the verify fails on the current token then it will throw an exception.\n\t\tconst headerAndPayload = await TokenHelper.verify(\n\t\t\tthis._vaultConnector,\n\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\ttoken\n\t\t);\n\n\t\tconst refreshTokenAndExpiry = await TokenHelper.createToken(\n\t\t\tthis._vaultConnector,\n\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\theaderAndPayload.payload.sub ?? \"\",\n\t\t\tIs.stringValue(headerAndPayload.payload.org) ? headerAndPayload.payload.org : \"\",\n\t\t\tthis._defaultTtlMinutes\n\t\t);\n\n\t\treturn refreshTokenAndExpiry;\n\t}\n\n\t/**\n\t * Update the user's password.\n\t * @param email The email address of the user to update.\n\t * @param currentPassword The current password for the user.\n\t * @param newPassword The new password for the user.\n\t * @returns Nothing.\n\t */\n\tpublic async updatePassword(\n\t\temail: string,\n\t\tcurrentPassword: string,\n\t\tnewPassword: string\n\t): Promise<void> {\n\t\treturn this._authenticationAdminService.updatePassword(email, newPassword, currentPassword);\n\t}\n}\n"]}
1
+ {"version":3,"file":"entityStorageAuthenticationService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationRateActionConfig,\n\tIAuthenticationRateComponent,\n\tIAuthenticationAuditComponent,\n\tIAuthenticationComponent\n} from \"@twin.org/api-auth-entity-storage-models\";\nimport { AuthAuditEvent } from \"@twin.org/api-auth-entity-storage-models\";\nimport type { ITenantAdminComponent, IUrlTransformerComponent } from \"@twin.org/api-models\";\nimport { ContextIdHelper, ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport {\n\tCoerce,\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tNotFoundError,\n\tUnauthorizedError\n} from \"@twin.org/core\";\nimport { PasswordGenerator, PasswordValidator } from \"@twin.org/crypto\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { VaultConnectorFactory, type IVaultConnector } from \"@twin.org/vault-models\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationServiceConstructorOptions.js\";\nimport { PasswordHelper } from \"../utils/passwordHelper.js\";\nimport { TokenHelper } from \"../utils/tokenHelper.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationService implements IAuthenticationComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationService>();\n\n\t/**\n\t * Default TTL in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TTL_MINUTES: number = 60;\n\n\t/**\n\t * Default maximum login attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_LOGIN_RATE_MAX_ATTEMPTS: number = 5;\n\n\t/**\n\t * Default login rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_LOGIN_RATE_WINDOW_MINUTES: number = 15;\n\n\t/**\n\t * Default maximum password change attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS: number = 5;\n\n\t/**\n\t * Default password change rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES: number = 15;\n\n\t/**\n\t * Default maximum token refresh attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS: number = 30;\n\n\t/**\n\t * Default token refresh rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES: number = 60;\n\n\t/**\n\t * The audit service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAuditService?: IAuthenticationAuditComponent;\n\n\t/**\n\t * The rate service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationRateService: IAuthenticationRateComponent;\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The vault for the keys.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector: IVaultConnector;\n\n\t/**\n\t * The transformer component, used to resolve public origins for tenants and encrypt/decrypt tenant tokens.\n\t * @internal\n\t */\n\tprivate readonly _urlTransformerService: IUrlTransformerComponent;\n\n\t/**\n\t * The name of the key to retrieve from the vault for signing JWT.\n\t * @internal\n\t */\n\tprivate readonly _signingKeyName: string;\n\n\t/**\n\t * The default TTL for the token.\n\t * @internal\n\t */\n\tprivate readonly _defaultTtlMinutes: number;\n\n\t/**\n\t * The minimum password length for validation.\n\t * @internal\n\t */\n\tprivate readonly _minPasswordLength?: number;\n\n\t/**\n\t * Rate limit configuration for login failures.\n\t * @internal\n\t */\n\tprivate readonly _loginRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * Rate limit configuration for password changes.\n\t * @internal\n\t */\n\tprivate readonly _passwordChangeRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * Rate limit configuration for token refresh.\n\t * @internal\n\t */\n\tprivate readonly _tokenRefreshRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * The component to retrieve tenant information.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponent?: ITenantAdminComponent;\n\n\t/**\n\t * The node identity.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthentication.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? \"vault\");\n\n\t\tthis._urlTransformerService = ComponentFactory.get(\n\t\t\toptions?.urlTransformerComponentType ?? \"url-transformer\"\n\t\t);\n\n\t\tthis._authenticationAuditService = ComponentFactory.getIfExists<IAuthenticationAuditComponent>(\n\t\t\toptions?.authenticationAuditServiceType ?? \"authentication-audit\"\n\t\t);\n\n\t\tthis._authenticationRateService = ComponentFactory.get<IAuthenticationRateComponent>(\n\t\t\toptions?.authenticationRateServiceType ?? \"authentication-rate\"\n\t\t);\n\n\t\tthis._tenantAdminComponent = ComponentFactory.getIfExists<ITenantAdminComponent>(\n\t\t\toptions?.tenantAdminComponentType ?? \"tenant-admin\"\n\t\t);\n\n\t\tthis._signingKeyName = options?.config?.signingKeyName ?? \"auth-signing\";\n\t\tthis._defaultTtlMinutes =\n\t\t\toptions?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;\n\t\tthis._minPasswordLength = options?.config?.minPasswordLength;\n\t\tthis._loginRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.loginRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.loginRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_WINDOW_MINUTES\n\t\t};\n\t\tthis._passwordChangeRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.passwordChangeRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.passwordChangeRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES\n\t\t};\n\t\tthis._tokenRefreshRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.tokenRefreshRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.tokenRefreshRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES\n\t\t};\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationService.CLASS_NAME;\n\t}\n\n\t/**\n\t * The service needs to be started when the application is initialized.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns Nothing.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.Node);\n\t\tthis._nodeId = contextIds[ContextIdKeys.Node];\n\n\t\tawait this._authenticationRateService.registerAction(\"login\", this._loginRateLimit);\n\t\tawait this._authenticationRateService.registerAction(\n\t\t\t\"password-change\",\n\t\t\tthis._passwordChangeRateLimit\n\t\t);\n\t\tawait this._authenticationRateService.registerAction(\n\t\t\t\"token-refresh\",\n\t\t\tthis._tokenRefreshRateLimit\n\t\t);\n\t}\n\n\t/**\n\t * The component needs to be stopped when the node is closed.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns Nothing.\n\t */\n\tpublic async stop(nodeLoggingComponentType?: string): Promise<void> {\n\t\tawait this._authenticationRateService.unregisterAction(\"login\");\n\t\tawait this._authenticationRateService.unregisterAction(\"password-change\");\n\t\tawait this._authenticationRateService.unregisterAction(\"token-refresh\");\n\t}\n\n\t/**\n\t * Perform a login for the user.\n\t * @param email The email address for the user.\n\t * @param password The password for the user.\n\t * @returns The authentication token for the user, if it uses a mechanism with public access.\n\t */\n\tpublic async login(\n\t\temail: string,\n\t\tpassword: string\n\t): Promise<{\n\t\ttoken?: string;\n\t\texpiry: number;\n\t}> {\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(password), password);\n\n\t\tlet loginUser: AuthenticationUser | undefined;\n\t\tlet loginTenantId: string | undefined;\n\t\tlet tokenAndExpiry: { token?: string; expiry: number } | undefined;\n\n\t\ttry {\n\t\t\tawait this._authenticationRateService.check(\"login\", email);\n\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!user) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"userNotFound\");\n\t\t\t}\n\n\t\t\tconst saltBytes = Converter.base64ToBytes(user.salt);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(password);\n\n\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tif (!PasswordValidator.comparePasswordHashes(hashedPassword, user.password)) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"passwordMismatch\");\n\t\t\t}\n\n\t\t\t// This might be undefined if the login is performed in a single tenant context\n\t\t\t// it is verified during the token processing, tenant id will be matched against\n\t\t\t// the context\n\t\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\t\tloginTenantId = contextIds?.[ContextIdKeys.Tenant];\n\n\t\t\ttokenAndExpiry = await TokenHelper.createToken(\n\t\t\t\tthis._vaultConnector,\n\t\t\t\tthis._urlTransformerService,\n\t\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\t\tuser.identity,\n\t\t\t\tuser.organization,\n\t\t\t\tloginTenantId,\n\t\t\t\tthis._defaultTtlMinutes,\n\t\t\t\tuser.scope,\n\t\t\t\tuser.passwordVersion ?? 0\n\t\t\t);\n\t\t\tloginUser = user;\n\t\t} catch (error) {\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: email,\n\t\t\t\tevent: AuthAuditEvent.LoginFailure\n\t\t\t});\n\n\t\t\tthrow new UnauthorizedError(\n\t\t\t\tEntityStorageAuthenticationService.CLASS_NAME,\n\t\t\t\t\"loginFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\n\t\tawait this._authenticationRateService.clear(\"login\", email);\n\n\t\tawait this._authenticationAuditService?.create({\n\t\t\tactorId: email,\n\t\t\tevent: AuthAuditEvent.LoginSuccess,\n\t\t\tdata: {\n\t\t\t\tuserIdentity: loginUser.identity,\n\t\t\t\torganizationIdentity: loginUser.organization,\n\t\t\t\ttenantId: loginTenantId,\n\t\t\t\tscope: loginUser.scope.split(\",\")\n\t\t\t}\n\t\t});\n\n\t\treturn tokenAndExpiry;\n\t}\n\n\t/**\n\t * Logout the current user.\n\t * @param token The token to logout, if it uses a mechanism with public access.\n\t * @returns Nothing.\n\t */\n\tpublic async logout(token?: string): Promise<void> {\n\t\t// Nothing to do here, as we are stateless.\n\t\t// The cookie will be revoked by the REST route handling\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst identifier = contextIds?.[ContextIdKeys.User];\n\t\tif (Is.stringValue(identifier)) {\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: identifier,\n\t\t\t\tevent: AuthAuditEvent.Logout\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Refresh the token.\n\t * @param token The token to refresh, if it uses a mechanism with public access.\n\t * @returns The refreshed token, if it uses a mechanism with public access.\n\t */\n\tpublic async refresh(token?: string): Promise<{\n\t\ttoken?: string;\n\t\texpiry: number;\n\t}> {\n\t\tlet refreshPasswordVersion: number | undefined;\n\t\tlet tenantId: string | undefined;\n\n\t\t// If the verify fails on the current token then it will throw an exception.\n\t\tconst headerAndPayload = await TokenHelper.verify(\n\t\t\tthis._vaultConnector,\n\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\ttoken,\n\t\t\tundefined,\n\t\t\tasync (userIdentity, organizationIdentity, encryptedTenantId, passwordVersion) => {\n\t\t\t\tconst validParts = [];\n\n\t\t\t\tconst contextIds = (await ContextIdStore.getContextIds()) ?? {};\n\n\t\t\t\t// If the token carries an encrypted tenant ID and the admin component is available,\n\t\t\t\t// decrypt and resolve the tenant first so the user lookup runs in the correct partition.\n\t\t\t\tif (Is.stringValue(encryptedTenantId)) {\n\t\t\t\t\ttenantId = await this._urlTransformerService.decryptParam(encryptedTenantId);\n\t\t\t\t\tif (Is.stringValue(tenantId)) {\n\t\t\t\t\t\tconst tenant = await this._tenantAdminComponent?.get(tenantId);\n\t\t\t\t\t\tif (!Is.empty(tenant)) {\n\t\t\t\t\t\t\tvalidParts.push(\"tenant\");\n\t\t\t\t\t\t\tcontextIds[ContextIdKeys.Tenant] = tenantId;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Wrap the user lookup in the request context so partitioned storage uses the correct tenant.\n\t\t\t\tconst user = await ContextIdStore.run(contextIds, async () =>\n\t\t\t\t\tthis._userEntityStorage.get(userIdentity, \"identity\")\n\t\t\t\t);\n\n\t\t\t\trefreshPasswordVersion = user?.passwordVersion;\n\t\t\t\tif (\n\t\t\t\t\tuser?.identity === userIdentity &&\n\t\t\t\t\t(passwordVersion ?? 0) === (refreshPasswordVersion ?? 0)\n\t\t\t\t) {\n\t\t\t\t\tvalidParts.push(\"user\");\n\t\t\t\t}\n\t\t\t\tif (user?.organization === organizationIdentity) {\n\t\t\t\t\tvalidParts.push(\"organization\");\n\t\t\t\t}\n\t\t\t\treturn validParts;\n\t\t\t}\n\t\t);\n\n\t\tconst refreshSub = headerAndPayload.payload.sub ?? \"\";\n\t\tawait this._authenticationRateService.check(\"token-refresh\", refreshSub);\n\n\t\tconst refreshTokenAndExpiry = await TokenHelper.createToken(\n\t\t\tthis._vaultConnector,\n\t\t\tthis._urlTransformerService,\n\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\trefreshSub,\n\t\t\tIs.stringValue(headerAndPayload.payload.org) ? headerAndPayload.payload.org : \"\",\n\t\t\ttenantId,\n\t\t\tthis._defaultTtlMinutes,\n\t\t\tCoerce.string(headerAndPayload.payload?.scope),\n\t\t\trefreshPasswordVersion ?? 0\n\t\t);\n\t\tconst refreshScope = Coerce.string(headerAndPayload.payload?.scope) ?? \"\";\n\n\t\tawait this._authenticationAuditService?.create({\n\t\t\tactorId: refreshSub,\n\t\t\tevent: AuthAuditEvent.TokenRefreshed,\n\t\t\tdata: {\n\t\t\t\torganizationIdentity: Is.stringValue(headerAndPayload.payload.org)\n\t\t\t\t\t? headerAndPayload.payload.org\n\t\t\t\t\t: \"\",\n\t\t\t\ttenantId,\n\t\t\t\tscope: refreshScope.split(\",\").filter(scope => scope.length > 0)\n\t\t\t}\n\t\t});\n\n\t\treturn refreshTokenAndExpiry;\n\t}\n\n\t/**\n\t * Update the user's password.\n\t * @param currentPassword The current password for the user.\n\t * @param newPassword The new password for the user.\n\t * @returns Nothing.\n\t */\n\tpublic async updatePassword(currentPassword: string, newPassword: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.User);\n\n\t\tconst userIdentity = contextIds[ContextIdKeys.User];\n\t\tawait this._authenticationRateService.check(\"password-change\", userIdentity);\n\n\t\tconst user = await this._userEntityStorage.get(userIdentity, \"identity\");\n\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\tthrow new NotFoundError(\n\t\t\t\tEntityStorageAuthenticationService.CLASS_NAME,\n\t\t\t\t\"userNotFound\",\n\t\t\t\tuserIdentity\n\t\t\t);\n\t\t}\n\n\t\tawait PasswordHelper.updatePassword(\n\t\t\tthis._userEntityStorage,\n\t\t\tthis._authenticationAuditService,\n\t\t\tuser,\n\t\t\tnewPassword,\n\t\t\tcurrentPassword,\n\t\t\tthis._minPasswordLength\n\t\t);\n\n\t\tawait this._authenticationRateService.clear(\"password-change\", userIdentity);\n\t}\n}\n"]}
@@ -1,7 +1,6 @@
1
- // Copyright 2024 IOTA Stiftung.
2
- // SPDX-License-Identifier: Apache-2.0.
3
- import { Converter, Guards } from "@twin.org/core";
4
- import { Blake2b } from "@twin.org/crypto";
1
+ import { AuthAuditEvent } from "@twin.org/api-auth-entity-storage-models";
2
+ import { Converter, GeneralError, Is, RandomHelper } from "@twin.org/core";
3
+ import { PasswordGenerator, PasswordValidator } from "@twin.org/crypto";
5
4
  /**
6
5
  * Helper class for password operations.
7
6
  */
@@ -11,19 +10,49 @@ export class PasswordHelper {
11
10
  */
12
11
  static CLASS_NAME = "PasswordHelper";
13
12
  /**
14
- * Hash the password for the user.
15
- * @param passwordBytes The password bytes.
16
- * @param saltBytes The salt bytes.
17
- * @returns The hashed password.
13
+ * Update the password for a user.
14
+ * Validates password strength, verifies the current password if provided, then hashes and stores the new password and raises an audit event.
15
+ * @param userEntityStorage The entity storage for users.
16
+ * @param authenticationAuditService The optional audit service.
17
+ * @param user The user whose password is being updated.
18
+ * @param newPassword The new password to set.
19
+ * @param currentPassword The current password to verify against, if supplied.
20
+ * @param minPasswordLength Optional minimum password length for validation.
21
+ * @returns Nothing.
18
22
  */
19
- static async hashPassword(passwordBytes, saltBytes) {
20
- Guards.uint8Array(PasswordHelper.CLASS_NAME, "passwordBytes", passwordBytes);
21
- Guards.uint8Array(PasswordHelper.CLASS_NAME, "saltBytes", saltBytes);
22
- const combined = new Uint8Array(saltBytes.length + passwordBytes.length);
23
- combined.set(saltBytes);
24
- combined.set(passwordBytes, saltBytes.length);
25
- const hashedPassword = Blake2b.sum256(combined);
26
- return Converter.bytesToBase64(hashedPassword);
23
+ static async updatePassword(userEntityStorage, authenticationAuditService, user, newPassword, currentPassword, minPasswordLength) {
24
+ PasswordValidator.validatePassword(newPassword, {
25
+ minLength: minPasswordLength
26
+ });
27
+ if (Is.stringValue(currentPassword)) {
28
+ const saltBytes = Converter.base64ToBytes(user.salt);
29
+ const passwordBytes = Converter.utf8ToBytes(currentPassword);
30
+ const hashedCurrentPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);
31
+ if (!PasswordValidator.comparePasswordHashes(hashedCurrentPassword, user.password)) {
32
+ throw new GeneralError(PasswordHelper.CLASS_NAME, "currentPasswordMismatch");
33
+ }
34
+ }
35
+ const saltBytes = RandomHelper.generate(16);
36
+ const passwordBytes = Converter.utf8ToBytes(newPassword);
37
+ const hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);
38
+ const updatedUser = {
39
+ email: user.email,
40
+ salt: Converter.bytesToBase64(saltBytes),
41
+ password: hashedPassword,
42
+ identity: user.identity,
43
+ organization: user.organization,
44
+ scope: user.scope,
45
+ passwordVersion: (user.passwordVersion ?? 0) + 1
46
+ };
47
+ await userEntityStorage.set(updatedUser);
48
+ await authenticationAuditService?.create({
49
+ actorId: user.email,
50
+ event: AuthAuditEvent.PasswordChanged,
51
+ data: {
52
+ userIdentity: updatedUser.identity,
53
+ organizationIdentity: updatedUser.organization
54
+ }
55
+ });
27
56
  }
28
57
  }
29
58
  //# sourceMappingURL=passwordHelper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"passwordHelper.js","sourceRoot":"","sources":["../../../src/utils/passwordHelper.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAG3C;;GAEG;AACH,MAAM,OAAO,cAAc;IAC1B;;OAEG;IACI,MAAM,CAAU,UAAU,oBAAoC;IAErE;;;;;OAKG;IACI,MAAM,CAAC,KAAK,CAAC,YAAY,CAC/B,aAAyB,EACzB,SAAqB;QAErB,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,UAAU,mBAAyB,aAAa,CAAC,CAAC;QACnF,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,UAAU,eAAqB,SAAS,CAAC,CAAC;QAE3E,MAAM,QAAQ,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACzE,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACxB,QAAQ,CAAC,GAAG,CAAC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;QAE9C,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAEhD,OAAO,SAAS,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IAChD,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Converter, Guards } from \"@twin.org/core\";\nimport { Blake2b } from \"@twin.org/crypto\";\nimport { nameof } from \"@twin.org/nameof\";\n\n/**\n * Helper class for password operations.\n */\nexport class PasswordHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<PasswordHelper>();\n\n\t/**\n\t * Hash the password for the user.\n\t * @param passwordBytes The password bytes.\n\t * @param saltBytes The salt bytes.\n\t * @returns The hashed password.\n\t */\n\tpublic static async hashPassword(\n\t\tpasswordBytes: Uint8Array,\n\t\tsaltBytes: Uint8Array\n\t): Promise<string> {\n\t\tGuards.uint8Array(PasswordHelper.CLASS_NAME, nameof(passwordBytes), passwordBytes);\n\t\tGuards.uint8Array(PasswordHelper.CLASS_NAME, nameof(saltBytes), saltBytes);\n\n\t\tconst combined = new Uint8Array(saltBytes.length + passwordBytes.length);\n\t\tcombined.set(saltBytes);\n\t\tcombined.set(passwordBytes, saltBytes.length);\n\n\t\tconst hashedPassword = Blake2b.sum256(combined);\n\n\t\treturn Converter.bytesToBase64(hashedPassword);\n\t}\n}\n"]}
1
+ {"version":3,"file":"passwordHelper.js","sourceRoot":"","sources":["../../../src/utils/passwordHelper.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAKxE;;GAEG;AACH,MAAM,OAAO,cAAc;IAC1B;;OAEG;IACI,MAAM,CAAU,UAAU,oBAAoC;IAErE;;;;;;;;;;OAUG;IACI,MAAM,CAAC,KAAK,CAAC,cAAc,CACjC,iBAA8D,EAC9D,0BAAqE,EACrE,IAAwB,EACxB,WAAmB,EACnB,eAAwB,EACxB,iBAA0B;QAE1B,iBAAiB,CAAC,gBAAgB,CAAC,WAAW,EAAE;YAC/C,SAAS,EAAE,iBAAiB;SAC5B,CAAC,CAAC;QAEH,IAAI,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,EAAE,CAAC;YACrC,MAAM,SAAS,GAAG,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrD,MAAM,aAAa,GAAG,SAAS,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;YAC7D,MAAM,qBAAqB,GAAG,MAAM,iBAAiB,CAAC,YAAY,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;YAC7F,IAAI,CAAC,iBAAiB,CAAC,qBAAqB,CAAC,qBAAqB,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACpF,MAAM,IAAI,YAAY,CAAC,cAAc,CAAC,UAAU,EAAE,yBAAyB,CAAC,CAAC;YAC9E,CAAC;QACF,CAAC;QAED,MAAM,SAAS,GAAG,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC5C,MAAM,aAAa,GAAG,SAAS,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QACzD,MAAM,cAAc,GAAG,MAAM,iBAAiB,CAAC,YAAY,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QAEtF,MAAM,WAAW,GAAuB;YACvC,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,IAAI,EAAE,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC;YACxC,QAAQ,EAAE,cAAc;YACxB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,eAAe,EAAE,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,CAAC,GAAG,CAAC;SAChD,CAAC;QAEF,MAAM,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACzC,MAAM,0BAA0B,EAAE,MAAM,CAAC;YACxC,OAAO,EAAE,IAAI,CAAC,KAAK;YACnB,KAAK,EAAE,cAAc,CAAC,eAAe;YACrC,IAAI,EAAE;gBACL,YAAY,EAAE,WAAW,CAAC,QAAQ;gBAClC,oBAAoB,EAAE,WAAW,CAAC,YAAY;aAC9C;SACD,CAAC,CAAC;IACJ,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IAuthenticationAuditComponent } from \"@twin.org/api-auth-entity-storage-models\";\nimport { AuthAuditEvent } from \"@twin.org/api-auth-entity-storage-models\";\nimport { Converter, GeneralError, Is, RandomHelper } from \"@twin.org/core\";\nimport { PasswordGenerator, PasswordValidator } from \"@twin.org/crypto\";\nimport type { IEntityStorageConnector } from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\n\n/**\n * Helper class for password operations.\n */\nexport class PasswordHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<PasswordHelper>();\n\n\t/**\n\t * Update the password for a user.\n\t * Validates password strength, verifies the current password if provided, then hashes and stores the new password and raises an audit event.\n\t * @param userEntityStorage The entity storage for users.\n\t * @param authenticationAuditService The optional audit service.\n\t * @param user The user whose password is being updated.\n\t * @param newPassword The new password to set.\n\t * @param currentPassword The current password to verify against, if supplied.\n\t * @param minPasswordLength Optional minimum password length for validation.\n\t * @returns Nothing.\n\t */\n\tpublic static async updatePassword(\n\t\tuserEntityStorage: IEntityStorageConnector<AuthenticationUser>,\n\t\tauthenticationAuditService: IAuthenticationAuditComponent | undefined,\n\t\tuser: AuthenticationUser,\n\t\tnewPassword: string,\n\t\tcurrentPassword?: string,\n\t\tminPasswordLength?: number\n\t): Promise<void> {\n\t\tPasswordValidator.validatePassword(newPassword, {\n\t\t\tminLength: minPasswordLength\n\t\t});\n\n\t\tif (Is.stringValue(currentPassword)) {\n\t\t\tconst saltBytes = Converter.base64ToBytes(user.salt);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(currentPassword);\n\t\t\tconst hashedCurrentPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\t\t\tif (!PasswordValidator.comparePasswordHashes(hashedCurrentPassword, user.password)) {\n\t\t\t\tthrow new GeneralError(PasswordHelper.CLASS_NAME, \"currentPasswordMismatch\");\n\t\t\t}\n\t\t}\n\n\t\tconst saltBytes = RandomHelper.generate(16);\n\t\tconst passwordBytes = Converter.utf8ToBytes(newPassword);\n\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\tconst updatedUser: AuthenticationUser = {\n\t\t\temail: user.email,\n\t\t\tsalt: Converter.bytesToBase64(saltBytes),\n\t\t\tpassword: hashedPassword,\n\t\t\tidentity: user.identity,\n\t\t\torganization: user.organization,\n\t\t\tscope: user.scope,\n\t\t\tpasswordVersion: (user.passwordVersion ?? 0) + 1\n\t\t};\n\n\t\tawait userEntityStorage.set(updatedUser);\n\t\tawait authenticationAuditService?.create({\n\t\t\tactorId: user.email,\n\t\t\tevent: AuthAuditEvent.PasswordChanged,\n\t\t\tdata: {\n\t\t\t\tuserIdentity: updatedUser.identity,\n\t\t\t\torganizationIdentity: updatedUser.organization\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
@@ -1,8 +1,6 @@
1
- // Copyright 2024 IOTA Stiftung.
2
- // SPDX-License-Identifier: Apache-2.0.
3
- import { Is, UnauthorizedError } from "@twin.org/core";
1
+ import { Coerce, Is, UnauthorizedError } from "@twin.org/core";
4
2
  import { VaultConnectorHelper } from "@twin.org/vault-models";
5
- import { HeaderHelper, HeaderTypes, Jwt } from "@twin.org/web";
3
+ import { CookieHelper, HeaderHelper, HeaderTypes, Jwt } from "@twin.org/web";
6
4
  /**
7
5
  * Helper class for token operations.
8
6
  */
@@ -14,19 +12,28 @@ export class TokenHelper {
14
12
  /**
15
13
  * Create a new token.
16
14
  * @param vaultConnector The vault connector.
15
+ * @param urlTransformerComponent The URL transformer component, used to encrypt the tenant ID for inclusion in the token.
17
16
  * @param signingKeyName The signing key name.
18
17
  * @param userIdentity The subject for the token.
19
18
  * @param organizationIdentity The organization for the token.
19
+ * @param tenantId The tenant id for the token.
20
20
  * @param ttlMinutes The time to live for the token in minutes.
21
+ * @param scope The scopes for the token.
22
+ * @param passwordVersion The user's current password version counter, embedded in the token so that a password change invalidates existing tokens.
21
23
  * @returns The new token and its expiry date.
22
24
  */
23
- static async createToken(vaultConnector, signingKeyName, userIdentity, organizationIdentity, ttlMinutes) {
25
+ static async createToken(vaultConnector, urlTransformerComponent, signingKeyName, userIdentity, organizationIdentity, tenantId, ttlMinutes, scope, passwordVersion) {
24
26
  const nowSeconds = Math.trunc(Date.now() / 1000);
25
27
  const ttlSeconds = ttlMinutes * 60;
26
28
  const jwt = await Jwt.encodeWithSigner({ alg: "EdDSA" }, {
27
29
  sub: userIdentity,
28
30
  org: organizationIdentity,
29
- exp: nowSeconds + ttlSeconds
31
+ tid: Is.stringValue(tenantId)
32
+ ? await urlTransformerComponent.encryptParam(tenantId)
33
+ : undefined,
34
+ exp: nowSeconds + ttlSeconds,
35
+ scope,
36
+ pver: passwordVersion
30
37
  }, async (header, payload) => VaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload));
31
38
  return {
32
39
  token: jwt,
@@ -38,10 +45,12 @@ export class TokenHelper {
38
45
  * @param vaultConnector The vault connector.
39
46
  * @param signingKeyName The signing key name.
40
47
  * @param token The token to verify.
48
+ * @param requiredScopes The required scopes.
49
+ * @param verifyUser A function to verify the user identity and organization. The password version counter embedded in the token (pver claim) is passed so callers can detect if the password has changed since the token was issued.
41
50
  * @returns The verified details.
42
51
  * @throws UnauthorizedError if the token is missing, invalid or expired.
43
52
  */
44
- static async verify(vaultConnector, signingKeyName, token) {
53
+ static async verify(vaultConnector, signingKeyName, token, requiredScopes, verifyUser) {
45
54
  if (!Is.stringValue(token)) {
46
55
  throw new UnauthorizedError(TokenHelper.CLASS_NAME, "missing");
47
56
  }
@@ -57,6 +66,29 @@ export class TokenHelper {
57
66
  decoded.payload.exp < Math.trunc(Date.now() / 1000)) {
58
67
  throw new UnauthorizedError(TokenHelper.CLASS_NAME, "expired");
59
68
  }
69
+ if (Is.function(verifyUser)) {
70
+ const encryptedTenantId = Coerce.string(decoded.payload.tid);
71
+ const userVerified = await verifyUser(decoded.payload.sub, decoded.payload.org, encryptedTenantId, Coerce.integer(decoded.payload.pver));
72
+ if (!userVerified.includes("user")) {
73
+ throw new UnauthorizedError(TokenHelper.CLASS_NAME, "userNotVerified");
74
+ }
75
+ else if (!userVerified.includes("organization")) {
76
+ throw new UnauthorizedError(TokenHelper.CLASS_NAME, "organizationNotVerified");
77
+ }
78
+ else if (Is.stringValue(encryptedTenantId) && !userVerified.includes("tenant")) {
79
+ throw new UnauthorizedError(TokenHelper.CLASS_NAME, "tenantNotVerified");
80
+ }
81
+ }
82
+ if (Is.arrayValue(requiredScopes)) {
83
+ const tokenScopes = Is.stringValue(decoded.payload.scope)
84
+ ? decoded.payload.scope.split(",")
85
+ : [];
86
+ for (const requiredScope of requiredScopes) {
87
+ if (!tokenScopes.includes(requiredScope)) {
88
+ throw new UnauthorizedError(TokenHelper.CLASS_NAME, "insufficientScopes");
89
+ }
90
+ }
91
+ }
60
92
  return {
61
93
  header: decoded.header,
62
94
  payload: decoded.payload
@@ -79,20 +111,12 @@ export class TokenHelper {
79
111
  };
80
112
  }
81
113
  else if (Is.notEmpty(cookiesHeader) && Is.stringValue(cookieName)) {
82
- const cookies = Is.arrayValue(cookiesHeader) ? cookiesHeader : [cookiesHeader];
83
- for (const cookie of cookies) {
84
- if (Is.stringValue(cookie)) {
85
- const accessTokenCookie = cookie
86
- .split(";")
87
- .map(c => c.trim())
88
- .find(c => c.startsWith(cookieName));
89
- if (Is.stringValue(accessTokenCookie)) {
90
- return {
91
- token: accessTokenCookie.slice(cookieName.length + 1).trim(),
92
- location: "cookie"
93
- };
94
- }
95
- }
114
+ const value = CookieHelper.getCookieFromHeaders(cookiesHeader, cookieName);
115
+ if (Is.stringValue(value)) {
116
+ return {
117
+ token: value,
118
+ location: "cookie"
119
+ };
96
120
  }
97
121
  }
98
122
  }