@twin.org/api-auth-entity-storage-service 0.0.2-next.9 → 0.0.3-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/es/entities/authenticationUser.js +53 -0
- package/dist/es/entities/authenticationUser.js.map +1 -0
- package/dist/es/index.js +18 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/IAuthHeaderProcessorConfig.js +4 -0
- package/dist/es/models/IAuthHeaderProcessorConfig.js.map +1 -0
- package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js +2 -0
- package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/IEntityStorageAuthenticationAdminServiceConfig.js +4 -0
- package/dist/es/models/IEntityStorageAuthenticationAdminServiceConfig.js.map +1 -0
- package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js +2 -0
- package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js.map +1 -0
- package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js +4 -0
- package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js.map +1 -0
- package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js +2 -0
- package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js.map +1 -0
- package/dist/es/processors/authHeaderProcessor.js +120 -0
- package/dist/es/processors/authHeaderProcessor.js.map +1 -0
- package/dist/es/restEntryPoints.js +10 -0
- package/dist/es/restEntryPoints.js.map +1 -0
- package/dist/es/routes/entityStorageAuthenticationRoutes.js +248 -0
- package/dist/es/routes/entityStorageAuthenticationRoutes.js.map +1 -0
- package/dist/es/schema.js +11 -0
- package/dist/es/schema.js.map +1 -0
- package/dist/es/services/entityStorageAuthenticationAdminService.js +146 -0
- package/dist/es/services/entityStorageAuthenticationAdminService.js.map +1 -0
- package/dist/es/services/entityStorageAuthenticationService.js +136 -0
- package/dist/es/services/entityStorageAuthenticationService.js.map +1 -0
- package/dist/es/utils/passwordHelper.js +29 -0
- package/dist/es/utils/passwordHelper.js.map +1 -0
- package/dist/es/utils/tokenHelper.js +100 -0
- package/dist/es/utils/tokenHelper.js.map +1 -0
- package/dist/types/entities/authenticationUser.d.ts +4 -0
- package/dist/types/index.d.ts +15 -15
- package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +1 -1
- package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +1 -1
- package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +1 -1
- package/dist/types/processors/authHeaderProcessor.d.ts +14 -9
- package/dist/types/services/entityStorageAuthenticationAdminService.d.ts +10 -4
- package/dist/types/services/entityStorageAuthenticationService.d.ts +8 -4
- package/dist/types/utils/passwordHelper.d.ts +4 -0
- package/dist/types/utils/tokenHelper.d.ts +7 -2
- package/docs/changelog.md +91 -0
- package/docs/reference/classes/AuthHeaderProcessor.md +28 -20
- package/docs/reference/classes/AuthenticationUser.md +8 -0
- package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +25 -5
- package/docs/reference/classes/EntityStorageAuthenticationService.md +18 -10
- package/docs/reference/classes/PasswordHelper.md +8 -0
- package/docs/reference/classes/TokenHelper.md +21 -7
- package/locales/en.json +3 -6
- package/package.json +29 -11
- package/dist/cjs/index.cjs +0 -811
- package/dist/esm/index.mjs +0 -797
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper } from "@twin.org/core";
|
|
2
|
+
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
3
|
+
import { PasswordHelper } from "../utils/passwordHelper.js";
|
|
4
|
+
/**
|
|
5
|
+
* Implementation of the authentication component using entity storage.
|
|
6
|
+
*/
|
|
7
|
+
export class EntityStorageAuthenticationAdminService {
|
|
8
|
+
/**
|
|
9
|
+
* Runtime name for the class.
|
|
10
|
+
*/
|
|
11
|
+
static CLASS_NAME = "EntityStorageAuthenticationAdminService";
|
|
12
|
+
/**
|
|
13
|
+
* The minimum password length.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
static _DEFAULT_MIN_PASSWORD_LENGTH = 8;
|
|
17
|
+
/**
|
|
18
|
+
* The entity storage for users.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
_userEntityStorage;
|
|
22
|
+
/**
|
|
23
|
+
* The minimum password length.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
_minPasswordLength;
|
|
27
|
+
/**
|
|
28
|
+
* Create a new instance of EntityStorageAuthentication.
|
|
29
|
+
* @param options The dependencies for the identity connector.
|
|
30
|
+
*/
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
|
|
33
|
+
this._minPasswordLength =
|
|
34
|
+
options?.config?.minPasswordLength ??
|
|
35
|
+
EntityStorageAuthenticationAdminService._DEFAULT_MIN_PASSWORD_LENGTH;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns the class name of the component.
|
|
39
|
+
* @returns The class name of the component.
|
|
40
|
+
*/
|
|
41
|
+
className() {
|
|
42
|
+
return EntityStorageAuthenticationAdminService.CLASS_NAME;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a login for the user.
|
|
46
|
+
* @param email The email address for the user.
|
|
47
|
+
* @param password The password for the user.
|
|
48
|
+
* @param userIdentity The DID to associate with the account.
|
|
49
|
+
* @param organizationIdentity The organization of the user.
|
|
50
|
+
* @returns Nothing.
|
|
51
|
+
*/
|
|
52
|
+
async create(email, password, userIdentity, organizationIdentity) {
|
|
53
|
+
Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "email", email);
|
|
54
|
+
Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "password", password);
|
|
55
|
+
Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "userIdentity", userIdentity);
|
|
56
|
+
Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "organizationIdentity", organizationIdentity);
|
|
57
|
+
try {
|
|
58
|
+
if (password.length < this._minPasswordLength) {
|
|
59
|
+
throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "passwordTooShort", {
|
|
60
|
+
minLength: this._minPasswordLength
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const user = await this._userEntityStorage.get(email);
|
|
64
|
+
if (user) {
|
|
65
|
+
throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userExists");
|
|
66
|
+
}
|
|
67
|
+
const saltBytes = RandomHelper.generate(16);
|
|
68
|
+
const passwordBytes = Converter.utf8ToBytes(password);
|
|
69
|
+
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
70
|
+
const newUser = {
|
|
71
|
+
email,
|
|
72
|
+
salt: Converter.bytesToBase64(saltBytes),
|
|
73
|
+
password: hashedPassword,
|
|
74
|
+
identity: userIdentity,
|
|
75
|
+
organization: organizationIdentity
|
|
76
|
+
};
|
|
77
|
+
await this._userEntityStorage.set(newUser);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "createUserFailed", undefined, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Remove the current user.
|
|
85
|
+
* @param email The email address of the user to remove.
|
|
86
|
+
* @returns Nothing.
|
|
87
|
+
*/
|
|
88
|
+
async remove(email) {
|
|
89
|
+
Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "email", email);
|
|
90
|
+
try {
|
|
91
|
+
const user = await this._userEntityStorage.get(email);
|
|
92
|
+
if (!user) {
|
|
93
|
+
throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", email);
|
|
94
|
+
}
|
|
95
|
+
await this._userEntityStorage.remove(email);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "removeUserFailed", undefined, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Update the user's password.
|
|
103
|
+
* @param email The email address of the user to update.
|
|
104
|
+
* @param newPassword The new password for the user.
|
|
105
|
+
* @param currentPassword The current password, optional, if supplied will check against existing.
|
|
106
|
+
* @returns Nothing.
|
|
107
|
+
*/
|
|
108
|
+
async updatePassword(email, newPassword, currentPassword) {
|
|
109
|
+
Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "email", email);
|
|
110
|
+
Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "newPassword", newPassword);
|
|
111
|
+
try {
|
|
112
|
+
if (newPassword.length < this._minPasswordLength) {
|
|
113
|
+
throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "passwordTooShort", {
|
|
114
|
+
minLength: this._minPasswordLength
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const user = await this._userEntityStorage.get(email);
|
|
118
|
+
if (!user) {
|
|
119
|
+
throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", email);
|
|
120
|
+
}
|
|
121
|
+
if (Is.stringValue(currentPassword)) {
|
|
122
|
+
const saltBytes = Converter.base64ToBytes(user.salt);
|
|
123
|
+
const passwordBytes = Converter.utf8ToBytes(currentPassword);
|
|
124
|
+
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
125
|
+
if (hashedPassword !== user.password) {
|
|
126
|
+
throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "currentPasswordMismatch");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const saltBytes = RandomHelper.generate(16);
|
|
130
|
+
const passwordBytes = Converter.utf8ToBytes(newPassword);
|
|
131
|
+
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
132
|
+
const updatedUser = {
|
|
133
|
+
email,
|
|
134
|
+
salt: Converter.bytesToBase64(saltBytes),
|
|
135
|
+
password: hashedPassword,
|
|
136
|
+
identity: user.identity,
|
|
137
|
+
organization: user.organization
|
|
138
|
+
};
|
|
139
|
+
await this._userEntityStorage.set(updatedUser);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "updatePasswordFailed", undefined, error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=entityStorageAuthenticationAdminService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entityStorageAuthenticationAdminService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationAdminService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IAuthenticationAdminComponent } from \"@twin.org/api-auth-entity-storage-models\";\nimport { Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper } from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationAdminServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js\";\nimport { PasswordHelper } from \"../utils/passwordHelper.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationAdminService implements IAuthenticationAdminComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationAdminService>();\n\n\t/**\n\t * The minimum password length.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_MIN_PASSWORD_LENGTH: number = 8;\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 minimum password length.\n\t * @internal\n\t */\n\tprivate readonly _minPasswordLength: number;\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?: IEntityStorageAuthenticationAdminServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._minPasswordLength =\n\t\t\toptions?.config?.minPasswordLength ??\n\t\t\tEntityStorageAuthenticationAdminService._DEFAULT_MIN_PASSWORD_LENGTH;\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 EntityStorageAuthenticationAdminService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Create 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 * @param userIdentity The DID to associate with the account.\n\t * @param organizationIdentity The organization of the user.\n\t * @returns Nothing.\n\t */\n\tpublic async create(\n\t\temail: string,\n\t\tpassword: string,\n\t\tuserIdentity: string,\n\t\torganizationIdentity: string\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(password),\n\t\t\tpassword\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(userIdentity),\n\t\t\tuserIdentity\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(organizationIdentity),\n\t\t\torganizationIdentity\n\t\t);\n\n\t\ttry {\n\t\t\tif (password.length < this._minPasswordLength) {\n\t\t\t\tthrow new GeneralError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"passwordTooShort\",\n\t\t\t\t\t{\n\t\t\t\t\t\tminLength: this._minPasswordLength\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (user) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, \"userExists\");\n\t\t\t}\n\n\t\t\tconst saltBytes = RandomHelper.generate(16);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(password);\n\n\t\t\tconst hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tconst newUser: AuthenticationUser = {\n\t\t\t\temail,\n\t\t\t\tsalt: Converter.bytesToBase64(saltBytes),\n\t\t\t\tpassword: hashedPassword,\n\t\t\t\tidentity: userIdentity,\n\t\t\t\torganization: organizationIdentity\n\t\t\t};\n\n\t\t\tawait this._userEntityStorage.set(newUser);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"createUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Remove the current user.\n\t * @param email The email address of the user to remove.\n\t * @returns Nothing.\n\t */\n\tpublic async remove(email: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\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 NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this._userEntityStorage.remove(email);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"removeUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\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 newPassword The new password for the user.\n\t * @param currentPassword The current password, optional, if supplied will check against existing.\n\t * @returns Nothing.\n\t */\n\tpublic async updatePassword(\n\t\temail: string,\n\t\tnewPassword: string,\n\t\tcurrentPassword?: string\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(newPassword),\n\t\t\tnewPassword\n\t\t);\n\n\t\ttry {\n\t\t\tif (newPassword.length < this._minPasswordLength) {\n\t\t\t\tthrow new GeneralError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"passwordTooShort\",\n\t\t\t\t\t{\n\t\t\t\t\t\tminLength: this._minPasswordLength\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!user) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (Is.stringValue(currentPassword)) {\n\t\t\t\tconst saltBytes = Converter.base64ToBytes(user.salt);\n\t\t\t\tconst passwordBytes = Converter.utf8ToBytes(currentPassword);\n\n\t\t\t\tconst hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);\n\n\t\t\t\tif (hashedPassword !== user.password) {\n\t\t\t\t\tthrow new GeneralError(\n\t\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\t\"currentPasswordMismatch\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst saltBytes = RandomHelper.generate(16);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(newPassword);\n\n\t\t\tconst hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tconst updatedUser: AuthenticationUser = {\n\t\t\t\temail,\n\t\t\t\tsalt: Converter.bytesToBase64(saltBytes),\n\t\t\t\tpassword: hashedPassword,\n\t\t\t\tidentity: user.identity,\n\t\t\t\torganization: user.organization\n\t\t\t};\n\n\t\t\tawait this._userEntityStorage.set(updatedUser);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"updatePasswordFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
2
|
+
import { ComponentFactory, Converter, GeneralError, Guards, Is, UnauthorizedError } from "@twin.org/core";
|
|
3
|
+
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
4
|
+
import { VaultConnectorFactory } from "@twin.org/vault-models";
|
|
5
|
+
import { PasswordHelper } from "../utils/passwordHelper.js";
|
|
6
|
+
import { TokenHelper } from "../utils/tokenHelper.js";
|
|
7
|
+
/**
|
|
8
|
+
* Implementation of the authentication component using entity storage.
|
|
9
|
+
*/
|
|
10
|
+
export class EntityStorageAuthenticationService {
|
|
11
|
+
/**
|
|
12
|
+
* Runtime name for the class.
|
|
13
|
+
*/
|
|
14
|
+
static CLASS_NAME = "EntityStorageAuthenticationService";
|
|
15
|
+
/**
|
|
16
|
+
* Default TTL in minutes.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
static _DEFAULT_TTL_MINUTES = 60;
|
|
20
|
+
/**
|
|
21
|
+
* The user admin service.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
_authenticationAdminService;
|
|
25
|
+
/**
|
|
26
|
+
* The entity storage for users.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
_userEntityStorage;
|
|
30
|
+
/**
|
|
31
|
+
* The vault for the keys.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
_vaultConnector;
|
|
35
|
+
/**
|
|
36
|
+
* The name of the key to retrieve from the vault for signing JWT.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
_signingKeyName;
|
|
40
|
+
/**
|
|
41
|
+
* The default TTL for the token.
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
_defaultTtlMinutes;
|
|
45
|
+
/**
|
|
46
|
+
* The node identity.
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
_nodeId;
|
|
50
|
+
/**
|
|
51
|
+
* Create a new instance of EntityStorageAuthentication.
|
|
52
|
+
* @param options The dependencies for the identity connector.
|
|
53
|
+
*/
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
|
|
56
|
+
this._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
|
|
57
|
+
this._authenticationAdminService = ComponentFactory.get(options?.authenticationAdminServiceType ?? "authentication-admin");
|
|
58
|
+
this._signingKeyName = options?.config?.signingKeyName ?? "auth-signing";
|
|
59
|
+
this._defaultTtlMinutes =
|
|
60
|
+
options?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns the class name of the component.
|
|
64
|
+
* @returns The class name of the component.
|
|
65
|
+
*/
|
|
66
|
+
className() {
|
|
67
|
+
return EntityStorageAuthenticationService.CLASS_NAME;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The service needs to be started when the application is initialized.
|
|
71
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
72
|
+
* @returns Nothing.
|
|
73
|
+
*/
|
|
74
|
+
async start(nodeLoggingComponentType) {
|
|
75
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
76
|
+
ContextIdHelper.guard(contextIds, ContextIdKeys.Node);
|
|
77
|
+
this._nodeId = contextIds[ContextIdKeys.Node];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Perform a login for the user.
|
|
81
|
+
* @param email The email address for the user.
|
|
82
|
+
* @param password The password for the user.
|
|
83
|
+
* @returns The authentication token for the user, if it uses a mechanism with public access.
|
|
84
|
+
*/
|
|
85
|
+
async login(email, password) {
|
|
86
|
+
Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "email", email);
|
|
87
|
+
Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "password", password);
|
|
88
|
+
try {
|
|
89
|
+
const user = await this._userEntityStorage.get(email);
|
|
90
|
+
if (!user) {
|
|
91
|
+
throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "userNotFound");
|
|
92
|
+
}
|
|
93
|
+
const saltBytes = Converter.base64ToBytes(user.salt);
|
|
94
|
+
const passwordBytes = Converter.utf8ToBytes(password);
|
|
95
|
+
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
96
|
+
if (hashedPassword !== user.password) {
|
|
97
|
+
throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "passwordMismatch");
|
|
98
|
+
}
|
|
99
|
+
const tokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, user.identity, user.organization, this._defaultTtlMinutes);
|
|
100
|
+
return tokenAndExpiry;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new UnauthorizedError(EntityStorageAuthenticationService.CLASS_NAME, "loginFailed", undefined, error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Logout the current user.
|
|
108
|
+
* @param token The token to logout, if it uses a mechanism with public access.
|
|
109
|
+
* @returns Nothing.
|
|
110
|
+
*/
|
|
111
|
+
async logout(token) {
|
|
112
|
+
// Nothing to do here.
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Refresh the token.
|
|
116
|
+
* @param token The token to refresh, if it uses a mechanism with public access.
|
|
117
|
+
* @returns The refreshed token, if it uses a mechanism with public access.
|
|
118
|
+
*/
|
|
119
|
+
async refresh(token) {
|
|
120
|
+
// 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);
|
|
123
|
+
return refreshTokenAndExpiry;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Update the user's password.
|
|
127
|
+
* @param email The email address of the user to update.
|
|
128
|
+
* @param currentPassword The current password for the user.
|
|
129
|
+
* @param newPassword The new password for the user.
|
|
130
|
+
* @returns Nothing.
|
|
131
|
+
*/
|
|
132
|
+
async updatePassword(email, currentPassword, newPassword) {
|
|
133
|
+
return this._authenticationAdminService.updatePassword(email, newPassword, currentPassword);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=entityStorageAuthenticationService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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\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"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
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";
|
|
5
|
+
/**
|
|
6
|
+
* Helper class for password operations.
|
|
7
|
+
*/
|
|
8
|
+
export class PasswordHelper {
|
|
9
|
+
/**
|
|
10
|
+
* Runtime name for the class.
|
|
11
|
+
*/
|
|
12
|
+
static CLASS_NAME = "PasswordHelper";
|
|
13
|
+
/**
|
|
14
|
+
* Hash the password for the user.
|
|
15
|
+
* @param passwordBytes The password bytes.
|
|
16
|
+
* @param saltBytes The salt bytes.
|
|
17
|
+
* @returns The hashed password.
|
|
18
|
+
*/
|
|
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);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=passwordHelper.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Copyright 2024 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { Is, UnauthorizedError } from "@twin.org/core";
|
|
4
|
+
import { VaultConnectorHelper } from "@twin.org/vault-models";
|
|
5
|
+
import { HeaderHelper, HeaderTypes, Jwt } from "@twin.org/web";
|
|
6
|
+
/**
|
|
7
|
+
* Helper class for token operations.
|
|
8
|
+
*/
|
|
9
|
+
export class TokenHelper {
|
|
10
|
+
/**
|
|
11
|
+
* Runtime name for the class.
|
|
12
|
+
*/
|
|
13
|
+
static CLASS_NAME = "TokenHelper";
|
|
14
|
+
/**
|
|
15
|
+
* Create a new token.
|
|
16
|
+
* @param vaultConnector The vault connector.
|
|
17
|
+
* @param signingKeyName The signing key name.
|
|
18
|
+
* @param userIdentity The subject for the token.
|
|
19
|
+
* @param organizationIdentity The organization for the token.
|
|
20
|
+
* @param ttlMinutes The time to live for the token in minutes.
|
|
21
|
+
* @returns The new token and its expiry date.
|
|
22
|
+
*/
|
|
23
|
+
static async createToken(vaultConnector, signingKeyName, userIdentity, organizationIdentity, ttlMinutes) {
|
|
24
|
+
const nowSeconds = Math.trunc(Date.now() / 1000);
|
|
25
|
+
const ttlSeconds = ttlMinutes * 60;
|
|
26
|
+
const jwt = await Jwt.encodeWithSigner({ alg: "EdDSA" }, {
|
|
27
|
+
sub: userIdentity,
|
|
28
|
+
org: organizationIdentity,
|
|
29
|
+
exp: nowSeconds + ttlSeconds
|
|
30
|
+
}, async (header, payload) => VaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload));
|
|
31
|
+
return {
|
|
32
|
+
token: jwt,
|
|
33
|
+
expiry: (nowSeconds + ttlSeconds) * 1000
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Verify the token.
|
|
38
|
+
* @param vaultConnector The vault connector.
|
|
39
|
+
* @param signingKeyName The signing key name.
|
|
40
|
+
* @param token The token to verify.
|
|
41
|
+
* @returns The verified details.
|
|
42
|
+
* @throws UnauthorizedError if the token is missing, invalid or expired.
|
|
43
|
+
*/
|
|
44
|
+
static async verify(vaultConnector, signingKeyName, token) {
|
|
45
|
+
if (!Is.stringValue(token)) {
|
|
46
|
+
throw new UnauthorizedError(TokenHelper.CLASS_NAME, "missing");
|
|
47
|
+
}
|
|
48
|
+
const decoded = await Jwt.verifyWithVerifier(token, async (t) => VaultConnectorHelper.jwtVerifier(vaultConnector, signingKeyName, t));
|
|
49
|
+
// If some of the header/payload data is not properly populated then it is unauthorized.
|
|
50
|
+
if (!Is.stringValue(decoded.payload.sub)) {
|
|
51
|
+
throw new UnauthorizedError(TokenHelper.CLASS_NAME, "payloadMissingSubject");
|
|
52
|
+
}
|
|
53
|
+
else if (!Is.stringValue(decoded.payload.org)) {
|
|
54
|
+
throw new UnauthorizedError(TokenHelper.CLASS_NAME, "payloadMissingOrganization");
|
|
55
|
+
}
|
|
56
|
+
else if (!Is.empty(decoded.payload?.exp) &&
|
|
57
|
+
decoded.payload.exp < Math.trunc(Date.now() / 1000)) {
|
|
58
|
+
throw new UnauthorizedError(TokenHelper.CLASS_NAME, "expired");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
header: decoded.header,
|
|
62
|
+
payload: decoded.payload
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Extract the auth token from the headers, either from the authorization header or the cookie header.
|
|
67
|
+
* @param headers The headers to extract the token from.
|
|
68
|
+
* @param cookieName The name of the cookie to extract the token from.
|
|
69
|
+
* @returns The token if found.
|
|
70
|
+
*/
|
|
71
|
+
static extractTokenFromHeaders(headers, cookieName) {
|
|
72
|
+
const authHeader = headers?.[HeaderTypes.Authorization];
|
|
73
|
+
const cookiesHeader = headers?.[HeaderTypes.Cookie];
|
|
74
|
+
const bearerToken = HeaderHelper.extractBearer(authHeader);
|
|
75
|
+
if (Is.stringValue(bearerToken)) {
|
|
76
|
+
return {
|
|
77
|
+
token: bearerToken,
|
|
78
|
+
location: "authorization"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
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
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=tokenHelper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenHelper.js","sourceRoot":"","sources":["../../../src/utils/tokenHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is, UnauthorizedError } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { type IVaultConnector, VaultConnectorHelper } from \"@twin.org/vault-models\";\nimport {\n\tHeaderHelper,\n\tHeaderTypes,\n\ttype IHttpHeaders,\n\ttype IJwtHeader,\n\ttype IJwtPayload,\n\tJwt\n} from \"@twin.org/web\";\n\n/**\n * Helper class for token operations.\n */\nexport class TokenHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TokenHelper>();\n\n\t/**\n\t * Create a new token.\n\t * @param vaultConnector The vault connector.\n\t * @param signingKeyName The signing key name.\n\t * @param userIdentity The subject for the token.\n\t * @param organizationIdentity The organization for the token.\n\t * @param ttlMinutes The time to live for the token in minutes.\n\t * @returns The new token and its expiry date.\n\t */\n\tpublic static async createToken(\n\t\tvaultConnector: IVaultConnector,\n\t\tsigningKeyName: string,\n\t\tuserIdentity: string,\n\t\torganizationIdentity: string | undefined,\n\t\tttlMinutes: number\n\t): Promise<{\n\t\ttoken: string;\n\t\texpiry: number;\n\t}> {\n\t\tconst nowSeconds = Math.trunc(Date.now() / 1000);\n\t\tconst ttlSeconds = ttlMinutes * 60;\n\n\t\tconst jwt = await Jwt.encodeWithSigner(\n\t\t\t{ alg: \"EdDSA\" },\n\t\t\t{\n\t\t\t\tsub: userIdentity,\n\t\t\t\torg: organizationIdentity,\n\t\t\t\texp: nowSeconds + ttlSeconds\n\t\t\t},\n\t\t\tasync (header, payload) =>\n\t\t\t\tVaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload)\n\t\t);\n\n\t\treturn {\n\t\t\ttoken: jwt,\n\t\t\texpiry: (nowSeconds + ttlSeconds) * 1000\n\t\t};\n\t}\n\n\t/**\n\t * Verify the token.\n\t * @param vaultConnector The vault connector.\n\t * @param signingKeyName The signing key name.\n\t * @param token The token to verify.\n\t * @returns The verified details.\n\t * @throws UnauthorizedError if the token is missing, invalid or expired.\n\t */\n\tpublic static async verify(\n\t\tvaultConnector: IVaultConnector,\n\t\tsigningKeyName: string,\n\t\ttoken: string | undefined\n\t): Promise<{\n\t\theader: IJwtHeader;\n\t\tpayload: IJwtPayload;\n\t}> {\n\t\tif (!Is.stringValue(token)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"missing\");\n\t\t}\n\n\t\tconst decoded = await Jwt.verifyWithVerifier(token, async t =>\n\t\t\tVaultConnectorHelper.jwtVerifier(vaultConnector, signingKeyName, t)\n\t\t);\n\n\t\t// If some of the header/payload data is not properly populated then it is unauthorized.\n\t\tif (!Is.stringValue(decoded.payload.sub)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"payloadMissingSubject\");\n\t\t} else if (!Is.stringValue(decoded.payload.org)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"payloadMissingOrganization\");\n\t\t} else if (\n\t\t\t!Is.empty(decoded.payload?.exp) &&\n\t\t\tdecoded.payload.exp < Math.trunc(Date.now() / 1000)\n\t\t) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"expired\");\n\t\t}\n\n\t\treturn {\n\t\t\theader: decoded.header,\n\t\t\tpayload: decoded.payload\n\t\t};\n\t}\n\n\t/**\n\t * Extract the auth token from the headers, either from the authorization header or the cookie header.\n\t * @param headers The headers to extract the token from.\n\t * @param cookieName The name of the cookie to extract the token from.\n\t * @returns The token if found.\n\t */\n\tpublic static extractTokenFromHeaders(\n\t\theaders?: IHttpHeaders,\n\t\tcookieName?: string\n\t):\n\t\t| {\n\t\t\t\ttoken: string;\n\t\t\t\tlocation: \"authorization\" | \"cookie\";\n\t\t }\n\t\t| undefined {\n\t\tconst authHeader = headers?.[HeaderTypes.Authorization];\n\t\tconst cookiesHeader = headers?.[HeaderTypes.Cookie];\n\n\t\tconst bearerToken = HeaderHelper.extractBearer(authHeader);\n\t\tif (Is.stringValue(bearerToken)) {\n\t\t\treturn {\n\t\t\t\ttoken: bearerToken,\n\t\t\t\tlocation: \"authorization\"\n\t\t\t};\n\t\t} else if (Is.notEmpty(cookiesHeader) && Is.stringValue(cookieName)) {\n\t\t\tconst cookies = Is.arrayValue(cookiesHeader) ? cookiesHeader : [cookiesHeader];\n\t\t\tfor (const cookie of cookies) {\n\t\t\t\tif (Is.stringValue(cookie)) {\n\t\t\t\t\tconst accessTokenCookie = cookie\n\t\t\t\t\t\t.split(\";\")\n\t\t\t\t\t\t.map(c => c.trim())\n\t\t\t\t\t\t.find(c => c.startsWith(cookieName));\n\t\t\t\t\tif (Is.stringValue(accessTokenCookie)) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttoken: accessTokenCookie.slice(cookieName.length + 1).trim(),\n\t\t\t\t\t\t\tlocation: \"cookie\"\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
export * from "./entities/authenticationUser";
|
|
2
|
-
export * from "./models/IAuthHeaderProcessorConfig";
|
|
3
|
-
export * from "./models/IAuthHeaderProcessorConstructorOptions";
|
|
4
|
-
export * from "./models/IEntityStorageAuthenticationAdminServiceConfig";
|
|
5
|
-
export * from "./models/IEntityStorageAuthenticationAdminServiceConstructorOptions";
|
|
6
|
-
export * from "./models/IEntityStorageAuthenticationServiceConfig";
|
|
7
|
-
export * from "./models/IEntityStorageAuthenticationServiceConstructorOptions";
|
|
8
|
-
export * from "./processors/authHeaderProcessor";
|
|
9
|
-
export * from "./restEntryPoints";
|
|
10
|
-
export * from "./routes/entityStorageAuthenticationRoutes";
|
|
11
|
-
export * from "./schema";
|
|
12
|
-
export * from "./services/entityStorageAuthenticationAdminService";
|
|
13
|
-
export * from "./services/entityStorageAuthenticationService";
|
|
14
|
-
export * from "./utils/passwordHelper";
|
|
15
|
-
export * from "./utils/tokenHelper";
|
|
1
|
+
export * from "./entities/authenticationUser.js";
|
|
2
|
+
export * from "./models/IAuthHeaderProcessorConfig.js";
|
|
3
|
+
export * from "./models/IAuthHeaderProcessorConstructorOptions.js";
|
|
4
|
+
export * from "./models/IEntityStorageAuthenticationAdminServiceConfig.js";
|
|
5
|
+
export * from "./models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js";
|
|
6
|
+
export * from "./models/IEntityStorageAuthenticationServiceConfig.js";
|
|
7
|
+
export * from "./models/IEntityStorageAuthenticationServiceConstructorOptions.js";
|
|
8
|
+
export * from "./processors/authHeaderProcessor.js";
|
|
9
|
+
export * from "./restEntryPoints.js";
|
|
10
|
+
export * from "./routes/entityStorageAuthenticationRoutes.js";
|
|
11
|
+
export * from "./schema.js";
|
|
12
|
+
export * from "./services/entityStorageAuthenticationAdminService.js";
|
|
13
|
+
export * from "./services/entityStorageAuthenticationService.js";
|
|
14
|
+
export * from "./utils/passwordHelper.js";
|
|
15
|
+
export * from "./utils/tokenHelper.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IEntityStorageAuthenticationAdminServiceConfig } from "./IEntityStorageAuthenticationAdminServiceConfig";
|
|
1
|
+
import type { IEntityStorageAuthenticationAdminServiceConfig } from "./IEntityStorageAuthenticationAdminServiceConfig.js";
|
|
2
2
|
/**
|
|
3
3
|
* Options for the EntityStorageAuthenticationAdminService constructor.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IEntityStorageAuthenticationServiceConfig } from "./IEntityStorageAuthenticationServiceConfig";
|
|
1
|
+
import type { IEntityStorageAuthenticationServiceConfig } from "./IEntityStorageAuthenticationServiceConfig.js";
|
|
2
2
|
/**
|
|
3
3
|
* Options for the EntityStorageAuthenticationService constructor.
|
|
4
4
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type IBaseRoute, type IBaseRouteProcessor, type
|
|
2
|
-
import type
|
|
1
|
+
import { type IBaseRoute, type IBaseRouteProcessor, type IHttpResponse, type IHttpServerRequest } from "@twin.org/api-models";
|
|
2
|
+
import { type IContextIds } from "@twin.org/context";
|
|
3
|
+
import type { IAuthHeaderProcessorConstructorOptions } from "../models/IAuthHeaderProcessorConstructorOptions.js";
|
|
3
4
|
/**
|
|
4
5
|
* Handle a JWT token in the authorization header or cookies and validate it to populate request context identity.
|
|
5
6
|
*/
|
|
@@ -7,28 +8,32 @@ export declare class AuthHeaderProcessor implements IBaseRouteProcessor {
|
|
|
7
8
|
/**
|
|
8
9
|
* Runtime name for the class.
|
|
9
10
|
*/
|
|
10
|
-
readonly CLASS_NAME: string;
|
|
11
|
+
static readonly CLASS_NAME: string;
|
|
11
12
|
/**
|
|
12
13
|
* Create a new instance of AuthCookiePreProcessor.
|
|
13
14
|
* @param options Options for the processor.
|
|
14
15
|
*/
|
|
15
16
|
constructor(options?: IAuthHeaderProcessorConstructorOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Returns the class name of the component.
|
|
19
|
+
* @returns The class name of the component.
|
|
20
|
+
*/
|
|
21
|
+
className(): string;
|
|
16
22
|
/**
|
|
17
23
|
* The service needs to be started when the application is initialized.
|
|
18
|
-
* @param nodeIdentity The identity of the node.
|
|
19
24
|
* @param nodeLoggingComponentType The node logging component type.
|
|
20
25
|
* @returns Nothing.
|
|
21
26
|
*/
|
|
22
|
-
start(
|
|
27
|
+
start(nodeLoggingComponentType?: string): Promise<void>;
|
|
23
28
|
/**
|
|
24
29
|
* Pre process the REST request for the specified route.
|
|
25
30
|
* @param request The incoming request.
|
|
26
31
|
* @param response The outgoing response.
|
|
27
32
|
* @param route The route to process.
|
|
28
|
-
* @param
|
|
33
|
+
* @param contextIds The context IDs of the request.
|
|
29
34
|
* @param processorState The state handed through the processors.
|
|
30
35
|
*/
|
|
31
|
-
pre(request: IHttpServerRequest, response: IHttpResponse, route: IBaseRoute | undefined,
|
|
36
|
+
pre(request: IHttpServerRequest, response: IHttpResponse, route: IBaseRoute | undefined, contextIds: IContextIds, processorState: {
|
|
32
37
|
[id: string]: unknown;
|
|
33
38
|
}): Promise<void>;
|
|
34
39
|
/**
|
|
@@ -36,10 +41,10 @@ export declare class AuthHeaderProcessor implements IBaseRouteProcessor {
|
|
|
36
41
|
* @param request The incoming request.
|
|
37
42
|
* @param response The outgoing response.
|
|
38
43
|
* @param route The route to process.
|
|
39
|
-
* @param
|
|
44
|
+
* @param contextIds The context IDs of the request.
|
|
40
45
|
* @param processorState The state handed through the processors.
|
|
41
46
|
*/
|
|
42
|
-
post(request: IHttpServerRequest, response: IHttpResponse, route: IBaseRoute | undefined,
|
|
47
|
+
post(request: IHttpServerRequest, response: IHttpResponse, route: IBaseRoute | undefined, contextIds: IContextIds, processorState: {
|
|
43
48
|
[id: string]: unknown;
|
|
44
49
|
}): Promise<void>;
|
|
45
50
|
}
|