@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.
- package/dist/cjs/index.cjs +221 -19
- package/dist/esm/index.mjs +223 -23
- package/dist/types/index.d.ts +5 -0
- package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +15 -0
- package/dist/types/models/IEntityStorageAuthenticationAdminServiceConfig.d.ts +10 -0
- package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +15 -0
- package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +25 -0
- package/dist/types/processors/authHeaderProcessor.d.ts +10 -11
- package/dist/types/routes/entityStorageAuthenticationRoutes.d.ts +9 -1
- package/dist/types/services/entityStorageAuthenticationAdminService.d.ts +42 -0
- package/dist/types/services/entityStorageAuthenticationService.d.ts +14 -9
- package/dist/types/utils/tokenHelper.d.ts +1 -1
- package/docs/changelog.md +115 -1
- package/docs/reference/classes/AuthHeaderProcessor.md +50 -28
- package/docs/reference/classes/AuthenticationUser.md +3 -3
- package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +149 -0
- package/docs/reference/classes/EntityStorageAuthenticationService.md +71 -39
- package/docs/reference/classes/PasswordHelper.md +9 -5
- package/docs/reference/classes/TokenHelper.md +36 -34
- package/docs/reference/functions/authenticationLogin.md +9 -3
- package/docs/reference/functions/authenticationLogout.md +9 -3
- package/docs/reference/functions/authenticationRefreshToken.md +9 -3
- package/docs/reference/functions/authenticationUpdatePassword.md +31 -0
- package/docs/reference/functions/generateRestRoutesAuthentication.md +8 -4
- package/docs/reference/index.md +6 -0
- package/docs/reference/interfaces/IAuthHeaderProcessorConstructorOptions.md +25 -0
- package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConfig.md +17 -0
- package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConstructorOptions.md +25 -0
- package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConstructorOptions.md +53 -0
- package/locales/en.json +10 -1
- package/package.json +6 -6
package/dist/cjs/index.cjs
CHANGED
|
@@ -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:
|
|
76
|
+
const jwt = await web.Jwt.encodeWithSigner({ alg: "EdDSA" }, {
|
|
78
77
|
sub: subject,
|
|
79
78
|
exp: nowSeconds + ttlSeconds
|
|
80
|
-
}, async (
|
|
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 (
|
|
99
|
-
// If
|
|
100
|
-
|
|
101
|
-
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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;
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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,
|
|
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:
|
|
74
|
+
const jwt = await Jwt.encodeWithSigner({ alg: "EdDSA" }, {
|
|
76
75
|
sub: subject,
|
|
77
76
|
exp: nowSeconds + ttlSeconds
|
|
78
|
-
}, async (
|
|
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 (
|
|
97
|
-
// If
|
|
98
|
-
|
|
99
|
-
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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 };
|