@twin.org/api-auth-entity-storage-service 0.0.1-next.9 → 0.0.2-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/cjs/index.cjs +221 -19
  2. package/dist/esm/index.mjs +223 -23
  3. package/dist/types/index.d.ts +5 -0
  4. package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +15 -0
  5. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConfig.d.ts +10 -0
  6. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +15 -0
  7. package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +25 -0
  8. package/dist/types/processors/authHeaderProcessor.d.ts +10 -11
  9. package/dist/types/routes/entityStorageAuthenticationRoutes.d.ts +9 -1
  10. package/dist/types/services/entityStorageAuthenticationAdminService.d.ts +42 -0
  11. package/dist/types/services/entityStorageAuthenticationService.d.ts +14 -9
  12. package/dist/types/utils/tokenHelper.d.ts +1 -1
  13. package/docs/changelog.md +115 -1
  14. package/docs/reference/classes/AuthHeaderProcessor.md +50 -28
  15. package/docs/reference/classes/AuthenticationUser.md +3 -3
  16. package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +149 -0
  17. package/docs/reference/classes/EntityStorageAuthenticationService.md +71 -39
  18. package/docs/reference/classes/PasswordHelper.md +9 -5
  19. package/docs/reference/classes/TokenHelper.md +36 -34
  20. package/docs/reference/functions/authenticationLogin.md +9 -3
  21. package/docs/reference/functions/authenticationLogout.md +9 -3
  22. package/docs/reference/functions/authenticationRefreshToken.md +9 -3
  23. package/docs/reference/functions/authenticationUpdatePassword.md +31 -0
  24. package/docs/reference/functions/generateRestRoutesAuthentication.md +8 -4
  25. package/docs/reference/index.md +6 -0
  26. package/docs/reference/interfaces/IAuthHeaderProcessorConstructorOptions.md +25 -0
  27. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConfig.md +17 -0
  28. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConstructorOptions.md +25 -0
  29. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConstructorOptions.md +53 -0
  30. package/locales/en.json +10 -1
  31. package/package.json +6 -6
