@twin.org/api-auth-entity-storage-service 0.0.2-next.8 → 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 +107 -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
package/dist/esm/index.mjs
DELETED
|
@@ -1,797 +0,0 @@
|
|
|
1
|
-
import { property, entity, EntitySchemaFactory, EntitySchemaHelper } from '@twin.org/entity';
|
|
2
|
-
import { HttpErrorHelper } from '@twin.org/api-models';
|
|
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
|
-
import { EntityStorageConnectorFactory } from '@twin.org/entity-storage-models';
|
|
7
|
-
import { Blake2b } from '@twin.org/crypto';
|
|
8
|
-
|
|
9
|
-
// Copyright 2024 IOTA Stiftung.
|
|
10
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
11
|
-
/**
|
|
12
|
-
* Class defining the storage for user login credentials.
|
|
13
|
-
*/
|
|
14
|
-
let AuthenticationUser = class AuthenticationUser {
|
|
15
|
-
/**
|
|
16
|
-
* The user e-mail address.
|
|
17
|
-
*/
|
|
18
|
-
email;
|
|
19
|
-
/**
|
|
20
|
-
* The encrypted password for the user.
|
|
21
|
-
*/
|
|
22
|
-
password;
|
|
23
|
-
/**
|
|
24
|
-
* The salt for the password.
|
|
25
|
-
*/
|
|
26
|
-
salt;
|
|
27
|
-
/**
|
|
28
|
-
* The user identity.
|
|
29
|
-
*/
|
|
30
|
-
identity;
|
|
31
|
-
};
|
|
32
|
-
__decorate([
|
|
33
|
-
property({ type: "string", isPrimary: true }),
|
|
34
|
-
__metadata("design:type", String)
|
|
35
|
-
], AuthenticationUser.prototype, "email", void 0);
|
|
36
|
-
__decorate([
|
|
37
|
-
property({ type: "string" }),
|
|
38
|
-
__metadata("design:type", String)
|
|
39
|
-
], AuthenticationUser.prototype, "password", void 0);
|
|
40
|
-
__decorate([
|
|
41
|
-
property({ type: "string" }),
|
|
42
|
-
__metadata("design:type", String)
|
|
43
|
-
], AuthenticationUser.prototype, "salt", void 0);
|
|
44
|
-
__decorate([
|
|
45
|
-
property({ type: "string" }),
|
|
46
|
-
__metadata("design:type", String)
|
|
47
|
-
], AuthenticationUser.prototype, "identity", void 0);
|
|
48
|
-
AuthenticationUser = __decorate([
|
|
49
|
-
entity()
|
|
50
|
-
], AuthenticationUser);
|
|
51
|
-
|
|
52
|
-
// Copyright 2024 IOTA Stiftung.
|
|
53
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
54
|
-
/**
|
|
55
|
-
* Helper class for token operations.
|
|
56
|
-
*/
|
|
57
|
-
class TokenHelper {
|
|
58
|
-
/**
|
|
59
|
-
* Runtime name for the class.
|
|
60
|
-
* @internal
|
|
61
|
-
*/
|
|
62
|
-
static _CLASS_NAME = "TokenHelper";
|
|
63
|
-
/**
|
|
64
|
-
* Create a new token.
|
|
65
|
-
* @param vaultConnector The vault connector.
|
|
66
|
-
* @param signingKeyName The signing key name.
|
|
67
|
-
* @param subject The subject for the token.
|
|
68
|
-
* @param ttlMinutes The time to live for the token in minutes.
|
|
69
|
-
* @returns The new token and its expiry date.
|
|
70
|
-
*/
|
|
71
|
-
static async createToken(vaultConnector, signingKeyName, subject, ttlMinutes) {
|
|
72
|
-
const nowSeconds = Math.trunc(Date.now() / 1000);
|
|
73
|
-
const ttlSeconds = ttlMinutes * 60;
|
|
74
|
-
const jwt = await Jwt.encodeWithSigner({ alg: "EdDSA" }, {
|
|
75
|
-
sub: subject,
|
|
76
|
-
exp: nowSeconds + ttlSeconds
|
|
77
|
-
}, async (header, payload) => VaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload));
|
|
78
|
-
return {
|
|
79
|
-
token: jwt,
|
|
80
|
-
expiry: (nowSeconds + ttlSeconds) * 1000
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Verify the token.
|
|
85
|
-
* @param vaultConnector The vault connector.
|
|
86
|
-
* @param signingKeyName The signing key name.
|
|
87
|
-
* @param token The token to verify.
|
|
88
|
-
* @returns The verified details.
|
|
89
|
-
* @throws UnauthorizedError if the token is missing, invalid or expired.
|
|
90
|
-
*/
|
|
91
|
-
static async verify(vaultConnector, signingKeyName, token) {
|
|
92
|
-
if (!Is.stringValue(token)) {
|
|
93
|
-
throw new UnauthorizedError(this._CLASS_NAME, "missing");
|
|
94
|
-
}
|
|
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");
|
|
99
|
-
}
|
|
100
|
-
else if (!Is.empty(decoded.payload?.exp) &&
|
|
101
|
-
decoded.payload.exp < Math.trunc(Date.now() / 1000)) {
|
|
102
|
-
throw new UnauthorizedError(this._CLASS_NAME, "expired");
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
header: decoded.header,
|
|
106
|
-
payload: decoded.payload
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Extract the auth token from the headers, either from the authorization header or the cookie header.
|
|
111
|
-
* @param headers The headers to extract the token from.
|
|
112
|
-
* @param cookieName The name of the cookie to extract the token from.
|
|
113
|
-
* @returns The token if found.
|
|
114
|
-
*/
|
|
115
|
-
static extractTokenFromHeaders(headers, cookieName) {
|
|
116
|
-
const authHeader = headers?.[HeaderTypes.Authorization];
|
|
117
|
-
const cookiesHeader = headers?.[HeaderTypes.Cookie];
|
|
118
|
-
if (Is.string(authHeader) && authHeader.startsWith("Bearer ")) {
|
|
119
|
-
return {
|
|
120
|
-
token: authHeader.slice(7).trim(),
|
|
121
|
-
location: "authorization"
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
else if (Is.notEmpty(cookiesHeader) && Is.stringValue(cookieName)) {
|
|
125
|
-
const cookies = Is.arrayValue(cookiesHeader) ? cookiesHeader : [cookiesHeader];
|
|
126
|
-
for (const cookie of cookies) {
|
|
127
|
-
if (Is.stringValue(cookie)) {
|
|
128
|
-
const accessTokenCookie = cookie
|
|
129
|
-
.split(";")
|
|
130
|
-
.map(c => c.trim())
|
|
131
|
-
.find(c => c.startsWith(cookieName));
|
|
132
|
-
if (Is.stringValue(accessTokenCookie)) {
|
|
133
|
-
return {
|
|
134
|
-
token: accessTokenCookie.slice(cookieName.length + 1).trim(),
|
|
135
|
-
location: "cookie"
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Copyright 2024 IOTA Stiftung.
|
|
145
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
146
|
-
/**
|
|
147
|
-
* Handle a JWT token in the authorization header or cookies and validate it to populate request context identity.
|
|
148
|
-
*/
|
|
149
|
-
class AuthHeaderProcessor {
|
|
150
|
-
/**
|
|
151
|
-
* The default name for the access token as a cookie.
|
|
152
|
-
* @internal
|
|
153
|
-
*/
|
|
154
|
-
static DEFAULT_COOKIE_NAME = "access_token";
|
|
155
|
-
/**
|
|
156
|
-
* Runtime name for the class.
|
|
157
|
-
*/
|
|
158
|
-
CLASS_NAME = "AuthHeaderProcessor";
|
|
159
|
-
/**
|
|
160
|
-
* The vault for the keys.
|
|
161
|
-
* @internal
|
|
162
|
-
*/
|
|
163
|
-
_vaultConnector;
|
|
164
|
-
/**
|
|
165
|
-
* The name of the key to retrieve from the vault for signing JWT.
|
|
166
|
-
* @internal
|
|
167
|
-
*/
|
|
168
|
-
_signingKeyName;
|
|
169
|
-
/**
|
|
170
|
-
* The name of the cookie to use for the token.
|
|
171
|
-
* @internal
|
|
172
|
-
*/
|
|
173
|
-
_cookieName;
|
|
174
|
-
/**
|
|
175
|
-
* The node identity.
|
|
176
|
-
* @internal
|
|
177
|
-
*/
|
|
178
|
-
_nodeIdentity;
|
|
179
|
-
/**
|
|
180
|
-
* Create a new instance of AuthCookiePreProcessor.
|
|
181
|
-
* @param options Options for the processor.
|
|
182
|
-
*/
|
|
183
|
-
constructor(options) {
|
|
184
|
-
this._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
|
|
185
|
-
this._signingKeyName = options?.config?.signingKeyName ?? "auth-signing";
|
|
186
|
-
this._cookieName = options?.config?.cookieName ?? AuthHeaderProcessor.DEFAULT_COOKIE_NAME;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* The service needs to be started when the application is initialized.
|
|
190
|
-
* @param nodeIdentity The identity of the node.
|
|
191
|
-
* @param nodeLoggingComponentType The node logging component type.
|
|
192
|
-
* @returns Nothing.
|
|
193
|
-
*/
|
|
194
|
-
async start(nodeIdentity, nodeLoggingComponentType) {
|
|
195
|
-
Guards.string(this.CLASS_NAME, "nodeIdentity", nodeIdentity);
|
|
196
|
-
this._nodeIdentity = nodeIdentity;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Pre process the REST request for the specified route.
|
|
200
|
-
* @param request The incoming request.
|
|
201
|
-
* @param response The outgoing response.
|
|
202
|
-
* @param route The route to process.
|
|
203
|
-
* @param requestIdentity The identity context for the request.
|
|
204
|
-
* @param processorState The state handed through the processors.
|
|
205
|
-
*/
|
|
206
|
-
async pre(request, response, route, requestIdentity, processorState) {
|
|
207
|
-
if (!Is.empty(route) && !(route.skipAuth ?? false)) {
|
|
208
|
-
try {
|
|
209
|
-
const tokenAndLocation = TokenHelper.extractTokenFromHeaders(request.headers, this._cookieName);
|
|
210
|
-
const headerAndPayload = await TokenHelper.verify(this._vaultConnector, `${this._nodeIdentity}/${this._signingKeyName}`, tokenAndLocation?.token);
|
|
211
|
-
requestIdentity.userIdentity = headerAndPayload.payload?.sub;
|
|
212
|
-
processorState.authToken = tokenAndLocation?.token;
|
|
213
|
-
processorState.authTokenLocation = tokenAndLocation?.location;
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
const error = BaseError.fromError(err);
|
|
217
|
-
HttpErrorHelper.buildResponse(response, error, HttpStatusCode.unauthorized);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Post process the REST request for the specified route.
|
|
223
|
-
* @param request The incoming request.
|
|
224
|
-
* @param response The outgoing response.
|
|
225
|
-
* @param route The route to process.
|
|
226
|
-
* @param requestIdentity The identity context for the request.
|
|
227
|
-
* @param processorState The state handed through the processors.
|
|
228
|
-
*/
|
|
229
|
-
async post(request, response, route, requestIdentity, processorState) {
|
|
230
|
-
const responseAuthOperation = processorState?.authOperation;
|
|
231
|
-
// We don't populate the cookie if the incoming request was from an authorization header.
|
|
232
|
-
if (!Is.empty(route) &&
|
|
233
|
-
Is.stringValue(responseAuthOperation) &&
|
|
234
|
-
processorState.authTokenLocation !== "authorization") {
|
|
235
|
-
if ((responseAuthOperation === "login" || responseAuthOperation === "refresh") &&
|
|
236
|
-
Is.stringValue(response.body?.token)) {
|
|
237
|
-
response.headers ??= {};
|
|
238
|
-
response.headers[HeaderTypes.SetCookie] =
|
|
239
|
-
`${this._cookieName}=${response.body.token}; Secure; HttpOnly; SameSite=None; Path=/`;
|
|
240
|
-
delete response.body.token;
|
|
241
|
-
}
|
|
242
|
-
else if (responseAuthOperation === "logout") {
|
|
243
|
-
response.headers ??= {};
|
|
244
|
-
response.headers[HeaderTypes.SetCookie] =
|
|
245
|
-
`${this._cookieName}=; Max-Age=0; Secure; HttpOnly; SameSite=None; Path=/`;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* The source used when communicating about these routes.
|
|
253
|
-
*/
|
|
254
|
-
const ROUTES_SOURCE = "authenticationRoutes";
|
|
255
|
-
/**
|
|
256
|
-
* The tag to associate with the routes.
|
|
257
|
-
*/
|
|
258
|
-
const tagsAuthentication = [
|
|
259
|
-
{
|
|
260
|
-
name: "Authentication",
|
|
261
|
-
description: "Authentication endpoints for the REST server."
|
|
262
|
-
}
|
|
263
|
-
];
|
|
264
|
-
/**
|
|
265
|
-
* The REST routes for authentication.
|
|
266
|
-
* @param baseRouteName Prefix to prepend to the paths.
|
|
267
|
-
* @param componentName The name of the component to use in the routes stored in the ComponentFactory.
|
|
268
|
-
* @returns The generated routes.
|
|
269
|
-
*/
|
|
270
|
-
function generateRestRoutesAuthentication(baseRouteName, componentName) {
|
|
271
|
-
const loginRoute = {
|
|
272
|
-
operationId: "authenticationLogin",
|
|
273
|
-
summary: "Login to the server",
|
|
274
|
-
tag: tagsAuthentication[0].name,
|
|
275
|
-
method: "POST",
|
|
276
|
-
path: `${baseRouteName}/login`,
|
|
277
|
-
handler: async (httpRequestContext, request) => authenticationLogin(httpRequestContext, componentName, request),
|
|
278
|
-
requestType: {
|
|
279
|
-
type: "ILoginRequest",
|
|
280
|
-
examples: [
|
|
281
|
-
{
|
|
282
|
-
id: "loginRequestExample",
|
|
283
|
-
description: "The request to login to the server.",
|
|
284
|
-
request: {
|
|
285
|
-
body: {
|
|
286
|
-
email: "user@example.com",
|
|
287
|
-
password: "MyPassword123!"
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
]
|
|
292
|
-
},
|
|
293
|
-
responseType: [
|
|
294
|
-
{
|
|
295
|
-
type: "ILoginResponse",
|
|
296
|
-
examples: [
|
|
297
|
-
{
|
|
298
|
-
id: "loginResponseExample",
|
|
299
|
-
description: "The response for the login request.",
|
|
300
|
-
response: {
|
|
301
|
-
body: {
|
|
302
|
-
token: "eyJhbGciOiJIU...sw5c",
|
|
303
|
-
expiry: 1722514341067
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
]
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
type: "IUnauthorizedResponse"
|
|
311
|
-
}
|
|
312
|
-
],
|
|
313
|
-
skipAuth: true
|
|
314
|
-
};
|
|
315
|
-
const logoutRoute = {
|
|
316
|
-
operationId: "authenticationLogout",
|
|
317
|
-
summary: "Logout from the server",
|
|
318
|
-
tag: tagsAuthentication[0].name,
|
|
319
|
-
method: "GET",
|
|
320
|
-
path: `${baseRouteName}/logout`,
|
|
321
|
-
handler: async (httpRequestContext, request) => authenticationLogout(httpRequestContext, componentName, request),
|
|
322
|
-
requestType: {
|
|
323
|
-
type: "ILogoutRequest",
|
|
324
|
-
examples: [
|
|
325
|
-
{
|
|
326
|
-
id: "logoutRequestExample",
|
|
327
|
-
description: "The request to logout from the server.",
|
|
328
|
-
request: {
|
|
329
|
-
query: {
|
|
330
|
-
token: "eyJhbGciOiJIU...sw5c"
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
]
|
|
335
|
-
},
|
|
336
|
-
responseType: [
|
|
337
|
-
{
|
|
338
|
-
type: "INoContentResponse"
|
|
339
|
-
}
|
|
340
|
-
],
|
|
341
|
-
skipAuth: true
|
|
342
|
-
};
|
|
343
|
-
const refreshTokenRoute = {
|
|
344
|
-
operationId: "authenticationRefreshToken",
|
|
345
|
-
summary: "Refresh an authentication token",
|
|
346
|
-
tag: tagsAuthentication[0].name,
|
|
347
|
-
method: "GET",
|
|
348
|
-
path: `${baseRouteName}/refresh`,
|
|
349
|
-
handler: async (httpRequestContext, request) => authenticationRefreshToken(httpRequestContext, componentName, request),
|
|
350
|
-
requestType: {
|
|
351
|
-
type: "IRefreshTokenRequest",
|
|
352
|
-
examples: [
|
|
353
|
-
{
|
|
354
|
-
id: "refreshTokenRequestExample",
|
|
355
|
-
description: "The request to refresh an auth token.",
|
|
356
|
-
request: {
|
|
357
|
-
query: {
|
|
358
|
-
token: "eyJhbGciOiJIU...sw5c"
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
]
|
|
363
|
-
},
|
|
364
|
-
responseType: [
|
|
365
|
-
{
|
|
366
|
-
type: "IRefreshTokenResponse",
|
|
367
|
-
examples: [
|
|
368
|
-
{
|
|
369
|
-
id: "refreshTokenResponseExample",
|
|
370
|
-
description: "The response for the refresh token request.",
|
|
371
|
-
response: {
|
|
372
|
-
body: {
|
|
373
|
-
token: "eyJhbGciOiJIU...sw5c",
|
|
374
|
-
expiry: 1722514341067
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
]
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
type: "IUnauthorizedResponse"
|
|
382
|
-
}
|
|
383
|
-
]
|
|
384
|
-
};
|
|
385
|
-
const updatePasswordRoute = {
|
|
386
|
-
operationId: "authenticationUpdatePassword",
|
|
387
|
-
summary: "Update the user's password",
|
|
388
|
-
tag: tagsAuthentication[0].name,
|
|
389
|
-
method: "PUT",
|
|
390
|
-
path: `${baseRouteName}/:email/password`,
|
|
391
|
-
handler: async (httpRequestContext, request) => authenticationUpdatePassword(httpRequestContext, componentName, request),
|
|
392
|
-
requestType: {
|
|
393
|
-
type: "IUpdatePasswordRequest",
|
|
394
|
-
examples: [
|
|
395
|
-
{
|
|
396
|
-
id: "updatePasswordRequestExample",
|
|
397
|
-
description: "The request to update the user's password.",
|
|
398
|
-
request: {
|
|
399
|
-
pathParams: {
|
|
400
|
-
email: "john:example.com"
|
|
401
|
-
},
|
|
402
|
-
body: {
|
|
403
|
-
currentPassword: "MyNewPassword123!",
|
|
404
|
-
newPassword: "MyNewPassword123!"
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
]
|
|
409
|
-
},
|
|
410
|
-
responseType: [
|
|
411
|
-
{
|
|
412
|
-
type: "INoContentResponse"
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
type: "IUnauthorizedResponse"
|
|
416
|
-
}
|
|
417
|
-
]
|
|
418
|
-
};
|
|
419
|
-
return [loginRoute, logoutRoute, refreshTokenRoute, updatePasswordRoute];
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Login to the server.
|
|
423
|
-
* @param httpRequestContext The request context for the API.
|
|
424
|
-
* @param componentName The name of the component to use in the routes.
|
|
425
|
-
* @param request The request.
|
|
426
|
-
* @returns The response object with additional http response properties.
|
|
427
|
-
*/
|
|
428
|
-
async function authenticationLogin(httpRequestContext, componentName, request) {
|
|
429
|
-
Guards.object(ROUTES_SOURCE, "request", request);
|
|
430
|
-
Guards.object(ROUTES_SOURCE, "request.body", request.body);
|
|
431
|
-
const component = ComponentFactory.get(componentName);
|
|
432
|
-
const result = await component.login(request.body.email, request.body.password);
|
|
433
|
-
// Need to give a hint to any auth processors about the operation
|
|
434
|
-
// in case they need to manipulate the response
|
|
435
|
-
httpRequestContext.processorState.authOperation = "login";
|
|
436
|
-
return {
|
|
437
|
-
body: result
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Logout from the server.
|
|
442
|
-
* @param httpRequestContext The request context for the API.
|
|
443
|
-
* @param componentName The name of the component to use in the routes.
|
|
444
|
-
* @param request The request.
|
|
445
|
-
* @returns The response object with additional http response properties.
|
|
446
|
-
*/
|
|
447
|
-
async function authenticationLogout(httpRequestContext, componentName, request) {
|
|
448
|
-
Guards.object(ROUTES_SOURCE, "request", request);
|
|
449
|
-
const component = ComponentFactory.get(componentName);
|
|
450
|
-
await component.logout(request.query?.token);
|
|
451
|
-
// Need to give a hint to any auth processors about the operation
|
|
452
|
-
// in case they need to manipulate the response
|
|
453
|
-
httpRequestContext.processorState.authOperation = "logout";
|
|
454
|
-
return {
|
|
455
|
-
statusCode: HttpStatusCode.noContent
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Refresh the login token.
|
|
460
|
-
* @param httpRequestContext The request context for the API.
|
|
461
|
-
* @param componentName The name of the component to use in the routes.
|
|
462
|
-
* @param request The request.
|
|
463
|
-
* @returns The response object with additional http response properties.
|
|
464
|
-
*/
|
|
465
|
-
async function authenticationRefreshToken(httpRequestContext, componentName, request) {
|
|
466
|
-
Guards.object(ROUTES_SOURCE, "request", request);
|
|
467
|
-
const component = ComponentFactory.get(componentName);
|
|
468
|
-
// If the token is not in the query, then maybe an auth processor has extracted it
|
|
469
|
-
// and stored it in the processor state
|
|
470
|
-
const token = request.query?.token ?? httpRequestContext.processorState.authToken;
|
|
471
|
-
const result = await component.refresh(token);
|
|
472
|
-
// Need to give a hint to any auth processors about the operation
|
|
473
|
-
// in case they need to manipulate the response
|
|
474
|
-
httpRequestContext.processorState.authOperation = "refresh";
|
|
475
|
-
return {
|
|
476
|
-
body: result
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Update the user's password.
|
|
481
|
-
* @param httpRequestContext The request context for the API.
|
|
482
|
-
* @param componentName The name of the component to use in the routes.
|
|
483
|
-
* @param request The request.
|
|
484
|
-
* @returns The response object with additional http response properties.
|
|
485
|
-
*/
|
|
486
|
-
async function authenticationUpdatePassword(httpRequestContext, componentName, request) {
|
|
487
|
-
Guards.object(ROUTES_SOURCE, "request", request);
|
|
488
|
-
Guards.object(ROUTES_SOURCE, "request.pathParams", request.pathParams);
|
|
489
|
-
Guards.object(ROUTES_SOURCE, "request.body", request.body);
|
|
490
|
-
const component = ComponentFactory.get(componentName);
|
|
491
|
-
await component.updatePassword(request.pathParams.email, request.body.currentPassword, request.body.newPassword);
|
|
492
|
-
return {
|
|
493
|
-
statusCode: HttpStatusCode.noContent
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const restEntryPoints = [
|
|
498
|
-
{
|
|
499
|
-
name: "authentication",
|
|
500
|
-
defaultBaseRoute: "authentication",
|
|
501
|
-
tags: tagsAuthentication,
|
|
502
|
-
generateRoutes: generateRestRoutesAuthentication
|
|
503
|
-
}
|
|
504
|
-
];
|
|
505
|
-
|
|
506
|
-
// Copyright 2024 IOTA Stiftung.
|
|
507
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
508
|
-
/**
|
|
509
|
-
* Initialize the schema for the authentication service.
|
|
510
|
-
*/
|
|
511
|
-
function initSchema() {
|
|
512
|
-
EntitySchemaFactory.register("AuthenticationUser", () => EntitySchemaHelper.getSchema(AuthenticationUser));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Copyright 2024 IOTA Stiftung.
|
|
516
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
517
|
-
/**
|
|
518
|
-
* Helper class for password operations.
|
|
519
|
-
*/
|
|
520
|
-
class PasswordHelper {
|
|
521
|
-
/**
|
|
522
|
-
* Runtime name for the class.
|
|
523
|
-
* @internal
|
|
524
|
-
*/
|
|
525
|
-
static _CLASS_NAME = "PasswordHelper";
|
|
526
|
-
/**
|
|
527
|
-
* Hash the password for the user.
|
|
528
|
-
* @param passwordBytes The password bytes.
|
|
529
|
-
* @param saltBytes The salt bytes.
|
|
530
|
-
* @returns The hashed password.
|
|
531
|
-
*/
|
|
532
|
-
static async hashPassword(passwordBytes, saltBytes) {
|
|
533
|
-
Guards.uint8Array(PasswordHelper._CLASS_NAME, "passwordBytes", passwordBytes);
|
|
534
|
-
Guards.uint8Array(PasswordHelper._CLASS_NAME, "saltBytes", saltBytes);
|
|
535
|
-
const combined = new Uint8Array(saltBytes.length + passwordBytes.length);
|
|
536
|
-
combined.set(saltBytes);
|
|
537
|
-
combined.set(passwordBytes, saltBytes.length);
|
|
538
|
-
const hashedPassword = Blake2b.sum256(combined);
|
|
539
|
-
return Converter.bytesToBase64(hashedPassword);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Implementation of the authentication component using entity storage.
|
|
545
|
-
*/
|
|
546
|
-
class EntityStorageAuthenticationAdminService {
|
|
547
|
-
/**
|
|
548
|
-
* The minimum password length.
|
|
549
|
-
* @internal
|
|
550
|
-
*/
|
|
551
|
-
static _DEFAULT_MIN_PASSWORD_LENGTH = 8;
|
|
552
|
-
/**
|
|
553
|
-
* Runtime name for the class.
|
|
554
|
-
*/
|
|
555
|
-
CLASS_NAME = "EntityStorageAuthenticationAdminService";
|
|
556
|
-
/**
|
|
557
|
-
* The entity storage for users.
|
|
558
|
-
* @internal
|
|
559
|
-
*/
|
|
560
|
-
_userEntityStorage;
|
|
561
|
-
/**
|
|
562
|
-
* The minimum password length.
|
|
563
|
-
* @internal
|
|
564
|
-
*/
|
|
565
|
-
_minPasswordLength;
|
|
566
|
-
/**
|
|
567
|
-
* Create a new instance of EntityStorageAuthentication.
|
|
568
|
-
* @param options The dependencies for the identity connector.
|
|
569
|
-
*/
|
|
570
|
-
constructor(options) {
|
|
571
|
-
this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
|
|
572
|
-
this._minPasswordLength =
|
|
573
|
-
options?.config?.minPasswordLength ??
|
|
574
|
-
EntityStorageAuthenticationAdminService._DEFAULT_MIN_PASSWORD_LENGTH;
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Create a login for the user.
|
|
578
|
-
* @param email The email address for the user.
|
|
579
|
-
* @param password The password for the user.
|
|
580
|
-
* @param identity The DID to associate with the account.
|
|
581
|
-
* @returns Nothing.
|
|
582
|
-
*/
|
|
583
|
-
async create(email, password, identity) {
|
|
584
|
-
Guards.stringValue(this.CLASS_NAME, "email", email);
|
|
585
|
-
Guards.stringValue(this.CLASS_NAME, "password", password);
|
|
586
|
-
try {
|
|
587
|
-
if (password.length < this._minPasswordLength) {
|
|
588
|
-
throw new GeneralError(this.CLASS_NAME, "passwordTooShort", {
|
|
589
|
-
minLength: this._minPasswordLength
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
const user = await this._userEntityStorage.get(email);
|
|
593
|
-
if (user) {
|
|
594
|
-
throw new GeneralError(this.CLASS_NAME, "userExists");
|
|
595
|
-
}
|
|
596
|
-
const saltBytes = RandomHelper.generate(16);
|
|
597
|
-
const passwordBytes = Converter.utf8ToBytes(password);
|
|
598
|
-
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
599
|
-
const newUser = {
|
|
600
|
-
email,
|
|
601
|
-
salt: Converter.bytesToBase64(saltBytes),
|
|
602
|
-
password: hashedPassword,
|
|
603
|
-
identity
|
|
604
|
-
};
|
|
605
|
-
await this._userEntityStorage.set(newUser);
|
|
606
|
-
}
|
|
607
|
-
catch (error) {
|
|
608
|
-
throw new GeneralError(this.CLASS_NAME, "createUserFailed", undefined, error);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Remove the current user.
|
|
613
|
-
* @param email The email address of the user to remove.
|
|
614
|
-
* @returns Nothing.
|
|
615
|
-
*/
|
|
616
|
-
async remove(email) {
|
|
617
|
-
Guards.stringValue(this.CLASS_NAME, "email", email);
|
|
618
|
-
try {
|
|
619
|
-
const user = await this._userEntityStorage.get(email);
|
|
620
|
-
if (!user) {
|
|
621
|
-
throw new NotFoundError(this.CLASS_NAME, "userNotFound", email);
|
|
622
|
-
}
|
|
623
|
-
await this._userEntityStorage.remove(email);
|
|
624
|
-
}
|
|
625
|
-
catch (error) {
|
|
626
|
-
throw new GeneralError(this.CLASS_NAME, "removeUserFailed", undefined, error);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Update the user's password.
|
|
631
|
-
* @param email The email address of the user to update.
|
|
632
|
-
* @param newPassword The new password for the user.
|
|
633
|
-
* @param currentPassword The current password, optional, if supplied will check against existing.
|
|
634
|
-
* @returns Nothing.
|
|
635
|
-
*/
|
|
636
|
-
async updatePassword(email, newPassword, currentPassword) {
|
|
637
|
-
Guards.stringValue(this.CLASS_NAME, "email", email);
|
|
638
|
-
Guards.stringValue(this.CLASS_NAME, "newPassword", newPassword);
|
|
639
|
-
try {
|
|
640
|
-
if (newPassword.length < this._minPasswordLength) {
|
|
641
|
-
throw new GeneralError(this.CLASS_NAME, "passwordTooShort", {
|
|
642
|
-
minLength: this._minPasswordLength
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
const user = await this._userEntityStorage.get(email);
|
|
646
|
-
if (!user) {
|
|
647
|
-
throw new NotFoundError(this.CLASS_NAME, "userNotFound", email);
|
|
648
|
-
}
|
|
649
|
-
if (Is.stringValue(currentPassword)) {
|
|
650
|
-
const saltBytes = Converter.base64ToBytes(user.salt);
|
|
651
|
-
const passwordBytes = Converter.utf8ToBytes(currentPassword);
|
|
652
|
-
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
653
|
-
if (hashedPassword !== user.password) {
|
|
654
|
-
throw new GeneralError(this.CLASS_NAME, "currentPasswordMismatch");
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const saltBytes = RandomHelper.generate(16);
|
|
658
|
-
const passwordBytes = Converter.utf8ToBytes(newPassword);
|
|
659
|
-
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
660
|
-
const updatedUser = {
|
|
661
|
-
email,
|
|
662
|
-
salt: Converter.bytesToBase64(saltBytes),
|
|
663
|
-
password: hashedPassword,
|
|
664
|
-
identity: user.identity
|
|
665
|
-
};
|
|
666
|
-
await this._userEntityStorage.set(updatedUser);
|
|
667
|
-
}
|
|
668
|
-
catch (error) {
|
|
669
|
-
throw new GeneralError(this.CLASS_NAME, "updatePasswordFailed", undefined, error);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Implementation of the authentication component using entity storage.
|
|
676
|
-
*/
|
|
677
|
-
class EntityStorageAuthenticationService {
|
|
678
|
-
/**
|
|
679
|
-
* Default TTL in minutes.
|
|
680
|
-
* @internal
|
|
681
|
-
*/
|
|
682
|
-
static _DEFAULT_TTL_MINUTES = 60;
|
|
683
|
-
/**
|
|
684
|
-
* Runtime name for the class.
|
|
685
|
-
*/
|
|
686
|
-
CLASS_NAME = "EntityStorageAuthenticationService";
|
|
687
|
-
/**
|
|
688
|
-
* The user admin service.
|
|
689
|
-
* @internal
|
|
690
|
-
*/
|
|
691
|
-
_authenticationAdminService;
|
|
692
|
-
/**
|
|
693
|
-
* The entity storage for users.
|
|
694
|
-
* @internal
|
|
695
|
-
*/
|
|
696
|
-
_userEntityStorage;
|
|
697
|
-
/**
|
|
698
|
-
* The vault for the keys.
|
|
699
|
-
* @internal
|
|
700
|
-
*/
|
|
701
|
-
_vaultConnector;
|
|
702
|
-
/**
|
|
703
|
-
* The name of the key to retrieve from the vault for signing JWT.
|
|
704
|
-
* @internal
|
|
705
|
-
*/
|
|
706
|
-
_signingKeyName;
|
|
707
|
-
/**
|
|
708
|
-
* The default TTL for the token.
|
|
709
|
-
* @internal
|
|
710
|
-
*/
|
|
711
|
-
_defaultTtlMinutes;
|
|
712
|
-
/**
|
|
713
|
-
* The node identity.
|
|
714
|
-
* @internal
|
|
715
|
-
*/
|
|
716
|
-
_nodeIdentity;
|
|
717
|
-
/**
|
|
718
|
-
* Create a new instance of EntityStorageAuthentication.
|
|
719
|
-
* @param options The dependencies for the identity connector.
|
|
720
|
-
*/
|
|
721
|
-
constructor(options) {
|
|
722
|
-
this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
|
|
723
|
-
this._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
|
|
724
|
-
this._authenticationAdminService = ComponentFactory.get(options?.authenticationAdminServiceType ?? "authentication-admin");
|
|
725
|
-
this._signingKeyName = options?.config?.signingKeyName ?? "auth-signing";
|
|
726
|
-
this._defaultTtlMinutes =
|
|
727
|
-
options?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* The service needs to be started when the application is initialized.
|
|
731
|
-
* @param nodeIdentity The identity of the node.
|
|
732
|
-
* @param nodeLoggingComponentType The node logging component type.
|
|
733
|
-
* @returns Nothing.
|
|
734
|
-
*/
|
|
735
|
-
async start(nodeIdentity, nodeLoggingComponentType) {
|
|
736
|
-
Guards.string(this.CLASS_NAME, "nodeIdentity", nodeIdentity);
|
|
737
|
-
this._nodeIdentity = nodeIdentity;
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Perform a login for the user.
|
|
741
|
-
* @param email The email address for the user.
|
|
742
|
-
* @param password The password for the user.
|
|
743
|
-
* @returns The authentication token for the user, if it uses a mechanism with public access.
|
|
744
|
-
*/
|
|
745
|
-
async login(email, password) {
|
|
746
|
-
Guards.stringValue(this.CLASS_NAME, "email", email);
|
|
747
|
-
Guards.stringValue(this.CLASS_NAME, "password", password);
|
|
748
|
-
try {
|
|
749
|
-
const user = await this._userEntityStorage.get(email);
|
|
750
|
-
if (!user) {
|
|
751
|
-
throw new GeneralError(this.CLASS_NAME, "userNotFound");
|
|
752
|
-
}
|
|
753
|
-
const saltBytes = Converter.base64ToBytes(user.salt);
|
|
754
|
-
const passwordBytes = Converter.utf8ToBytes(password);
|
|
755
|
-
const hashedPassword = await PasswordHelper.hashPassword(passwordBytes, saltBytes);
|
|
756
|
-
if (hashedPassword !== user.password) {
|
|
757
|
-
throw new GeneralError(this.CLASS_NAME, "passwordMismatch");
|
|
758
|
-
}
|
|
759
|
-
const tokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeIdentity}/${this._signingKeyName}`, user.identity, this._defaultTtlMinutes);
|
|
760
|
-
return tokenAndExpiry;
|
|
761
|
-
}
|
|
762
|
-
catch (error) {
|
|
763
|
-
throw new UnauthorizedError(this.CLASS_NAME, "loginFailed", error);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Logout the current user.
|
|
768
|
-
* @param token The token to logout, if it uses a mechanism with public access.
|
|
769
|
-
* @returns Nothing.
|
|
770
|
-
*/
|
|
771
|
-
async logout(token) {
|
|
772
|
-
// Nothing to do here.
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Refresh the token.
|
|
776
|
-
* @param token The token to refresh, if it uses a mechanism with public access.
|
|
777
|
-
* @returns The refreshed token, if it uses a mechanism with public access.
|
|
778
|
-
*/
|
|
779
|
-
async refresh(token) {
|
|
780
|
-
// If the verify fails on the current token then it will throw an exception.
|
|
781
|
-
const headerAndPayload = await TokenHelper.verify(this._vaultConnector, `${this._nodeIdentity}/${this._signingKeyName}`, token);
|
|
782
|
-
const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeIdentity}/${this._signingKeyName}`, headerAndPayload.payload.sub ?? "", this._defaultTtlMinutes);
|
|
783
|
-
return refreshTokenAndExpiry;
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* Update the user's password.
|
|
787
|
-
* @param email The email address of the user to update.
|
|
788
|
-
* @param currentPassword The current password for the user.
|
|
789
|
-
* @param newPassword The new password for the user.
|
|
790
|
-
* @returns Nothing.
|
|
791
|
-
*/
|
|
792
|
-
async updatePassword(email, currentPassword, newPassword) {
|
|
793
|
-
return this._authenticationAdminService.updatePassword(email, newPassword, currentPassword);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export { AuthHeaderProcessor, AuthenticationUser, EntityStorageAuthenticationAdminService, EntityStorageAuthenticationService, PasswordHelper, TokenHelper, authenticationLogin, authenticationLogout, authenticationRefreshToken, authenticationUpdatePassword, generateRestRoutesAuthentication, initSchema, restEntryPoints, tagsAuthentication };
|