@@ -71,13 +71,12 @@ class TokenHelper {
71
71
  * @returns The new token and its expiry date.
72
72
  */
73
73
  static async createToken(vaultConnector, signingKeyName, subject, ttlMinutes) {
74
- // Verify was a success so we can now generate a new token.
75
74
  const nowSeconds = Math.trunc(Date.now() / 1000);
76
75
  const ttlSeconds = ttlMinutes * 60;
77
- const jwt = await web.Jwt.encodeWithSigner({ alg: web.JwtAlgorithms.EdDSA }, {
76
+ const jwt = await web.Jwt.encodeWithSigner({ alg: "EdDSA" }, {
78
77
  sub: subject,
79
78
  exp: nowSeconds + ttlSeconds
80
- }, async (alg, key, payload) => vaultConnector.sign(signingKeyName, payload));
79
+ }, async (header, payload) => vaultModels.VaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload));
81
80
  return {
82
81
  token: jwt,
83
82
  expiry: (nowSeconds + ttlSeconds) * 1000
@@ -95,14 +94,10 @@ class TokenHelper {
95
94
  if (!core.Is.stringValue(token)) {
96
95
  throw new core.UnauthorizedError(this._CLASS_NAME, "missing");
97
96
  }
98
- const decoded = await web.Jwt.verifyWithVerifier(token, async (alg, key, payload, signature) => vaultConnector.verify(signingKeyName, payload, signature));
99
- // If the signature validation failed or some of the header/payload data
100
- // is not properly populated then it is unauthorized.
101
- if (!decoded.verified ||
102
- !core.Is.object(decoded.header) ||
103
- !core.Is.object(decoded.payload) ||
104
- !core.Is.stringValue(decoded.payload.sub)) {
105
- throw new core.UnauthorizedError(this._CLASS_NAME, "invalidToken");
97
+ const decoded = await web.Jwt.verifyWithVerifier(token, async (t) => vaultModels.VaultConnectorHelper.jwtVerifier(vaultConnector, signingKeyName, t));
98
+ // If some of the header/payload data is not properly populated then it is unauthorized.
99
+ if (!core.Is.stringValue(decoded.payload.sub)) {
100
+ throw new core.UnauthorizedError(this._CLASS_NAME, "payloadMissingSubject");
106
101
  }
107
102
  else if (!core.Is.empty(decoded.payload?.exp) &&
108
103
  decoded.payload.exp < Math.trunc(Date.now() / 1000)) {
@@ -154,6 +149,10 @@ class TokenHelper {
154
149
  * Handle a JWT token in the authorization header or cookies and validate it to populate request context identity.
155
150
  */
156
151
  class AuthHeaderProcessor {
152
+ /**
153
+ * The namespace supported by the processor.
154
+ */
155
+ static NAMESPACE = "auth-header";
157
156
  /**
158
157
  * The default name for the access token as a cookie.
159
158
  * @internal
@@ -186,8 +185,6 @@ class AuthHeaderProcessor {
186
185
  /**
187
186
  * Create a new instance of AuthCookiePreProcessor.
188
187
  * @param options Options for the processor.
189
- * @param options.vaultConnectorType The vault for the private keys, defaults to "vault".
190
- * @param options.config The configuration for the processor.
191
188
  */
192
189
  constructor(options) {
193
190
  this._vaultConnector = vaultModels.VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
@@ -244,13 +241,13 @@ class AuthHeaderProcessor {
244
241
  if ((responseAuthOperation === "login" || responseAuthOperation === "refresh") &&
245
242
  core.Is.stringValue(response.body?.token)) {
246
243
  response.headers ??= {};
247
- response.headers["Set-Cookie"] =
244
+ response.headers[web.HeaderTypes.SetCookie] =
248
245
  `${this._cookieName}=${response.body.token}; Secure; HttpOnly; SameSite=None; Path=/`;
249
246
  delete response.body.token;
250
247
  }
251
248
  else if (responseAuthOperation === "logout") {
252
249
  response.headers ??= {};
253
- response.headers["Set-Cookie"] =
250
+ response.headers[web.HeaderTypes.SetCookie] =
254
251
  `${this._cookieName}=; Max-Age=0; Secure; HttpOnly; SameSite=None; Path=/`;
255
252
  }
256
253
  }
@@ -391,7 +388,41 @@ function generateRestRoutesAuthentication(baseRouteName, componentName) {
391
388
  }
392
389
  ]
393
390
  };
394
- return [loginRoute, logoutRoute, refreshTokenRoute];
391
+ const updatePasswordRoute = {
392
+ operationId: "authenticationUpdatePassword",
393
+ summary: "Update the user's password",
394
+ tag: tagsAuthentication[0].name,
395
+ method: "PUT",
396
+ path: `${baseRouteName}/:email/password`,
397
+ handler: async (httpRequestContext, request) => authenticationUpdatePassword(httpRequestContext, componentName, request),
398
+ requestType: {
399
+ type: "IUpdatePasswordRequest",
400
+ examples: [
401
+ {
402
+ id: "updatePasswordRequestExample",
403
+ description: "The request to update the user's password.",
404
+ request: {
405
+ pathParams: {
406
+ email: "john:example.com"
407
+ },
408
+ body: {
409
+ currentPassword: "MyNewPassword123!",
410
+ newPassword: "MyNewPassword123!"
411
+ }
412
+ }
413
+ }
414
+ ]
415
+ },
416
+ responseType: [
417
+ {
418
+ type: "INoContentResponse"
419
+ },
420
+ {
421
+ type: "IUnauthorizedResponse"
422
+ }
423
+ ]
424
+ };
425
+ return [loginRoute, logoutRoute, refreshTokenRoute, updatePasswordRoute];
395
426
  }
396
427
  /**
397
428
  * Login to the server.
@@ -451,6 +482,23 @@ async function authenticationRefreshToken(httpRequestContext, componentName, req
451
482
  body: result
452
483
  };
453
484
  }
485
+ /**
486
+ * Update the user's password.
487
+ * @param httpRequestContext The request context for the API.
488
+ * @param componentName The name of the component to use in the routes.
489
+ * @param request The request.
490
+ * @returns The response object with additional http response properties.
491
+ */
492
+ async function authenticationUpdatePassword(httpRequestContext, componentName, request) {
493
+ core.Guards.object(ROUTES_SOURCE, "request", request);
494
+ core.Guards.object(ROUTES_SOURCE, "request.pathParams", request.pathParams);
495
+ core.Guards.object(ROUTES_SOURCE, "request.body", request.body);
496
+ const component = core.ComponentFactory.get(componentName);
497
+ await component.updatePassword(request.pathParams.email, request.body.currentPassword, request.body.newPassword);
498
+ return {
499
+ statusCode: web.HttpStatusCode.noContent
500
+ };
501
+ }
454
502
 
455
503
  const restEntryPoints = [
456
504
  {
@@ -498,10 +546,149 @@ class PasswordHelper {
498
546
  }
499
547
  }
500
548
 
549
+ /**
550
+ * Implementation of the authentication component using entity storage.
551
+ */
552
+ class EntityStorageAuthenticationAdminService {
553
+ /**
554
+ * The namespace supported by the authentication service.
555
+ */
556
+ static NAMESPACE = "authentication-admin-entity-storage";
557
+ /**
558
+ * The minimum password length.
559
+ * @internal
560
+ */
561
+ static _DEFAULT_MIN_PASSWORD_LENGTH = 8;
562
+ /**
563
+ * Runtime name for the class.
564
+ */
565
+ CLASS_NAME = "EntityStorageAuthenticationAdminService";
566
+ /**
567
+ * The entity storage for users.
568
+ * @internal
569
+ */
570
+ _userEntityStorage;
571
+ /**
572
+ * The minimum password length.
573
+ * @internal
574
+ */
575
+ _minPasswordLength;
576
+ /**
577
+ * Create a new instance of EntityStorageAuthentication.
578
+ * @param options The dependencies for the identity connector.
579
+ */
580
+ constructor(options) {
581
+ this._userEntityStorage = entityStorageModels.EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
582
+ this._minPasswordLength =
583
+ options?.config?.minPasswordLength ??
584
+ EntityStorageAuthenticationAdminService._DEFAULT_MIN_PASSWORD_LENGTH;
585
+ }
586
+ /**
587
+ * Create a login for the user.
588
+ * @param email The email address for the user.
589
+ * @param password The password for the user.
590
+ * @param identity The DID to associate with the account.
591
+ * @returns Nothing.
592
+ */
593
+ async create(email, password, identity) {
594
+ core.Guards.stringValue(this.CLASS_NAME, "email", email);
595
+ core.Guards.stringValue(this.CLASS_NAME, "password", password);
596
+ try {
597
+ if (password.length < this._minPasswordLength) {
598
+ throw new core.GeneralError(this.CLASS_NAME, "passwordTooShort", {
599
+ minLength: this._minPasswordLength
600
+ });
601
+ }
602
+ const user = await this._userEntityStorage.get(email);
603
+ if (user) {
604
+ throw new core.GeneralError(this.CLASS_NAME, "userExists");
605
+ }
606
+ const saltBytes = core.RandomHelper.generate(16);
607
+ const passwordBytes = core.Converter.utf8ToBytes(password);
608
+ const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
609
+ const newUser = {
610
+ email,
611
+ salt: core.Converter.bytesToBase64(saltBytes),
612
+ password: hashedPassword,
613
+ identity
614
+ };
615
+ await this._userEntityStorage.set(newUser);
616
+ }
617
+ catch (error) {
618
+ throw new core.GeneralError(this.CLASS_NAME, "createUserFailed", undefined, error);
619
+ }
620
+ }
621
+ /**
622
+ * Remove the current user.
623
+ * @param email The email address of the user to remove.
624
+ * @returns Nothing.
625
+ */
626
+ async remove(email) {
627
+ core.Guards.stringValue(this.CLASS_NAME, "email", email);
628
+ try {
629
+ const user = await this._userEntityStorage.get(email);
630
+ if (!user) {
631
+ throw new core.NotFoundError(this.CLASS_NAME, "userNotFound", email);
632
+ }
633
+ await this._userEntityStorage.remove(email);
634
+ }
635
+ catch (error) {
636
+ throw new core.GeneralError(this.CLASS_NAME, "removeUserFailed", undefined, error);
637
+ }
638
+ }
639
+ /**
640
+ * Update the user's password.
641
+ * @param email The email address of the user to update.
642
+ * @param newPassword The new password for the user.
643
+ * @param currentPassword The current password, optional, if supplied will check against existing.
644
+ * @returns Nothing.
645
+ */
646
+ async updatePassword(email, newPassword, currentPassword) {
647
+ core.Guards.stringValue(this.CLASS_NAME, "email", email);
648
+ core.Guards.stringValue(this.CLASS_NAME, "newPassword", newPassword);
649
+ try {
650
+ if (newPassword.length < this._minPasswordLength) {
651
+ throw new core.GeneralError(this.CLASS_NAME, "passwordTooShort", {
652
+ minLength: this._minPasswordLength
653
+ });
654
+ }
655
+ const user = await this._userEntityStorage.get(email);
656
+ if (!user) {
657
+ throw new core.NotFoundError(this.CLASS_NAME, "userNotFound", email);
658
+ }
659
+ if (core.Is.stringValue(currentPassword)) {
660
+ const saltBytes = core.Converter.base64ToBytes(user.salt);
661
+ const passwordBytes = core.Converter.utf8ToBytes(currentPassword);
662
+ const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
663
+ if (hashedPassword !== user.password) {
664
+ throw new core.GeneralError(this.CLASS_NAME, "currentPasswordMismatch");
665
+ }
666
+ }
667
+ const saltBytes = core.RandomHelper.generate(16);
668
+ const passwordBytes = core.Converter.utf8ToBytes(newPassword);
669
+ const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
670
+ const updatedUser = {
671
+ email,
672
+ salt: core.Converter.bytesToBase64(saltBytes),
673
+ password: hashedPassword,
674
+ identity: user.identity
675
+ };
676
+ await this._userEntityStorage.set(updatedUser);
677
+ }
678
+ catch (error) {
679
+ throw new core.GeneralError(this.CLASS_NAME, "updatePasswordFailed", undefined, error);
680
+ }
681
+ }
682
+ }
683
+
501
684
  /**
502
685
  * Implementation of the authentication component using entity storage.
503
686
  */
504
687
  class EntityStorageAuthenticationService {
688
+ /**
689
+ * The namespace supported by the authentication service.
690
+ */
691
+ static NAMESPACE = "authentication-entity-storage";
505
692
  /**
506
693
  * Default TTL in minutes.
507
694
  * @internal
@@ -511,6 +698,11 @@ class EntityStorageAuthenticationService {
511
698
  * Runtime name for the class.
512
699
  */
513
700
  CLASS_NAME = "EntityStorageAuthenticationService";
701
+ /**
702
+ * The user admin service.
703
+ * @internal
704
+ */
705
+ _authenticationAdminService;
514
706
  /**
515
707
  * The entity storage for users.
516
708
  * @internal
@@ -539,13 +731,11 @@ class EntityStorageAuthenticationService {
539
731
  /**
540
732
  * Create a new instance of EntityStorageAuthentication.
541
733
  * @param options The dependencies for the identity connector.
542
- * @param options.userEntityStorageType The entity storage for the users, defaults to "authentication-user".
543
- * @param options.vaultConnectorType The vault for the private keys, defaults to "vault".
544
- * @param options.config The configuration for the authentication.
545
734
  */
546
735
  constructor(options) {
547
736
  this._userEntityStorage = entityStorageModels.EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
548
737
  this._vaultConnector = vaultModels.VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
738
+ this._authenticationAdminService = core.ComponentFactory.get(options?.authenticationAdminServiceType ?? "authentication-admin");
549
739
  this._signingKeyName = options?.config?.signingKeyName ?? "auth-signing";
550
740
  this._defaultTtlMinutes =
551
741
  options?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;
@@ -606,15 +796,27 @@ class EntityStorageAuthenticationService {
606
796
  const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeIdentity}/${this._signingKeyName}`, headerAndPayload.payload.sub ?? "", this._defaultTtlMinutes);
607
797
  return refreshTokenAndExpiry;
608
798
  }
799
+ /**
800
+ * Update the user's password.
801
+ * @param email The email address of the user to update.
802
+ * @param currentPassword The current password for the user.
803
+ * @param newPassword The new password for the user.
804
+ * @returns Nothing.
805
+ */
806
+ async updatePassword(email, currentPassword, newPassword) {
807
+ return this._authenticationAdminService.updatePassword(email, newPassword, currentPassword);
808
+ }
609
809
  }
610
810
 
611
811
  exports.AuthHeaderProcessor = AuthHeaderProcessor;
812
+ exports.EntityStorageAuthenticationAdminService = EntityStorageAuthenticationAdminService;
612
813
  exports.EntityStorageAuthenticationService = EntityStorageAuthenticationService;
613
814
  exports.PasswordHelper = PasswordHelper;
614
815
  exports.TokenHelper = TokenHelper;
615
816
  exports.authenticationLogin = authenticationLogin;
616
817
  exports.authenticationLogout = authenticationLogout;
617
818
  exports.authenticationRefreshToken = authenticationRefreshToken;
819
+ exports.authenticationUpdatePassword = authenticationUpdatePassword;
618
820
  exports.generateRestRoutesAuthentication = generateRestRoutesAuthentication;
619
821
  exports.initSchema = initSchema;
620
822
  exports.restEntryPoints = restEntryPoints;
@@ -1,8 +1,8 @@
1
1
  import { property, entity, EntitySchemaFactory, EntitySchemaHelper } from '@twin.org/entity';
2
2
  import { HttpErrorHelper } from '@twin.org/api-models';
3
- import { Is, UnauthorizedError, Guards, BaseError, ComponentFactory, Converter, GeneralError } from '@twin.org/core';
4
- import { VaultConnectorFactory } from '@twin.org/vault-models';
5
- import { Jwt, JwtAlgorithms, HeaderTypes, HttpStatusCode } from '@twin.org/web';
3
+ import { Is, UnauthorizedError, Guards, BaseError, ComponentFactory, Converter, GeneralError, RandomHelper, NotFoundError } from '@twin.org/core';
4
+ import { VaultConnectorHelper, VaultConnectorFactory } from '@twin.org/vault-models';
5
+ import { Jwt, HeaderTypes, HttpStatusCode } from '@twin.org/web';
6
6
  import { EntityStorageConnectorFactory } from '@twin.org/entity-storage-models';
7
7
  import { Blake2b } from '@twin.org/crypto';
8
8
 
@@ -69,13 +69,12 @@ class TokenHelper {
69
69
  * @returns The new token and its expiry date.
70
70
  */
71
71
  static async createToken(vaultConnector, signingKeyName, subject, ttlMinutes) {
72
- // Verify was a success so we can now generate a new token.
73
72
  const nowSeconds = Math.trunc(Date.now() / 1000);
74
73
  const ttlSeconds = ttlMinutes * 60;
75
- const jwt = await Jwt.encodeWithSigner({ alg: JwtAlgorithms.EdDSA }, {
74
+ const jwt = await Jwt.encodeWithSigner({ alg: "EdDSA" }, {
76
75
  sub: subject,
77
76
  exp: nowSeconds + ttlSeconds
78
- }, async (alg, key, payload) => vaultConnector.sign(signingKeyName, payload));
77
+ }, async (header, payload) => VaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload));
79
78
  return {
80
79
  token: jwt,
81
80
  expiry: (nowSeconds + ttlSeconds) * 1000
@@ -93,14 +92,10 @@ class TokenHelper {
93
92
  if (!Is.stringValue(token)) {
94
93
  throw new UnauthorizedError(this._CLASS_NAME, "missing");
95
94
  }
96
- const decoded = await Jwt.verifyWithVerifier(token, async (alg, key, payload, signature) => vaultConnector.verify(signingKeyName, payload, signature));
97
- // If the signature validation failed or some of the header/payload data
98
- // is not properly populated then it is unauthorized.
99
- if (!decoded.verified ||
100
- !Is.object(decoded.header) ||
101
- !Is.object(decoded.payload) ||
102
- !Is.stringValue(decoded.payload.sub)) {
103
- throw new UnauthorizedError(this._CLASS_NAME, "invalidToken");
95
+ const decoded = await Jwt.verifyWithVerifier(token, async (t) => VaultConnectorHelper.jwtVerifier(vaultConnector, signingKeyName, t));
96
+ // If some of the header/payload data is not properly populated then it is unauthorized.
97
+ if (!Is.stringValue(decoded.payload.sub)) {
98
+ throw new UnauthorizedError(this._CLASS_NAME, "payloadMissingSubject");
104
99
  }
105
100
  else if (!Is.empty(decoded.payload?.exp) &&
106
101
  decoded.payload.exp < Math.trunc(Date.now() / 1000)) {
@@ -152,6 +147,10 @@ class TokenHelper {
152
147
  * Handle a JWT token in the authorization header or cookies and validate it to populate request context identity.
153
148
  */
154
149
  class AuthHeaderProcessor {
150
+ /**
151
+ * The namespace supported by the processor.
152
+ */
153
+ static NAMESPACE = "auth-header";
155
154
  /**
156
155
  * The default name for the access token as a cookie.
157
156
  * @internal
@@ -184,8 +183,6 @@ class AuthHeaderProcessor {
184
183
  /**
185
184
  * Create a new instance of AuthCookiePreProcessor.
186
185
  * @param options Options for the processor.
187
- * @param options.vaultConnectorType The vault for the private keys, defaults to "vault".
188
- * @param options.config The configuration for the processor.
189
186
  */
190
187
  constructor(options) {
191
188
  this._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
@@ -242,13 +239,13 @@ class AuthHeaderProcessor {
242
239
  if ((responseAuthOperation === "login" || responseAuthOperation === "refresh") &&
243
240
  Is.stringValue(response.body?.token)) {
244
241
  response.headers ??= {};
245
- response.headers["Set-Cookie"] =
242
+ response.headers[HeaderTypes.SetCookie] =
246
243
  `${this._cookieName}=${response.body.token}; Secure; HttpOnly; SameSite=None; Path=/`;
247
244
  delete response.body.token;
248
245
  }
249
246
  else if (responseAuthOperation === "logout") {
250
247
  response.headers ??= {};
251
- response.headers["Set-Cookie"] =
248
+ response.headers[HeaderTypes.SetCookie] =
252
249
  `${this._cookieName}=; Max-Age=0; Secure; HttpOnly; SameSite=None; Path=/`;
253
250
  }
254
251
  }
@@ -389,7 +386,41 @@ function generateRestRoutesAuthentication(baseRouteName, componentName) {
389
386
  }
390
387
  ]
391
388
  };
392
- return [loginRoute, logoutRoute, refreshTokenRoute];
389
+ const updatePasswordRoute = {
390
+ operationId: "authenticationUpdatePassword",
391
+ summary: "Update the user's password",
392
+ tag: tagsAuthentication[0].name,
393
+ method: "PUT",
394
+ path: `${baseRouteName}/:email/password`,
395
+ handler: async (httpRequestContext, request) => authenticationUpdatePassword(httpRequestContext, componentName, request),
396
+ requestType: {
397
+ type: "IUpdatePasswordRequest",
398
+ examples: [
399
+ {
400
+ id: "updatePasswordRequestExample",
401
+ description: "The request to update the user's password.",
402
+ request: {
403
+ pathParams: {
404
+ email: "john:example.com"
405
+ },
406
+ body: {
407
+ currentPassword: "MyNewPassword123!",
408
+ newPassword: "MyNewPassword123!"
409
+ }
410
+ }
411
+ }
412
+ ]
413
+ },
414
+ responseType: [
415
+ {
416
+ type: "INoContentResponse"
417
+ },
418
+ {
419
+ type: "IUnauthorizedResponse"
420
+ }
421
+ ]
422
+ };
423
+ return [loginRoute, logoutRoute, refreshTokenRoute, updatePasswordRoute];
393
424
  }
394
425
  /**
395
426
  * Login to the server.
@@ -449,6 +480,23 @@ async function authenticationRefreshToken(httpRequestContext, componentName, req
449
480
  body: result
450
481
  };
451
482
  }
483
+ /**
484
+ * Update the user's password.
485
+ * @param httpRequestContext The request context for the API.
486
+ * @param componentName The name of the component to use in the routes.
487
+ * @param request The request.
488
+ * @returns The response object with additional http response properties.
489
+ */
490
+ async function authenticationUpdatePassword(httpRequestContext, componentName, request) {
491
+ Guards.object(ROUTES_SOURCE, "request", request);
492
+ Guards.object(ROUTES_SOURCE, "request.pathParams", request.pathParams);
493
+ Guards.object(ROUTES_SOURCE, "request.body", request.body);
494
+ const component = ComponentFactory.get(componentName);
495
+ await component.updatePassword(request.pathParams.email, request.body.currentPassword, request.body.newPassword);
496
+ return {
497
+ statusCode: HttpStatusCode.noContent
498
+ };
499
+ }
452
500
 
453
501
  const restEntryPoints = [
454
502
  {
@@ -496,10 +544,149 @@ class PasswordHelper {
496
544
  }
497
545
  }
498
546
 
547
+ /**
548
+ * Implementation of the authentication component using entity storage.
549
+ */
550
+ class EntityStorageAuthenticationAdminService {
551
+ /**
552
+ * The namespace supported by the authentication service.
553
+ */
554
+ static NAMESPACE = "authentication-admin-entity-storage";
555
+ /**
556
+ * The minimum password length.
557
+ * @internal
558
+ */
559
+ static _DEFAULT_MIN_PASSWORD_LENGTH = 8;
560
+ /**
561
+ * Runtime name for the class.
562
+ */
563
+ CLASS_NAME = "EntityStorageAuthenticationAdminService";
564
+ /**
565
+ * The entity storage for users.
566
+ * @internal
567
+ */
568
+ _userEntityStorage;
569
+ /**
570
+ * The minimum password length.
571
+ * @internal
572
+ */
573
+ _minPasswordLength;
574
+ /**
575
+ * Create a new instance of EntityStorageAuthentication.
576
+ * @param options The dependencies for the identity connector.
577
+ */
578
+ constructor(options) {
579
+ this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
580
+ this._minPasswordLength =
581
+ options?.config?.minPasswordLength ??
582
+ EntityStorageAuthenticationAdminService._DEFAULT_MIN_PASSWORD_LENGTH;
583
+ }
584
+ /**
585
+ * Create a login for the user.
586
+ * @param email The email address for the user.
587
+ * @param password The password for the user.
588
+ * @param identity The DID to associate with the account.
589
+ * @returns Nothing.
590
+ */
591
+ async create(email, password, identity) {
592
+ Guards.stringValue(this.CLASS_NAME, "email", email);
593
+ Guards.stringValue(this.CLASS_NAME, "password", password);
594
+ try {
595
+ if (password.length < this._minPasswordLength) {
596
+ throw new GeneralError(this.CLASS_NAME, "passwordTooShort", {
597
+ minLength: this._minPasswordLength
598
+ });
599
+ }
600
+ const user = await this._userEntityStorage.get(email);
601
+ if (user) {
602
+ throw new GeneralError(this.CLASS_NAME, "userExists");
603
+ }
604
+ const saltBytes = RandomHelper.generate(16);
605
+ const passwordBytes = Converter.utf8ToBytes(password);
606
+ const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
607
+ const newUser = {
608
+ email,
609
+ salt: Converter.bytesToBase64(saltBytes),
610
+ password: hashedPassword,
611
+ identity
612
+ };
613
+ await this._userEntityStorage.set(newUser);
614
+ }
615
+ catch (error) {
616
+ throw new GeneralError(this.CLASS_NAME, "createUserFailed", undefined, error);
617
+ }
618
+ }
619
+ /**
620
+ * Remove the current user.
621
+ * @param email The email address of the user to remove.
622
+ * @returns Nothing.
623
+ */
624
+ async remove(email) {
625
+ Guards.stringValue(this.CLASS_NAME, "email", email);
626
+ try {
627
+ const user = await this._userEntityStorage.get(email);
628
+ if (!user) {
629
+ throw new NotFoundError(this.CLASS_NAME, "userNotFound", email);
630
+ }
631
+ await this._userEntityStorage.remove(email);
632
+ }
633
+ catch (error) {
634
+ throw new GeneralError(this.CLASS_NAME, "removeUserFailed", undefined, error);
635
+ }
636
+ }
637
+ /**
638
+ * Update the user's password.
639
+ * @param email The email address of the user to update.
640
+ * @param newPassword The new password for the user.
641
+ * @param currentPassword The current password, optional, if supplied will check against existing.
642
+ * @returns Nothing.
643
+ */
644
+ async updatePassword(email, newPassword, currentPassword) {
645
+ Guards.stringValue(this.CLASS_NAME, "email", email);
646
+ Guards.stringValue(this.CLASS_NAME, "newPassword", newPassword);
647
+ try {
648
+ if (newPassword.length < this._minPasswordLength) {
649
+ throw new GeneralError(this.CLASS_NAME, "passwordTooShort", {
650
+ minLength: this._minPasswordLength
651
+ });
652
+ }
653
+ const user = await this._userEntityStorage.get(email);
654
+ if (!user) {
655
+ throw new NotFoundError(this.CLASS_NAME, "userNotFound", email);
656
+ }
657
+ if (Is.stringValue(currentPassword)) {
658
+ const saltBytes = Converter.base64ToBytes(user.salt);
659
+ const passwordBytes = Converter.utf8ToBytes(currentPassword);
660
+ const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
661
+ if (hashedPassword !== user.password) {
662
+ throw new GeneralError(this.CLASS_NAME, "currentPasswordMismatch");
663
+ }
664
+ }
665
+ const saltBytes = RandomHelper.generate(16);
666
+ const passwordBytes = Converter.utf8ToBytes(newPassword);
667
+ const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
668
+ const updatedUser = {
669
+ email,
670
+ salt: Converter.bytesToBase64(saltBytes),
671
+ password: hashedPassword,
672
+ identity: user.identity
673
+ };
674
+ await this._userEntityStorage.set(updatedUser);
675
+ }
676
+ catch (error) {
677
+ throw new GeneralError(this.CLASS_NAME, "updatePasswordFailed", undefined, error);
678
+ }
679
+ }
680
+ }
681
+
499
682
  /**
500
683
  * Implementation of the authentication component using entity storage.
501
684
  */
502
685
  class EntityStorageAuthenticationService {
686
+ /**
687
+ * The namespace supported by the authentication service.
688
+ */
689
+ static NAMESPACE = "authentication-entity-storage";
503
690
  /**
504
691
  * Default TTL in minutes.
505
692
  * @internal
@@ -509,6 +696,11 @@ class EntityStorageAuthenticationService {
509
696
  * Runtime name for the class.
510
697
  */
511
698
  CLASS_NAME = "EntityStorageAuthenticationService";
699
+ /**
700
+ * The user admin service.
701
+ * @internal
702
+ */
703
+ _authenticationAdminService;
512
704
  /**
513
705
  * The entity storage for users.
514
706
  * @internal
@@ -537,13 +729,11 @@ class EntityStorageAuthenticationService {
537
729
  /**
538
730
  * Create a new instance of EntityStorageAuthentication.
539
731
  * @param options The dependencies for the identity connector.
540
- * @param options.userEntityStorageType The entity storage for the users, defaults to "authentication-user".
541
- * @param options.vaultConnectorType The vault for the private keys, defaults to "vault".
542
- * @param options.config The configuration for the authentication.
543
732
  */
544
733
  constructor(options) {
545
734
  this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
546
735
  this._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
736
+ this._authenticationAdminService = ComponentFactory.get(options?.authenticationAdminServiceType ?? "authentication-admin");
547
737
  this._signingKeyName = options?.config?.signingKeyName ?? "auth-signing";
548
738
  this._defaultTtlMinutes =
549
739
  options?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;
@@ -604,6 +794,16 @@ class EntityStorageAuthenticationService {
604
794
  const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeIdentity}/${this._signingKeyName}`, headerAndPayload.payload.sub ?? "", this._defaultTtlMinutes);
605
795
  return refreshTokenAndExpiry;
606
796
  }
797
+ /**
798
+ * Update the user's password.
799
+ * @param email The email address of the user to update.
800
+ * @param currentPassword The current password for the user.
801
+ * @param newPassword The new password for the user.
802
+ * @returns Nothing.
803
+ */
804
+ async updatePassword(email, currentPassword, newPassword) {
805
+ return this._authenticationAdminService.updatePassword(email, newPassword, currentPassword);
806
+ }
607
807
  }
608
808
 
609
- export { AuthHeaderProcessor, AuthenticationUser, EntityStorageAuthenticationService, PasswordHelper, TokenHelper, authenticationLogin, authenticationLogout, authenticationRefreshToken, generateRestRoutesAuthentication, initSchema, restEntryPoints, tagsAuthentication };
809
+ export { AuthHeaderProcessor, AuthenticationUser, EntityStorageAuthenticationAdminService, EntityStorageAuthenticationService, PasswordHelper, TokenHelper, authenticationLogin, authenticationLogout, authenticationRefreshToken, authenticationUpdatePassword, generateRestRoutesAuthentication, initSchema, restEntryPoints, tagsAuthentication };