@gugananuvem/aws-local-simulator 1.0.15 → 1.0.16
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/README.md +789 -594
- package/bin/aws-local-simulator.js +63 -63
- package/package.json +2 -2
- package/src/config/config-loader.js +114 -114
- package/src/config/default-config.js +68 -68
- package/src/config/env-loader.js +68 -68
- package/src/index.js +146 -146
- package/src/index.mjs +123 -123
- package/src/server.js +227 -227
- package/src/services/apigateway/index.js +75 -73
- package/src/services/apigateway/server.js +570 -507
- package/src/services/apigateway/simulator.js +1261 -1261
- package/src/services/athena/index.js +75 -75
- package/src/services/athena/server.js +101 -101
- package/src/services/athena/simulador.js +998 -998
- package/src/services/athena/simulator.js +346 -346
- package/src/services/cloudformation/index.js +106 -106
- package/src/services/cloudformation/server.js +417 -417
- package/src/services/cloudformation/simulador.js +1045 -1045
- package/src/services/cloudtrail/index.js +84 -84
- package/src/services/cloudtrail/server.js +235 -235
- package/src/services/cloudtrail/simulador.js +719 -719
- package/src/services/cloudwatch/index.js +84 -84
- package/src/services/cloudwatch/server.js +366 -366
- package/src/services/cloudwatch/simulador.js +1173 -1173
- package/src/services/cognito/index.js +79 -79
- package/src/services/cognito/server.js +301 -301
- package/src/services/cognito/simulator.js +1655 -1655
- package/src/services/config/index.js +96 -96
- package/src/services/config/server.js +215 -215
- package/src/services/config/simulador.js +1260 -1260
- package/src/services/dynamodb/index.js +74 -74
- package/src/services/dynamodb/server.js +125 -125
- package/src/services/dynamodb/simulator.js +630 -630
- package/src/services/ecs/index.js +65 -65
- package/src/services/ecs/server.js +235 -235
- package/src/services/ecs/simulator.js +844 -844
- package/src/services/eventbridge/index.js +89 -89
- package/src/services/eventbridge/server.js +209 -209
- package/src/services/eventbridge/simulator.js +684 -684
- package/src/services/index.js +45 -45
- package/src/services/kms/index.js +75 -75
- package/src/services/kms/server.js +67 -67
- package/src/services/kms/simulator.js +324 -324
- package/src/services/lambda/handler-loader.js +183 -183
- package/src/services/lambda/index.js +78 -78
- package/src/services/lambda/route-registry.js +274 -274
- package/src/services/lambda/server.js +145 -145
- package/src/services/lambda/simulator.js +199 -199
- package/src/services/parameter-store/index.js +80 -80
- package/src/services/parameter-store/server.js +50 -50
- package/src/services/parameter-store/simulator.js +201 -201
- package/src/services/s3/index.js +73 -73
- package/src/services/s3/server.js +329 -329
- package/src/services/s3/simulator.js +565 -565
- package/src/services/secret-manager/index.js +80 -80
- package/src/services/secret-manager/server.js +50 -50
- package/src/services/secret-manager/simulator.js +171 -171
- package/src/services/sns/index.js +89 -89
- package/src/services/sns/server.js +580 -580
- package/src/services/sns/simulator.js +1482 -1482
- package/src/services/sqs/index.js +98 -93
- package/src/services/sqs/server.js +349 -349
- package/src/services/sqs/simulator.js +441 -441
- package/src/services/sts/index.js +37 -37
- package/src/services/sts/server.js +144 -144
- package/src/services/sts/simulator.js +69 -69
- package/src/services/xray/index.js +83 -83
- package/src/services/xray/server.js +308 -308
- package/src/services/xray/simulador.js +994 -994
- package/src/template/aws-config-template.js +87 -87
- package/src/template/aws-config-template.mjs +90 -90
- package/src/template/config-template.json +203 -203
- package/src/utils/aws-config.js +91 -91
- package/src/utils/cloudtrail-audit.js +129 -129
- package/src/utils/local-store.js +83 -83
- package/src/utils/logger.js +59 -59
|
@@ -1,1655 +1,1655 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cognito Simulator Core
|
|
3
|
-
* Simula User Pools, Identity Pools, Autenticação, Tokens JWT
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const crypto = require("crypto");
|
|
7
|
-
const jwt = require("jsonwebtoken");
|
|
8
|
-
const { v4: uuidv4 } = require("uuid");
|
|
9
|
-
const logger = require("../../utils/logger");
|
|
10
|
-
const LocalStore = require("../../utils/local-store");
|
|
11
|
-
const path = require("path");
|
|
12
|
-
const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
13
|
-
|
|
14
|
-
class CognitoSimulator {
|
|
15
|
-
constructor(config) {
|
|
16
|
-
this.config = config;
|
|
17
|
-
this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, "cognito");
|
|
18
|
-
this.store = new LocalStore(this.dataDir);
|
|
19
|
-
this.userPools = new Map();
|
|
20
|
-
this.identityPools = new Map();
|
|
21
|
-
this.users = new Map();
|
|
22
|
-
this.sessions = new Map();
|
|
23
|
-
this.refreshTokens = new Map();
|
|
24
|
-
this.accessTokens = new Map();
|
|
25
|
-
this.jwtSecret = crypto.randomBytes(64).toString("hex");
|
|
26
|
-
this.audit = new CloudTrailAudit("cognito-idp.amazonaws.com");
|
|
27
|
-
this.lambdaSimulator = null;
|
|
28
|
-
this.customAuthSessions = new Map();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
setLambdaSimulator(lambdaSimulator) {
|
|
32
|
-
this.lambdaSimulator = lambdaSimulator;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
_warnUnregisteredTriggers(pool) {
|
|
36
|
-
const triggers = pool.LambdaTriggers || {};
|
|
37
|
-
for (const [triggerName, fnName] of Object.entries(triggers)) {
|
|
38
|
-
if (fnName && !this.lambdaSimulator?.getLambda(fnName)) {
|
|
39
|
-
logger.warn(`⚠️ Cognito pool "${pool.Name}": trigger "${triggerName}" references Lambda "${fnName}" which is not registered`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
_buildTriggerEvent(triggerSource, userPool, user, clientId, requestFields) {
|
|
45
|
-
return {
|
|
46
|
-
version: "1",
|
|
47
|
-
triggerSource,
|
|
48
|
-
region: "us-east-1",
|
|
49
|
-
userPoolId: userPool.Id,
|
|
50
|
-
userName: user.Username,
|
|
51
|
-
callerContext: {
|
|
52
|
-
awsSdkVersion: "aws-sdk-unknown-unknown",
|
|
53
|
-
clientId,
|
|
54
|
-
},
|
|
55
|
-
request: { ...requestFields },
|
|
56
|
-
response: {},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Returns userAttributes in the flat key/value format that real Cognito sends to triggers
|
|
61
|
-
_triggerUserAttributes(user) {
|
|
62
|
-
return {
|
|
63
|
-
sub: user.UserId,
|
|
64
|
-
"cognito:user_status": user.UserStatus,
|
|
65
|
-
...user.Attributes,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async _invokeTrigger(userPool, triggerName, event) {
|
|
70
|
-
const fnName = userPool.LambdaTriggers?.[triggerName];
|
|
71
|
-
if (!fnName) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!this.lambdaSimulator || !this.lambdaSimulator.getLambda(fnName)) {
|
|
76
|
-
logger.warn(`⚠️ Cognito trigger "${triggerName}": Lambda "${fnName}" is not available, skipping`);
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const result = await this.lambdaSimulator.invoke(fnName, event, "RequestResponse");
|
|
81
|
-
return result.Payload;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async initialize() {
|
|
85
|
-
logger.debug("Inicializando Cognito Simulator...");
|
|
86
|
-
this.loadUserPools();
|
|
87
|
-
this.loadIdentityPools();
|
|
88
|
-
this.loadUsers();
|
|
89
|
-
this.loadSessions();
|
|
90
|
-
|
|
91
|
-
logger.debug(`✅ Cognito Simulator inicializado com ${this.userPools.size} user pools, ${this.identityPools.size} identity pools, ${this.users.size} usuários`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ============ User Pool Operations ============
|
|
95
|
-
|
|
96
|
-
createUserPool(params) {
|
|
97
|
-
const { PoolName, Policies, LambdaConfig, AutoVerifiedAttributes, AliasAttributes, UsernameAttributes, MfaConfiguration, UserPoolId } = params;
|
|
98
|
-
|
|
99
|
-
const poolId = UserPoolId ? UserPoolId : `local_${PoolName}_${Date.now()}`;
|
|
100
|
-
const userPool = {
|
|
101
|
-
Id: poolId,
|
|
102
|
-
Name: PoolName,
|
|
103
|
-
Arn: `arn:aws:cognito:local:000000000000:userpool/${poolId}`,
|
|
104
|
-
Status: "ACTIVE",
|
|
105
|
-
CreationDate: new Date().toISOString(),
|
|
106
|
-
LastModifiedDate: new Date().toISOString(),
|
|
107
|
-
Policies: Policies || {
|
|
108
|
-
PasswordPolicy: {
|
|
109
|
-
MinimumLength: 8,
|
|
110
|
-
RequireUppercase: true,
|
|
111
|
-
RequireLowercase: true,
|
|
112
|
-
RequireNumbers: true,
|
|
113
|
-
RequireSymbols: false,
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
LambdaConfig: LambdaConfig || {},
|
|
117
|
-
AutoVerifiedAttributes: AutoVerifiedAttributes || ["email"],
|
|
118
|
-
AliasAttributes: AliasAttributes || [],
|
|
119
|
-
UsernameAttributes: UsernameAttributes || ["email"],
|
|
120
|
-
MfaConfiguration: MfaConfiguration || "OFF",
|
|
121
|
-
EstimatedNumberOfUsers: 0,
|
|
122
|
-
Users: [],
|
|
123
|
-
Clients: new Map(),
|
|
124
|
-
Groups: new Map(),
|
|
125
|
-
IdentityProviders: new Map(),
|
|
126
|
-
ResourceServers: new Map(),
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
this.userPools.set(poolId, userPool);
|
|
130
|
-
this.persistUserPools();
|
|
131
|
-
|
|
132
|
-
logger.debug(`✅ User Pool criado: ${PoolName} (${poolId})`);
|
|
133
|
-
this.audit.record({ eventName: "CreateUserPool", readOnly: false, resources: [{ ARN: userPool.Arn, type: "AWS::Cognito::UserPool" }], requestParameters: { poolName: PoolName } });
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
UserPool: {
|
|
137
|
-
Id: userPool.Id,
|
|
138
|
-
Name: userPool.Name,
|
|
139
|
-
Arn: userPool.Arn,
|
|
140
|
-
Status: userPool.Status,
|
|
141
|
-
CreationDate: userPool.CreationDate,
|
|
142
|
-
LastModifiedDate: userPool.LastModifiedDate,
|
|
143
|
-
MfaConfiguration: userPool.MfaConfiguration,
|
|
144
|
-
EstimatedNumberOfUsers: 0,
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
listUserPools(params = {}) {
|
|
150
|
-
const { MaxResults = 60, NextToken } = params;
|
|
151
|
-
let userPools = Array.from(this.userPools.values());
|
|
152
|
-
|
|
153
|
-
if (NextToken) {
|
|
154
|
-
const startIndex = parseInt(NextToken);
|
|
155
|
-
userPools = userPools.slice(startIndex);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const results = userPools.slice(0, MaxResults);
|
|
159
|
-
const nextToken = results.length === MaxResults ? String(MaxResults) : null;
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
UserPools: results.map((pool) => ({
|
|
163
|
-
Id: pool.Id,
|
|
164
|
-
Name: pool.Name,
|
|
165
|
-
Arn: pool.Arn,
|
|
166
|
-
Status: pool.Status,
|
|
167
|
-
CreationDate: pool.CreationDate,
|
|
168
|
-
LastModifiedDate: pool.LastModifiedDate,
|
|
169
|
-
})),
|
|
170
|
-
NextToken: nextToken,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
describeUserPool(params) {
|
|
175
|
-
const { UserPoolId } = params;
|
|
176
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
177
|
-
|
|
178
|
-
if (!userPool) {
|
|
179
|
-
throw new Error(`User pool ${UserPoolId} not found`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
UserPool: {
|
|
184
|
-
Id: userPool.Id,
|
|
185
|
-
Name: userPool.Name,
|
|
186
|
-
Arn: userPool.Arn,
|
|
187
|
-
Status: userPool.Status,
|
|
188
|
-
CreationDate: userPool.CreationDate,
|
|
189
|
-
LastModifiedDate: userPool.LastModifiedDate,
|
|
190
|
-
Policies: userPool.Policies,
|
|
191
|
-
LambdaConfig: userPool.LambdaConfig,
|
|
192
|
-
AutoVerifiedAttributes: userPool.AutoVerifiedAttributes,
|
|
193
|
-
AliasAttributes: userPool.AliasAttributes,
|
|
194
|
-
UsernameAttributes: userPool.UsernameAttributes,
|
|
195
|
-
MfaConfiguration: userPool.MfaConfiguration,
|
|
196
|
-
EstimatedNumberOfUsers: userPool.Users.length,
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
listUsers(params = {}) {
|
|
202
|
-
const { UserPoolId, Filter, Limit = 60, PaginationToken } = params;
|
|
203
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
204
|
-
|
|
205
|
-
if (!userPool) {
|
|
206
|
-
throw new Error(`User pool ${UserPoolId} not found`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let users = Array.from(this.users.values()).filter((u) => u.UserPoolId === UserPoolId);
|
|
210
|
-
|
|
211
|
-
if (PaginationToken) {
|
|
212
|
-
const startIndex = parseInt(PaginationToken);
|
|
213
|
-
users = users.slice(startIndex);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const results = users.slice(0, Limit);
|
|
217
|
-
const nextToken = results.length === Limit && users.length > Limit ? String(Limit) : null;
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
Users: results.map((u) => ({
|
|
221
|
-
Username: u.Username,
|
|
222
|
-
UserStatus: u.UserStatus,
|
|
223
|
-
Enabled: u.Enabled,
|
|
224
|
-
UserCreateDate: u.CreatedDate,
|
|
225
|
-
UserLastModifiedDate: u.LastModifiedDate,
|
|
226
|
-
Attributes: this.formatUserAttributes(u.Attributes),
|
|
227
|
-
})),
|
|
228
|
-
PaginationToken: nextToken,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
listUserPoolClients(params = {}) {
|
|
233
|
-
const { UserPoolId, MaxResults = 60, NextToken } = params;
|
|
234
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
235
|
-
if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
|
|
236
|
-
|
|
237
|
-
let clients = Array.from(userPool.Clients.values());
|
|
238
|
-
if (NextToken) clients = clients.slice(parseInt(NextToken));
|
|
239
|
-
const results = clients.slice(0, MaxResults);
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
UserPoolClients: results.map((c) => ({
|
|
243
|
-
ClientId: c.ClientId,
|
|
244
|
-
ClientName: c.ClientName,
|
|
245
|
-
UserPoolId: c.UserPoolId,
|
|
246
|
-
})),
|
|
247
|
-
NextToken: results.length === MaxResults && clients.length > MaxResults ? String(MaxResults) : null,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
describeUserPoolClient(params = {}) {
|
|
252
|
-
const { UserPoolId, ClientId } = params;
|
|
253
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
254
|
-
if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
|
|
255
|
-
const client = userPool.Clients.get(ClientId);
|
|
256
|
-
if (!client) throw new Error(`Client ${ClientId} not found`);
|
|
257
|
-
return { UserPoolClient: client };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
deleteUserPoolClient(params = {}) {
|
|
261
|
-
const { UserPoolId, ClientId } = params;
|
|
262
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
263
|
-
if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
|
|
264
|
-
userPool.Clients.delete(ClientId);
|
|
265
|
-
this.persistUserPools();
|
|
266
|
-
return {};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
forgotPassword(params = {}) {
|
|
270
|
-
const { ClientId, Username } = params;
|
|
271
|
-
const userPool = this.findUserPoolByClientId(ClientId);
|
|
272
|
-
if (!userPool) throw new Error(`Client ${ClientId} not found`);
|
|
273
|
-
|
|
274
|
-
const user = this.findUserByUsername(Username, ClientId);
|
|
275
|
-
if (!user) throw new Error(`User not found: ${Username}`);
|
|
276
|
-
|
|
277
|
-
if (user.UserStatus !== "CONFIRMED") {
|
|
278
|
-
const err = new Error("Cannot reset password for the user as there is no registered/verified email or phone_number");
|
|
279
|
-
err.code = "InvalidParameterException";
|
|
280
|
-
throw err;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const resetCode = Math.floor(100000 + Math.random() * 900000).toString();
|
|
284
|
-
user.PasswordResetCode = resetCode;
|
|
285
|
-
this.persistUsers();
|
|
286
|
-
|
|
287
|
-
const userEmail = user.Attributes.email || Username;
|
|
288
|
-
logger.info(`📧 [COGNITO] Código de redefinição de senha para "${Username}": ${resetCode}`);
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
CodeDeliveryDetails: {
|
|
292
|
-
Destination: userEmail,
|
|
293
|
-
DeliveryMedium: "EMAIL",
|
|
294
|
-
AttributeName: "email",
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
confirmForgotPassword(params = {}) {
|
|
300
|
-
const { ClientId, Username, ConfirmationCode, Password } = params;
|
|
301
|
-
const userPool = this.findUserPoolByClientId(ClientId);
|
|
302
|
-
if (!userPool) throw new Error(`Client ${ClientId} not found`);
|
|
303
|
-
|
|
304
|
-
const user = this.findUserByUsername(Username, ClientId);
|
|
305
|
-
if (!user) throw new Error(`User not found: ${Username}`);
|
|
306
|
-
|
|
307
|
-
if (user.UserStatus !== "CONFIRMED") {
|
|
308
|
-
const err = new Error("Cannot reset password for the user as there is no registered/verified email or phone_number");
|
|
309
|
-
err.code = "InvalidParameterException";
|
|
310
|
-
throw err;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (user.PasswordResetCode && user.PasswordResetCode !== ConfirmationCode) {
|
|
314
|
-
const err = new Error("Invalid verification code provided, please try again.");
|
|
315
|
-
err.code = "CodeMismatchException";
|
|
316
|
-
throw err;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
user.Password = this.hashPassword(Password);
|
|
320
|
-
delete user.PasswordResetCode;
|
|
321
|
-
this.persistUsers();
|
|
322
|
-
return {};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
changePassword(params = {}) {
|
|
326
|
-
const { AccessToken, PreviousPassword, ProposedPassword } = params;
|
|
327
|
-
const session = this.accessTokens.get(AccessToken);
|
|
328
|
-
if (!session) throw new Error("Invalid access token");
|
|
329
|
-
const user = this.users.get(session.UserId);
|
|
330
|
-
if (!user) throw new Error("User not found");
|
|
331
|
-
if (!this.verifyPassword(PreviousPassword, user.Password)) throw new Error("Incorrect previous password");
|
|
332
|
-
user.Password = this.hashPassword(ProposedPassword);
|
|
333
|
-
this.persistUsers();
|
|
334
|
-
return {};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async respondToAuthChallenge(params = {}) {
|
|
338
|
-
const { ChallengeName } = params;
|
|
339
|
-
|
|
340
|
-
// NEW_PASSWORD_REQUIRED — user was created by admin and must set a permanent password
|
|
341
|
-
if (ChallengeName === "NEW_PASSWORD_REQUIRED") {
|
|
342
|
-
const session = this.customAuthSessions.get(params.Session);
|
|
343
|
-
if (!session || session.challenge !== "NEW_PASSWORD_REQUIRED") {
|
|
344
|
-
throw new Error("Invalid session token");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const newPassword = params.ChallengeResponses?.NEW_PASSWORD;
|
|
348
|
-
if (!newPassword) throw new Error("NEW_PASSWORD is required");
|
|
349
|
-
|
|
350
|
-
const user = this.users.get(session.userId);
|
|
351
|
-
const userPool = this.userPools.get(session.userPoolId);
|
|
352
|
-
if (!user || !userPool) throw new Error("Invalid session");
|
|
353
|
-
|
|
354
|
-
// 1. PreAuthentication — dispara antes de processar a nova senha
|
|
355
|
-
const preAuthEvent = this._buildTriggerEvent("PreAuthentication_Authentication", userPool, user, session.clientId, {
|
|
356
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
357
|
-
validationData: {},
|
|
358
|
-
});
|
|
359
|
-
await this._invokeTrigger(userPool, "PreAuthentication", preAuthEvent);
|
|
360
|
-
|
|
361
|
-
// 2. Aplica nova senha e confirma usuário
|
|
362
|
-
user.Password = this.hashPassword(newPassword);
|
|
363
|
-
user.UserStatus = "CONFIRMED";
|
|
364
|
-
user.LastModifiedDate = new Date().toISOString();
|
|
365
|
-
this.persistUsers();
|
|
366
|
-
this.customAuthSessions.delete(params.Session);
|
|
367
|
-
|
|
368
|
-
logger.debug(`🔑 Senha alterada e usuário confirmado: ${user.Username}`);
|
|
369
|
-
|
|
370
|
-
// 3. PostConfirmation — dispara após confirmação do usuário (troca de senha forçada)
|
|
371
|
-
const postConfirmEvent = this._buildTriggerEvent("PostConfirmation_ConfirmSignUp", userPool, user, session.clientId, {
|
|
372
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
373
|
-
});
|
|
374
|
-
try {
|
|
375
|
-
await this._invokeTrigger(userPool, "PostConfirmation", postConfirmEvent);
|
|
376
|
-
} catch (err) {
|
|
377
|
-
logger.error(`PostConfirmation trigger error (ignored): ${err.message}`);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// 4. PreTokenGeneration — antes de gerar tokens
|
|
381
|
-
const preTokenEvent = this._buildTriggerEvent("TokenGeneration_Authentication", userPool, user, session.clientId, {
|
|
382
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
383
|
-
groupConfiguration: { groupsToOverride: [], iamRolesToOverride: [], preferredRole: null },
|
|
384
|
-
});
|
|
385
|
-
const preTokenResponse = await this._invokeTrigger(userPool, "PreTokenGeneration", preTokenEvent);
|
|
386
|
-
const claimsOverride = preTokenResponse?.response?.claimsOverrideDetails || null;
|
|
387
|
-
|
|
388
|
-
// 5. Gera tokens
|
|
389
|
-
const accessToken = this.generateAccessToken(user, userPool, session.clientId);
|
|
390
|
-
const idToken = this.generateIdToken(user, userPool, session.clientId, claimsOverride);
|
|
391
|
-
const refreshToken = this.generateRefreshToken(user, userPool, session.clientId);
|
|
392
|
-
|
|
393
|
-
const sessionId = uuidv4();
|
|
394
|
-
const authSession = {
|
|
395
|
-
Id: sessionId,
|
|
396
|
-
UserId: user.UserId,
|
|
397
|
-
UserPoolId: userPool.Id,
|
|
398
|
-
ClientId: session.clientId,
|
|
399
|
-
AccessToken: accessToken,
|
|
400
|
-
IdToken: idToken,
|
|
401
|
-
RefreshToken: refreshToken,
|
|
402
|
-
CreatedAt: new Date().toISOString(),
|
|
403
|
-
ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
404
|
-
};
|
|
405
|
-
this.sessions.set(sessionId, authSession);
|
|
406
|
-
this.accessTokens.set(accessToken, authSession);
|
|
407
|
-
this.refreshTokens.set(refreshToken, authSession);
|
|
408
|
-
this.persistSessions();
|
|
409
|
-
|
|
410
|
-
// 6. PostAuthentication — após auth bem-sucedida (non-blocking)
|
|
411
|
-
const postAuthEvent = this._buildTriggerEvent("PostAuthentication_Authentication", userPool, user, session.clientId, {
|
|
412
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
413
|
-
newDeviceUsed: false,
|
|
414
|
-
});
|
|
415
|
-
try {
|
|
416
|
-
await this._invokeTrigger(userPool, "PostAuthentication", postAuthEvent);
|
|
417
|
-
} catch (err) {
|
|
418
|
-
logger.error(`PostAuthentication trigger error (ignored): ${err.message}`);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
AuthenticationResult: {
|
|
423
|
-
AccessToken: accessToken,
|
|
424
|
-
IdToken: idToken,
|
|
425
|
-
RefreshToken: refreshToken,
|
|
426
|
-
TokenType: "Bearer",
|
|
427
|
-
ExpiresIn: 3600,
|
|
428
|
-
},
|
|
429
|
-
ChallengeName: null,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (ChallengeName === "CUSTOM_CHALLENGE") {
|
|
434
|
-
const session = this.customAuthSessions.get(params.Session);
|
|
435
|
-
if (!session) {
|
|
436
|
-
throw new Error("Invalid session token");
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const user = this.users.get(session.userId);
|
|
440
|
-
const userPool = this.userPools.get(session.userPoolId);
|
|
441
|
-
|
|
442
|
-
// Invoke VerifyAuthChallengeResponse
|
|
443
|
-
const verifyEvent = this._buildTriggerEvent("VerifyAuthChallengeResponse_Authentication", userPool, user, session.clientId, {
|
|
444
|
-
challengeAnswer: params.ChallengeResponses?.ANSWER,
|
|
445
|
-
privateChallengeParameters: session.privateChallengeParameters,
|
|
446
|
-
});
|
|
447
|
-
const verifyResponse = await this._invokeTrigger(userPool, "VerifyAuthChallengeResponse", verifyEvent);
|
|
448
|
-
|
|
449
|
-
// Append challenge result to session
|
|
450
|
-
session.session.push({
|
|
451
|
-
challengeName: "CUSTOM_CHALLENGE",
|
|
452
|
-
challengeResult: verifyResponse?.response?.answerCorrect === true,
|
|
453
|
-
challengeMetadata: "",
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// Invoke DefineAuthChallenge again with updated session
|
|
457
|
-
const defineEvent = this._buildTriggerEvent("DefineAuthChallenge_Authentication", userPool, user, session.clientId, {
|
|
458
|
-
session: session.session,
|
|
459
|
-
});
|
|
460
|
-
const defineResponse = await this._invokeTrigger(userPool, "DefineAuthChallenge", defineEvent);
|
|
461
|
-
|
|
462
|
-
if (defineResponse?.response?.issueTokens === true) {
|
|
463
|
-
// 1. PreTokenGeneration antes de gerar tokens
|
|
464
|
-
const preTokenEvent = this._buildTriggerEvent("TokenGeneration_Authentication", userPool, user, session.clientId, {
|
|
465
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
466
|
-
groupConfiguration: { groupsToOverride: [], iamRolesToOverride: [], preferredRole: null },
|
|
467
|
-
});
|
|
468
|
-
const preTokenResponse = await this._invokeTrigger(userPool, "PreTokenGeneration", preTokenEvent);
|
|
469
|
-
const claimsOverride = preTokenResponse?.response?.claimsOverrideDetails || null;
|
|
470
|
-
|
|
471
|
-
// 2. Gera tokens
|
|
472
|
-
const accessToken = this.generateAccessToken(user, userPool, session.clientId);
|
|
473
|
-
const idToken = this.generateIdToken(user, userPool, session.clientId, claimsOverride);
|
|
474
|
-
const refreshToken = this.generateRefreshToken(user, userPool, session.clientId);
|
|
475
|
-
|
|
476
|
-
const sessionId = uuidv4();
|
|
477
|
-
const authSession = {
|
|
478
|
-
Id: sessionId,
|
|
479
|
-
UserId: user.UserId,
|
|
480
|
-
UserPoolId: userPool.Id,
|
|
481
|
-
ClientId: session.clientId,
|
|
482
|
-
AccessToken: accessToken,
|
|
483
|
-
IdToken: idToken,
|
|
484
|
-
RefreshToken: refreshToken,
|
|
485
|
-
CreatedAt: new Date().toISOString(),
|
|
486
|
-
ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
this.sessions.set(sessionId, authSession);
|
|
490
|
-
this.accessTokens.set(accessToken, authSession);
|
|
491
|
-
this.refreshTokens.set(refreshToken, authSession);
|
|
492
|
-
this.persistSessions();
|
|
493
|
-
|
|
494
|
-
this.customAuthSessions.delete(params.Session);
|
|
495
|
-
|
|
496
|
-
// 3. PostAuthentication (non-blocking)
|
|
497
|
-
const postAuthEvent = this._buildTriggerEvent("PostAuthentication_Authentication", userPool, user, session.clientId, {
|
|
498
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
499
|
-
newDeviceUsed: false,
|
|
500
|
-
});
|
|
501
|
-
try {
|
|
502
|
-
await this._invokeTrigger(userPool, "PostAuthentication", postAuthEvent);
|
|
503
|
-
} catch (err) {
|
|
504
|
-
logger.error(`PostAuthentication trigger error (ignored): ${err.message}`);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return {
|
|
508
|
-
AuthenticationResult: {
|
|
509
|
-
AccessToken: accessToken,
|
|
510
|
-
IdToken: idToken,
|
|
511
|
-
RefreshToken: refreshToken,
|
|
512
|
-
TokenType: "Bearer",
|
|
513
|
-
ExpiresIn: 3600,
|
|
514
|
-
},
|
|
515
|
-
ChallengeName: null,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Issue next challenge
|
|
520
|
-
const createEvent = this._buildTriggerEvent("CreateAuthChallenge_Authentication", userPool, user, session.clientId, {
|
|
521
|
-
challengeName: "CUSTOM_CHALLENGE",
|
|
522
|
-
session: session.session,
|
|
523
|
-
});
|
|
524
|
-
const createResponse = await this._invokeTrigger(userPool, "CreateAuthChallenge", createEvent);
|
|
525
|
-
|
|
526
|
-
// Update stored private challenge parameters for next round
|
|
527
|
-
session.privateChallengeParameters = createResponse?.response?.privateChallengeParameters || {};
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
ChallengeName: "CUSTOM_CHALLENGE",
|
|
531
|
-
ChallengeParameters: createResponse?.response?.publicChallengeParameters || {},
|
|
532
|
-
Session: params.Session,
|
|
533
|
-
AuthenticationResult: null,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
return { AuthenticationResult: null, ChallengeName: null };
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
revokeToken(params = {}) {
|
|
541
|
-
const { Token, ClientId } = params;
|
|
542
|
-
// Remove refresh token session if it exists
|
|
543
|
-
const session = this.refreshTokens.get(Token);
|
|
544
|
-
if (session) {
|
|
545
|
-
this.sessions.delete(session.Id);
|
|
546
|
-
this.accessTokens.delete(session.AccessToken);
|
|
547
|
-
this.refreshTokens.delete(Token);
|
|
548
|
-
this.persistSessions();
|
|
549
|
-
}
|
|
550
|
-
return {};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
globalSignOut(params = {}) {
|
|
554
|
-
const { AccessToken } = params;
|
|
555
|
-
const session = this.accessTokens.get(AccessToken);
|
|
556
|
-
if (session) {
|
|
557
|
-
this.sessions.delete(session.Id);
|
|
558
|
-
this.accessTokens.delete(AccessToken);
|
|
559
|
-
this.refreshTokens.delete(session.RefreshToken);
|
|
560
|
-
this.persistSessions();
|
|
561
|
-
}
|
|
562
|
-
return {};
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
getUser(params = {}) {
|
|
566
|
-
const { AccessToken } = params;
|
|
567
|
-
const session = this.accessTokens.get(AccessToken);
|
|
568
|
-
if (!session) throw new Error("Invalid access token");
|
|
569
|
-
// Check session is still active
|
|
570
|
-
if (!this.sessions.has(session.Id)) throw new Error("Token has been revoked");
|
|
571
|
-
const user = this.users.get(session.UserId);
|
|
572
|
-
if (!user) throw new Error("User not found");
|
|
573
|
-
return {
|
|
574
|
-
Username: user.Username,
|
|
575
|
-
UserAttributes: this.formatUserAttributes(user.Attributes),
|
|
576
|
-
UserStatus: user.UserStatus,
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
updateUserAttributes(params = {}) {
|
|
581
|
-
const { AccessToken, UserAttributes } = params;
|
|
582
|
-
const session = this.accessTokens.get(AccessToken);
|
|
583
|
-
if (!session) throw new Error("Invalid access token");
|
|
584
|
-
const user = this.users.get(session.UserId);
|
|
585
|
-
if (!user) throw new Error("User not found");
|
|
586
|
-
const updates = this.normalizeUserAttributes(UserAttributes || []);
|
|
587
|
-
Object.assign(user.Attributes, updates);
|
|
588
|
-
this.persistUsers();
|
|
589
|
-
return { CodeDeliveryDetailsList: [] };
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
deleteUser(params = {}) {
|
|
593
|
-
const { AccessToken } = params;
|
|
594
|
-
const session = this.accessTokens.get(AccessToken);
|
|
595
|
-
if (!session) throw new Error("Invalid access token");
|
|
596
|
-
this.users.delete(session.UserId);
|
|
597
|
-
this.persistUsers();
|
|
598
|
-
return {};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
adminDisableUser(params = {}) {
|
|
602
|
-
const { UserPoolId, Username } = params;
|
|
603
|
-
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
604
|
-
if (!user) throw new Error(`User not found: ${Username}`);
|
|
605
|
-
user.Enabled = false;
|
|
606
|
-
this.persistUsers();
|
|
607
|
-
return {};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
adminEnableUser(params = {}) {
|
|
611
|
-
const { UserPoolId, Username } = params;
|
|
612
|
-
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
613
|
-
if (!user) throw new Error(`User not found: ${Username}`);
|
|
614
|
-
user.Enabled = true;
|
|
615
|
-
this.persistUsers();
|
|
616
|
-
return {};
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
adminResetUserPassword(params = {}) {
|
|
620
|
-
const { UserPoolId, Username } = params;
|
|
621
|
-
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
622
|
-
if (!user) throw new Error(`User not found: ${Username}`);
|
|
623
|
-
user.UserStatus = "RESET_REQUIRED";
|
|
624
|
-
this.persistUsers();
|
|
625
|
-
return {};
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
adminUserGlobalSignOut(params = {}) {
|
|
629
|
-
const { UserPoolId, Username } = params;
|
|
630
|
-
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
631
|
-
if (!user) throw new Error(`User not found: ${Username}`);
|
|
632
|
-
// Invalidate all sessions for this user
|
|
633
|
-
for (const [id, session] of this.sessions.entries()) {
|
|
634
|
-
if (session.UserId === user.UserId) {
|
|
635
|
-
this.accessTokens.delete(session.AccessToken);
|
|
636
|
-
this.refreshTokens.delete(session.RefreshToken);
|
|
637
|
-
this.sessions.delete(id);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
this.persistSessions();
|
|
641
|
-
return {};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
adminListGroupsForUser(params = {}) {
|
|
645
|
-
return { Groups: [], NextToken: null };
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
deleteUserPool(params) {
|
|
649
|
-
const { UserPoolId } = params;
|
|
650
|
-
|
|
651
|
-
if (!this.userPools.has(UserPoolId)) {
|
|
652
|
-
throw new Error(`User pool ${UserPoolId} not found`);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
this.userPools.delete(UserPoolId);
|
|
656
|
-
this.persistUserPools();
|
|
657
|
-
|
|
658
|
-
return {};
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// ============ User Pool Client Operations ============
|
|
662
|
-
|
|
663
|
-
createUserPoolClient(params) {
|
|
664
|
-
const { UserPoolId, ClientName, GenerateSecret, RefreshTokenValidity, AccessTokenValidity, IdTokenValidity, AllowedOAuthFlows, AllowedOAuthScopes, CallbackURLs, LogoutURLs } = params;
|
|
665
|
-
|
|
666
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
667
|
-
if (!userPool) {
|
|
668
|
-
throw new Error(`User pool ${UserPoolId} not found`);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const clientId = crypto.randomBytes(20).toString("hex");
|
|
672
|
-
const clientSecret = GenerateSecret ? crypto.randomBytes(32).toString("hex") : null;
|
|
673
|
-
|
|
674
|
-
const client = {
|
|
675
|
-
ClientId: clientId,
|
|
676
|
-
ClientName: ClientName,
|
|
677
|
-
ClientSecret: clientSecret,
|
|
678
|
-
UserPoolId: UserPoolId,
|
|
679
|
-
RefreshTokenValidity: RefreshTokenValidity || 30,
|
|
680
|
-
AccessTokenValidity: AccessTokenValidity || 1,
|
|
681
|
-
IdTokenValidity: IdTokenValidity || 1,
|
|
682
|
-
AllowedOAuthFlows: AllowedOAuthFlows || ["code"],
|
|
683
|
-
AllowedOAuthScopes: AllowedOAuthScopes || ["openid", "email", "profile"],
|
|
684
|
-
CallbackURLs: CallbackURLs || [],
|
|
685
|
-
LogoutURLs: LogoutURLs || [],
|
|
686
|
-
CreatedDate: new Date().toISOString(),
|
|
687
|
-
LastModifiedDate: new Date().toISOString(),
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
userPool.Clients.set(clientId, client);
|
|
691
|
-
this.persistUserPools();
|
|
692
|
-
|
|
693
|
-
logger.debug(`✅ User Pool Client criado: ${ClientName} (${clientId})`);
|
|
694
|
-
|
|
695
|
-
return {
|
|
696
|
-
UserPoolClient: {
|
|
697
|
-
ClientId: client.ClientId,
|
|
698
|
-
ClientName: client.ClientName,
|
|
699
|
-
ClientSecret: client.ClientSecret,
|
|
700
|
-
UserPoolId: client.UserPoolId,
|
|
701
|
-
RefreshTokenValidity: client.RefreshTokenValidity,
|
|
702
|
-
AccessTokenValidity: client.AccessTokenValidity,
|
|
703
|
-
IdTokenValidity: client.IdTokenValidity,
|
|
704
|
-
AllowedOAuthFlows: client.AllowedOAuthFlows,
|
|
705
|
-
AllowedOAuthScopes: client.AllowedOAuthScopes,
|
|
706
|
-
CallbackURLs: client.CallbackURLs,
|
|
707
|
-
LogoutURLs: client.LogoutURLs,
|
|
708
|
-
CreationDate: client.CreatedDate,
|
|
709
|
-
},
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// ============ User Operations ============
|
|
714
|
-
|
|
715
|
-
async signUp(params = {}) {
|
|
716
|
-
const { ClientId, Username, Password, UserAttributes, ValidationData } = params;
|
|
717
|
-
|
|
718
|
-
// Encontra o user pool pelo client id
|
|
719
|
-
const userPool = this.findUserPoolByClientId(ClientId);
|
|
720
|
-
if (!userPool) {
|
|
721
|
-
throw new Error(`Client ${ClientId} not found`);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Verifica se usuário já existe
|
|
725
|
-
const existingUser = Array.from(this.users.values()).find((u) => u.Username === Username && u.UserPoolId === userPool.Id);
|
|
726
|
-
|
|
727
|
-
if (existingUser) {
|
|
728
|
-
throw new Error(`User already exists: ${Username}`);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const userId = uuidv4();
|
|
732
|
-
const confirmationCode = Math.floor(100000 + Math.random() * 900000).toString();
|
|
733
|
-
|
|
734
|
-
const user = {
|
|
735
|
-
Username: Username,
|
|
736
|
-
UserPoolId: userPool.Id,
|
|
737
|
-
UserId: userId,
|
|
738
|
-
Attributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
739
|
-
Enabled: true,
|
|
740
|
-
UserStatus: "UNCONFIRMED",
|
|
741
|
-
CreatedDate: new Date().toISOString(),
|
|
742
|
-
LastModifiedDate: new Date().toISOString(),
|
|
743
|
-
Password: this.hashPassword(Password),
|
|
744
|
-
ConfirmationCode: confirmationCode,
|
|
745
|
-
MfaOptions: [],
|
|
746
|
-
PreferredMfaSetting: null,
|
|
747
|
-
UserMFASettingList: [],
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
// Invoke PreSignUp trigger before persisting the user
|
|
751
|
-
const event = this._buildTriggerEvent("PreSignUp_SignUp", userPool, user, ClientId, {
|
|
752
|
-
userAttributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
753
|
-
validationData: ValidationData || {},
|
|
754
|
-
clientMetadata: {},
|
|
755
|
-
});
|
|
756
|
-
const triggerResponse = await this._invokeTrigger(userPool, "PreSignUp", event);
|
|
757
|
-
|
|
758
|
-
if (triggerResponse !== null) {
|
|
759
|
-
if (triggerResponse.response?.autoConfirmUser === true) {
|
|
760
|
-
user.UserStatus = "CONFIRMED";
|
|
761
|
-
delete user.ConfirmationCode;
|
|
762
|
-
}
|
|
763
|
-
if (triggerResponse.response?.autoVerifyEmail === true) {
|
|
764
|
-
user.Attributes.email_verified = "true";
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
this.users.set(userId, user);
|
|
769
|
-
userPool.Users.push(userId);
|
|
770
|
-
userPool.EstimatedNumberOfUsers++;
|
|
771
|
-
this.persistUsers();
|
|
772
|
-
this.persistUserPools();
|
|
773
|
-
|
|
774
|
-
const userEmail = user.Attributes.email || Username;
|
|
775
|
-
|
|
776
|
-
if (user.UserStatus === "UNCONFIRMED") {
|
|
777
|
-
logger.info(`📧 [COGNITO] Código de confirmação para "${Username}": ${confirmationCode}`);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
logger.debug(`✅ Usuário criado: ${Username} (${userId}) — status: ${user.UserStatus}`);
|
|
781
|
-
|
|
782
|
-
// PostConfirmation — dispara se usuário foi auto-confirmado pelo PreSignUp
|
|
783
|
-
if (user.UserStatus === "CONFIRMED") {
|
|
784
|
-
const postConfirmEvent = this._buildTriggerEvent("PostConfirmation_ConfirmSignUp", userPool, user, ClientId, {
|
|
785
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
786
|
-
});
|
|
787
|
-
try {
|
|
788
|
-
await this._invokeTrigger(userPool, "PostConfirmation", postConfirmEvent);
|
|
789
|
-
} catch (err) {
|
|
790
|
-
logger.error(`PostConfirmation trigger error (ignored): ${err.message}`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return {
|
|
795
|
-
UserConfirmed: user.UserStatus === "CONFIRMED",
|
|
796
|
-
UserSub: userId,
|
|
797
|
-
CodeDeliveryDetails: user.UserStatus === "UNCONFIRMED"
|
|
798
|
-
? { Destination: userEmail, DeliveryMedium: "EMAIL", AttributeName: "email" }
|
|
799
|
-
: null,
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
async confirmSignUp(params) {
|
|
804
|
-
const { ClientId, Username, ConfirmationCode } = params;
|
|
805
|
-
|
|
806
|
-
const userPool = this.findUserPoolByClientId(ClientId);
|
|
807
|
-
if (!userPool) {
|
|
808
|
-
throw new Error(`Client ${ClientId} not found`);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const user = this.findUserByUsername(Username, ClientId);
|
|
812
|
-
if (!user) {
|
|
813
|
-
throw new Error(`User not found: ${Username}`);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if (user.ConfirmationCode && user.ConfirmationCode !== ConfirmationCode) {
|
|
817
|
-
throw new Error("Invalid verification code provided, please try again.");
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
user.UserStatus = "CONFIRMED";
|
|
821
|
-
user.LastModifiedDate = new Date().toISOString();
|
|
822
|
-
delete user.ConfirmationCode;
|
|
823
|
-
this.persistUsers();
|
|
824
|
-
|
|
825
|
-
const event = this._buildTriggerEvent("PostConfirmation_ConfirmSignUp", userPool, user, ClientId, {
|
|
826
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
827
|
-
});
|
|
828
|
-
try {
|
|
829
|
-
await this._invokeTrigger(userPool, "PostConfirmation", event);
|
|
830
|
-
} catch (err) {
|
|
831
|
-
logger.error(`PostConfirmation trigger error (ignored): ${err.message}`);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
return {};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
async initiateAuth(params) {
|
|
838
|
-
const { AuthFlow, ClientId, AuthParameters } = params;
|
|
839
|
-
const userPool = this.findUserPoolByClientId(ClientId);
|
|
840
|
-
|
|
841
|
-
if (!userPool) {
|
|
842
|
-
throw new Error(`Client ${ClientId} not found`);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// CUSTOM_AUTH flow — no password check, challenge-based
|
|
846
|
-
if (AuthFlow === "CUSTOM_AUTH") {
|
|
847
|
-
const username = AuthParameters.USERNAME;
|
|
848
|
-
const user = this.findUserByUsername(username, ClientId);
|
|
849
|
-
if (!user) {
|
|
850
|
-
throw new Error(`User not found: ${username}`);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// 1. PreAuthentication — dispara antes de qualquer challenge
|
|
854
|
-
const preAuthEvent = this._buildTriggerEvent("PreAuthentication_Authentication", userPool, user, ClientId, {
|
|
855
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
856
|
-
validationData: AuthParameters.ValidationData || {},
|
|
857
|
-
});
|
|
858
|
-
await this._invokeTrigger(userPool, "PreAuthentication", preAuthEvent);
|
|
859
|
-
|
|
860
|
-
// 2. DefineAuthChallenge — define qual challenge usar
|
|
861
|
-
const defineEvent = this._buildTriggerEvent("DefineAuthChallenge_Authentication", userPool, user, ClientId, {
|
|
862
|
-
session: [],
|
|
863
|
-
});
|
|
864
|
-
const defineResponse = await this._invokeTrigger(userPool, "DefineAuthChallenge", defineEvent);
|
|
865
|
-
|
|
866
|
-
if (defineResponse?.response?.challengeName === "CUSTOM_CHALLENGE") {
|
|
867
|
-
const createEvent = this._buildTriggerEvent("CreateAuthChallenge_Authentication", userPool, user, ClientId, {
|
|
868
|
-
challengeName: "CUSTOM_CHALLENGE",
|
|
869
|
-
session: [],
|
|
870
|
-
});
|
|
871
|
-
const createResponse = await this._invokeTrigger(userPool, "CreateAuthChallenge", createEvent);
|
|
872
|
-
|
|
873
|
-
const sessionToken = uuidv4();
|
|
874
|
-
this.customAuthSessions.set(sessionToken, {
|
|
875
|
-
sessionToken,
|
|
876
|
-
userId: user.UserId,
|
|
877
|
-
userPoolId: userPool.Id,
|
|
878
|
-
clientId: ClientId,
|
|
879
|
-
session: [],
|
|
880
|
-
privateChallengeParameters: createResponse?.response?.privateChallengeParameters || {},
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
return {
|
|
884
|
-
ChallengeName: "CUSTOM_CHALLENGE",
|
|
885
|
-
ChallengeParameters: createResponse?.response?.publicChallengeParameters || {},
|
|
886
|
-
Session: sessionToken,
|
|
887
|
-
AuthenticationResult: null,
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// DefineAuthChallenge did not return CUSTOM_CHALLENGE — nothing to do
|
|
892
|
-
return {
|
|
893
|
-
ChallengeName: null,
|
|
894
|
-
ChallengeParameters: {},
|
|
895
|
-
Session: null,
|
|
896
|
-
AuthenticationResult: null,
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const username = AuthParameters.USERNAME;
|
|
901
|
-
const password = AuthParameters.PASSWORD;
|
|
902
|
-
|
|
903
|
-
const user = this.findUserByUsername(username, ClientId);
|
|
904
|
-
if (!user) {
|
|
905
|
-
throw new Error(`User not found: ${username}`);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// FORCE_CHANGE_PASSWORD — validate temp password then return NEW_PASSWORD_REQUIRED challenge
|
|
909
|
-
if (user.UserStatus === "FORCE_CHANGE_PASSWORD") {
|
|
910
|
-
if (!this.verifyPassword(password, user.Password)) {
|
|
911
|
-
throw new Error("Incorrect username or password");
|
|
912
|
-
}
|
|
913
|
-
const sessionToken = uuidv4();
|
|
914
|
-
this.customAuthSessions.set(sessionToken, {
|
|
915
|
-
sessionToken,
|
|
916
|
-
userId: user.UserId,
|
|
917
|
-
userPoolId: userPool.Id,
|
|
918
|
-
clientId: ClientId,
|
|
919
|
-
challenge: "NEW_PASSWORD_REQUIRED",
|
|
920
|
-
});
|
|
921
|
-
return {
|
|
922
|
-
ChallengeName: "NEW_PASSWORD_REQUIRED",
|
|
923
|
-
ChallengeParameters: {
|
|
924
|
-
USER_ID_FOR_SRP: user.Username,
|
|
925
|
-
requiredAttributes: "[]",
|
|
926
|
-
userAttributes: JSON.stringify(this._triggerUserAttributes(user)),
|
|
927
|
-
},
|
|
928
|
-
Session: sessionToken,
|
|
929
|
-
AuthenticationResult: null,
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
if (user.UserStatus !== "CONFIRMED") {
|
|
934
|
-
// Valida senha antes de revelar o status — igual ao Cognito real
|
|
935
|
-
if (!this.verifyPassword(password, user.Password)) {
|
|
936
|
-
throw new Error("Incorrect username or password");
|
|
937
|
-
}
|
|
938
|
-
const err = new Error("User is not confirmed.");
|
|
939
|
-
err.code = "UserNotConfirmedException";
|
|
940
|
-
throw err;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// 1. PreAuthentication — dispara antes de validar senha
|
|
944
|
-
const preAuthEvent = this._buildTriggerEvent("PreAuthentication_Authentication", userPool, user, ClientId, {
|
|
945
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
946
|
-
validationData: AuthParameters.ValidationData || {},
|
|
947
|
-
});
|
|
948
|
-
await this._invokeTrigger(userPool, "PreAuthentication", preAuthEvent);
|
|
949
|
-
|
|
950
|
-
// 2. Valida senha
|
|
951
|
-
if (!this.verifyPassword(password, user.Password)) {
|
|
952
|
-
throw new Error("Incorrect username or password");
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// 3. PreTokenGeneration — dispara antes de gerar tokens, pode sobrescrever claims
|
|
956
|
-
const preTokenEvent = this._buildTriggerEvent("TokenGeneration_Authentication", userPool, user, ClientId, {
|
|
957
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
958
|
-
groupConfiguration: { groupsToOverride: [], iamRolesToOverride: [], preferredRole: null },
|
|
959
|
-
});
|
|
960
|
-
const preTokenResponse = await this._invokeTrigger(userPool, "PreTokenGeneration", preTokenEvent);
|
|
961
|
-
const claimsOverride = preTokenResponse?.response?.claimsOverrideDetails || null;
|
|
962
|
-
|
|
963
|
-
// 4. Gera tokens com claims override se houver
|
|
964
|
-
const accessToken = this.generateAccessToken(user, userPool, ClientId);
|
|
965
|
-
const idToken = this.generateIdToken(user, userPool, ClientId, claimsOverride);
|
|
966
|
-
const refreshToken = this.generateRefreshToken(user, userPool, ClientId);
|
|
967
|
-
|
|
968
|
-
const sessionId = uuidv4();
|
|
969
|
-
const session = {
|
|
970
|
-
Id: sessionId,
|
|
971
|
-
UserId: user.UserId,
|
|
972
|
-
UserPoolId: userPool.Id,
|
|
973
|
-
ClientId: ClientId,
|
|
974
|
-
AccessToken: accessToken,
|
|
975
|
-
IdToken: idToken,
|
|
976
|
-
RefreshToken: refreshToken,
|
|
977
|
-
CreatedAt: new Date().toISOString(),
|
|
978
|
-
ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
this.sessions.set(sessionId, session);
|
|
982
|
-
this.accessTokens.set(accessToken, session);
|
|
983
|
-
this.refreshTokens.set(refreshToken, session);
|
|
984
|
-
this.persistSessions();
|
|
985
|
-
|
|
986
|
-
// 5. PostAuthentication — dispara após auth bem-sucedida (não bloqueia)
|
|
987
|
-
const postAuthEvent = this._buildTriggerEvent("PostAuthentication_Authentication", userPool, user, ClientId, {
|
|
988
|
-
userAttributes: this._triggerUserAttributes(user),
|
|
989
|
-
newDeviceUsed: false,
|
|
990
|
-
});
|
|
991
|
-
try {
|
|
992
|
-
await this._invokeTrigger(userPool, "PostAuthentication", postAuthEvent);
|
|
993
|
-
} catch (err) {
|
|
994
|
-
logger.error(`PostAuthentication trigger error (ignored): ${err.message}`);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
logger.debug(`🔐 Usuário autenticado: ${username}`);
|
|
998
|
-
this.audit.record({
|
|
999
|
-
eventName: "InitiateAuth",
|
|
1000
|
-
readOnly: false,
|
|
1001
|
-
resources: [{ ARN: userPool.Arn, type: "AWS::Cognito::UserPool" }],
|
|
1002
|
-
requestParameters: { clientId: ClientId, authFlow: AuthFlow },
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
return {
|
|
1006
|
-
AuthenticationResult: {
|
|
1007
|
-
AccessToken: accessToken,
|
|
1008
|
-
IdToken: idToken,
|
|
1009
|
-
RefreshToken: refreshToken,
|
|
1010
|
-
TokenType: "Bearer",
|
|
1011
|
-
ExpiresIn: 3600,
|
|
1012
|
-
},
|
|
1013
|
-
ChallengeName: null,
|
|
1014
|
-
Session: null,
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
getToken(params) {
|
|
1019
|
-
const { AuthFlow, ClientId, AuthParameters } = params;
|
|
1020
|
-
|
|
1021
|
-
if (AuthFlow === "REFRESH_TOKEN_AUTH") {
|
|
1022
|
-
const refreshToken = AuthParameters.REFRESH_TOKEN;
|
|
1023
|
-
const session = this.refreshTokens.get(refreshToken);
|
|
1024
|
-
|
|
1025
|
-
if (!session) {
|
|
1026
|
-
throw new Error("Invalid refresh token");
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const user = this.users.get(session.UserId);
|
|
1030
|
-
const userPool = this.userPools.get(session.UserPoolId);
|
|
1031
|
-
|
|
1032
|
-
if (!user || !userPool) {
|
|
1033
|
-
throw new Error("Invalid session");
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Gera novos tokens
|
|
1037
|
-
const newAccessToken = this.generateAccessToken(user, userPool, session.ClientId);
|
|
1038
|
-
const newIdToken = this.generateIdToken(user, userPool, session.ClientId);
|
|
1039
|
-
|
|
1040
|
-
session.AccessToken = newAccessToken;
|
|
1041
|
-
session.IdToken = newIdToken;
|
|
1042
|
-
session.ExpiresAt = new Date(Date.now() + 3600000).toISOString();
|
|
1043
|
-
|
|
1044
|
-
this.accessTokens.set(newAccessToken, session);
|
|
1045
|
-
this.persistSessions();
|
|
1046
|
-
|
|
1047
|
-
return {
|
|
1048
|
-
AuthenticationResult: {
|
|
1049
|
-
AccessToken: newAccessToken,
|
|
1050
|
-
IdToken: newIdToken,
|
|
1051
|
-
TokenType: "Bearer",
|
|
1052
|
-
ExpiresIn: 3600,
|
|
1053
|
-
},
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
throw new Error(`Unsupported AuthFlow: ${AuthFlow}`);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// ============ Token Management ============
|
|
1061
|
-
|
|
1062
|
-
generateAccessToken(user, userPool, clientId) {
|
|
1063
|
-
const payload = {
|
|
1064
|
-
sub: user.UserId,
|
|
1065
|
-
token_use: "access",
|
|
1066
|
-
client_id: clientId,
|
|
1067
|
-
username: user.Username,
|
|
1068
|
-
scope: "aws.cognito.signin.user.admin",
|
|
1069
|
-
iss: `https://cognito-idp.local/${userPool.Id}`,
|
|
1070
|
-
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
1071
|
-
iat: Math.floor(Date.now() / 1000),
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
return jwt.sign(payload, this.jwtSecret, { algorithm: "HS256" });
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
generateIdToken(user, userPool, clientId, claimsOverride = null) {
|
|
1078
|
-
const payload = {
|
|
1079
|
-
sub: user.UserId,
|
|
1080
|
-
token_use: "id",
|
|
1081
|
-
client_id: clientId,
|
|
1082
|
-
email: user.Attributes.email,
|
|
1083
|
-
email_verified: user.Attributes.email_verified || true,
|
|
1084
|
-
username: user.Username,
|
|
1085
|
-
iss: `https://cognito-idp.local/${userPool.Id}`,
|
|
1086
|
-
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
1087
|
-
iat: Math.floor(Date.now() / 1000),
|
|
1088
|
-
};
|
|
1089
|
-
|
|
1090
|
-
// Adiciona outros atributos do usuário
|
|
1091
|
-
for (const [key, value] of Object.entries(user.Attributes)) {
|
|
1092
|
-
if (key !== "email" && key !== "email_verified") {
|
|
1093
|
-
payload[key] = value;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// Apply PreTokenGeneration claims override
|
|
1098
|
-
if (claimsOverride !== null) {
|
|
1099
|
-
if (claimsOverride.claimsToAddOrOverride) {
|
|
1100
|
-
Object.assign(payload, claimsOverride.claimsToAddOrOverride);
|
|
1101
|
-
}
|
|
1102
|
-
if (Array.isArray(claimsOverride.claimsToSuppress)) {
|
|
1103
|
-
for (const key of claimsOverride.claimsToSuppress) {
|
|
1104
|
-
delete payload[key];
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
return jwt.sign(payload, this.jwtSecret, { algorithm: "HS256" });
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
generateRefreshToken(user, userPool, clientId) {
|
|
1113
|
-
const payload = {
|
|
1114
|
-
sub: user.UserId,
|
|
1115
|
-
token_use: "refresh",
|
|
1116
|
-
client_id: clientId,
|
|
1117
|
-
username: user.Username,
|
|
1118
|
-
iss: `https://cognito-idp.local/${userPool.Id}`,
|
|
1119
|
-
exp: Math.floor(Date.now() / 1000) + 2592000, // 30 dias
|
|
1120
|
-
iat: Math.floor(Date.now() / 1000),
|
|
1121
|
-
};
|
|
1122
|
-
|
|
1123
|
-
return jwt.sign(payload, this.jwtSecret, { algorithm: "HS256" });
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
verifyAccessToken(token) {
|
|
1127
|
-
try {
|
|
1128
|
-
const decoded = jwt.verify(token, this.jwtSecret);
|
|
1129
|
-
const session = this.accessTokens.get(token);
|
|
1130
|
-
|
|
1131
|
-
if (!session || session.ExpiresAt < new Date().toISOString()) {
|
|
1132
|
-
return null;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
return decoded;
|
|
1136
|
-
} catch (error) {
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// ============ Admin Operations ============
|
|
1142
|
-
|
|
1143
|
-
adminGetUser(params) {
|
|
1144
|
-
const { UserPoolId, Username } = params;
|
|
1145
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
1146
|
-
|
|
1147
|
-
if (!userPool) {
|
|
1148
|
-
throw new Error(`User pool ${UserPoolId} not found`);
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
1152
|
-
if (!user) {
|
|
1153
|
-
throw new Error(`User not found: ${Username}`);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
return {
|
|
1157
|
-
Username: user.Username,
|
|
1158
|
-
UserAttributes: this.formatUserAttributes(user.Attributes),
|
|
1159
|
-
UserCreateDate: user.CreatedDate,
|
|
1160
|
-
UserLastModifiedDate: user.LastModifiedDate,
|
|
1161
|
-
Enabled: user.Enabled,
|
|
1162
|
-
UserStatus: user.UserStatus,
|
|
1163
|
-
MFAOptions: user.MfaOptions,
|
|
1164
|
-
PreferredMfaSetting: user.PreferredMfaSetting,
|
|
1165
|
-
UserMFASettingList: user.UserMFASettingList,
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
adminCreateUser(params) {
|
|
1170
|
-
const { UserPoolId, Username, UserAttributes, TemporaryPassword, DesiredDeliveryMediums } = params;
|
|
1171
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
1172
|
-
|
|
1173
|
-
if (!userPool) {
|
|
1174
|
-
throw new Error(`User pool ${UserPoolId} not found`);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
const tempPassword = TemporaryPassword || this._generateTemporaryPassword();
|
|
1178
|
-
|
|
1179
|
-
const userId = uuidv4();
|
|
1180
|
-
const user = {
|
|
1181
|
-
Username: Username,
|
|
1182
|
-
UserPoolId: UserPoolId,
|
|
1183
|
-
UserId: userId,
|
|
1184
|
-
Attributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
1185
|
-
Enabled: true,
|
|
1186
|
-
UserStatus: "FORCE_CHANGE_PASSWORD",
|
|
1187
|
-
CreatedDate: new Date().toISOString(),
|
|
1188
|
-
LastModifiedDate: new Date().toISOString(),
|
|
1189
|
-
Password: this.hashPassword(tempPassword),
|
|
1190
|
-
MfaOptions: [],
|
|
1191
|
-
PreferredMfaSetting: null,
|
|
1192
|
-
UserMFASettingList: [],
|
|
1193
|
-
};
|
|
1194
|
-
|
|
1195
|
-
// PreSignUp trigger — dispara antes de criar o usuário (admin context)
|
|
1196
|
-
const preSignUpEvent = this._buildTriggerEvent("PreSignUp_AdminCreateUser", userPool, user, "ADMIN", {
|
|
1197
|
-
userAttributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
1198
|
-
validationData: {},
|
|
1199
|
-
clientMetadata: {},
|
|
1200
|
-
});
|
|
1201
|
-
// Fire and forget — AdminCreateUser PreSignUp errors are non-blocking in local sim
|
|
1202
|
-
this._invokeTrigger(userPool, "PreSignUp", preSignUpEvent).catch((err) => {
|
|
1203
|
-
logger.warn(`PreSignUp trigger error on AdminCreateUser (ignored): ${err.message}`);
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
this.users.set(userId, user);
|
|
1207
|
-
userPool.Users.push(userId);
|
|
1208
|
-
userPool.EstimatedNumberOfUsers++;
|
|
1209
|
-
this.persistUsers();
|
|
1210
|
-
this.persistUserPools();
|
|
1211
|
-
|
|
1212
|
-
logger.info(`👤 Usuário criado: ${Username} | Senha temporária: ${tempPassword}`);
|
|
1213
|
-
|
|
1214
|
-
return {
|
|
1215
|
-
User: {
|
|
1216
|
-
Username: user.Username,
|
|
1217
|
-
UserAttributes: this.formatUserAttributes(user.Attributes),
|
|
1218
|
-
UserCreateDate: user.CreatedDate,
|
|
1219
|
-
UserLastModifiedDate: user.LastModifiedDate,
|
|
1220
|
-
Enabled: user.Enabled,
|
|
1221
|
-
UserStatus: user.UserStatus,
|
|
1222
|
-
TemporaryPassword: tempPassword,
|
|
1223
|
-
},
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
_generateTemporaryPassword() {
|
|
1228
|
-
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
1229
|
-
const lower = "abcdefghijklmnopqrstuvwxyz";
|
|
1230
|
-
const digits = "0123456789";
|
|
1231
|
-
const special = "!@#$%^&*";
|
|
1232
|
-
const all = upper + lower + digits + special;
|
|
1233
|
-
const rand = (set) => set[Math.floor(Math.random() * set.length)];
|
|
1234
|
-
const password = [rand(upper), rand(lower), rand(digits), rand(special)];
|
|
1235
|
-
for (let i = 4; i < 10; i++) password.push(rand(all));
|
|
1236
|
-
return password.sort(() => Math.random() - 0.5).join("");
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
adminSetUserPassword(params) {
|
|
1240
|
-
const { UserPoolId, Username, Password, Permanent } = params;
|
|
1241
|
-
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
1242
|
-
|
|
1243
|
-
if (!user) {
|
|
1244
|
-
throw new Error(`User not found: ${Username}`);
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
user.Password = this.hashPassword(Password);
|
|
1248
|
-
if (Permanent) {
|
|
1249
|
-
user.UserStatus = "CONFIRMED";
|
|
1250
|
-
}
|
|
1251
|
-
user.LastModifiedDate = new Date().toISOString();
|
|
1252
|
-
this.persistUsers();
|
|
1253
|
-
|
|
1254
|
-
return {};
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
adminDeleteUser(params) {
|
|
1258
|
-
const { UserPoolId, Username } = params;
|
|
1259
|
-
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
1260
|
-
|
|
1261
|
-
if (!user) {
|
|
1262
|
-
throw new Error(`User not found: ${Username}`);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
const userPool = this.userPools.get(UserPoolId);
|
|
1266
|
-
if (userPool) {
|
|
1267
|
-
const index = userPool.Users.indexOf(user.UserId);
|
|
1268
|
-
if (index !== -1) {
|
|
1269
|
-
userPool.Users.splice(index, 1);
|
|
1270
|
-
userPool.EstimatedNumberOfUsers--;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
this.users.delete(user.UserId);
|
|
1275
|
-
this.persistUsers();
|
|
1276
|
-
this.persistUserPools();
|
|
1277
|
-
|
|
1278
|
-
return {};
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
// ============ Helper Methods ============
|
|
1282
|
-
|
|
1283
|
-
findUserPoolByClientId(clientId) {
|
|
1284
|
-
for (const userPool of this.userPools.values()) {
|
|
1285
|
-
if (userPool.Clients.has(clientId)) {
|
|
1286
|
-
return userPool;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
return null;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
findUserByUsername(username, clientId = null, userPoolId = null) {
|
|
1293
|
-
let targetUserPoolId = userPoolId;
|
|
1294
|
-
|
|
1295
|
-
if (clientId && !targetUserPoolId) {
|
|
1296
|
-
const userPool = this.findUserPoolByClientId(clientId);
|
|
1297
|
-
if (userPool) {
|
|
1298
|
-
targetUserPoolId = userPool.Id;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
for (const user of this.users.values()) {
|
|
1303
|
-
if (user.UserPoolId !== targetUserPoolId) continue;
|
|
1304
|
-
// Match by Username or by email attribute (when UsernameAttributes includes 'email')
|
|
1305
|
-
if (user.Username === username) return user;
|
|
1306
|
-
if (user.Attributes?.email === username) return user;
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
return null;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
normalizeUserAttributes(attributes) {
|
|
1313
|
-
const normalized = {};
|
|
1314
|
-
for (const attr of attributes) {
|
|
1315
|
-
normalized[attr.Name] = attr.Value;
|
|
1316
|
-
}
|
|
1317
|
-
return normalized;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
formatUserAttributes(attributes) {
|
|
1321
|
-
return Object.entries(attributes).map(([Name, Value]) => ({ Name, Value }));
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
hashPassword(password) {
|
|
1325
|
-
// Simulação de hash (não usar em produção real)
|
|
1326
|
-
return crypto.createHash("sha256").update(password).digest("hex");
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
verifyPassword(password, hash) {
|
|
1330
|
-
return this.hashPassword(password) === hash;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
// ============ Identity Pool Operations ============
|
|
1334
|
-
|
|
1335
|
-
createIdentityPool(params) {
|
|
1336
|
-
const { IdentityPoolName, AllowUnauthenticatedIdentities, SupportedLoginProviders, CognitoIdentityProviders } = params;
|
|
1337
|
-
|
|
1338
|
-
const identityPoolId = `local:${IdentityPoolName}_${Date.now()}`;
|
|
1339
|
-
const identityPool = {
|
|
1340
|
-
IdentityPoolId: identityPoolId,
|
|
1341
|
-
IdentityPoolName: IdentityPoolName,
|
|
1342
|
-
AllowUnauthenticatedIdentities: AllowUnauthenticatedIdentities || false,
|
|
1343
|
-
SupportedLoginProviders: SupportedLoginProviders || {},
|
|
1344
|
-
CognitoIdentityProviders: CognitoIdentityProviders || [],
|
|
1345
|
-
Identities: new Map(),
|
|
1346
|
-
};
|
|
1347
|
-
|
|
1348
|
-
this.identityPools.set(identityPoolId, identityPool);
|
|
1349
|
-
this.persistIdentityPools();
|
|
1350
|
-
|
|
1351
|
-
logger.debug(`✅ Identity Pool criado: ${IdentityPoolName} (${identityPoolId})`);
|
|
1352
|
-
|
|
1353
|
-
return {
|
|
1354
|
-
IdentityPoolId: identityPoolId,
|
|
1355
|
-
IdentityPoolName: identityPoolName,
|
|
1356
|
-
AllowUnauthenticatedIdentities: identityPool.AllowUnauthenticatedIdentities,
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
getId(params) {
|
|
1361
|
-
const { IdentityPoolId, Logins } = params;
|
|
1362
|
-
const identityPool = this.identityPools.get(IdentityPoolId);
|
|
1363
|
-
|
|
1364
|
-
if (!identityPool) {
|
|
1365
|
-
throw new Error(`Identity pool ${IdentityPoolId} not found`);
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
let identityId = null;
|
|
1369
|
-
|
|
1370
|
-
if (Logins) {
|
|
1371
|
-
// Procura identidade existente com os logins fornecidos
|
|
1372
|
-
for (const [id, identity] of identityPool.Identities) {
|
|
1373
|
-
if (identity.Logins && this.matchesLogins(identity.Logins, Logins)) {
|
|
1374
|
-
identityId = id;
|
|
1375
|
-
break;
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (!identityId) {
|
|
1381
|
-
identityId = uuidv4();
|
|
1382
|
-
identityPool.Identities.set(identityId, {
|
|
1383
|
-
IdentityId: identityId,
|
|
1384
|
-
Logins: Logins || {},
|
|
1385
|
-
CreationDate: new Date().toISOString(),
|
|
1386
|
-
LastModifiedDate: new Date().toISOString(),
|
|
1387
|
-
});
|
|
1388
|
-
this.persistIdentityPools();
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
return {
|
|
1392
|
-
IdentityId: identityId,
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
getCredentialsForIdentity(params) {
|
|
1397
|
-
const { IdentityId, Logins } = params;
|
|
1398
|
-
const identityPool = this.findIdentityPoolByIdentityId(IdentityId);
|
|
1399
|
-
|
|
1400
|
-
if (!identityPool) {
|
|
1401
|
-
throw new Error(`Identity ${IdentityId} not found`);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
const identity = identityPool.Identities.get(IdentityId);
|
|
1405
|
-
if (!identity) {
|
|
1406
|
-
throw new Error(`Identity ${IdentityId} not found in pool`);
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// Gera credenciais temporárias (simuladas)
|
|
1410
|
-
const credentials = {
|
|
1411
|
-
AccessKeyId: `AKIA${crypto.randomBytes(16).toString("hex").toUpperCase()}`,
|
|
1412
|
-
SecretKey: crypto.randomBytes(32).toString("hex"),
|
|
1413
|
-
SessionToken: crypto.randomBytes(64).toString("base64"),
|
|
1414
|
-
Expiration: new Date(Date.now() + 3600000).toISOString(),
|
|
1415
|
-
};
|
|
1416
|
-
|
|
1417
|
-
return {
|
|
1418
|
-
Credentials: credentials,
|
|
1419
|
-
IdentityId: IdentityId,
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
findIdentityPoolByIdentityId(identityId) {
|
|
1424
|
-
for (const pool of this.identityPools.values()) {
|
|
1425
|
-
if (pool.Identities.has(identityId)) {
|
|
1426
|
-
return pool;
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
return null;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
matchesLogins(existingLogins, newLogins) {
|
|
1433
|
-
const existingKeys = Object.keys(existingLogins);
|
|
1434
|
-
const newKeys = Object.keys(newLogins);
|
|
1435
|
-
|
|
1436
|
-
if (existingKeys.length !== newKeys.length) return false;
|
|
1437
|
-
|
|
1438
|
-
for (const key of existingKeys) {
|
|
1439
|
-
if (existingLogins[key] !== newLogins[key]) {
|
|
1440
|
-
return false;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
return true;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// ============ Persistence ============
|
|
1448
|
-
|
|
1449
|
-
loadUserPools() {
|
|
1450
|
-
// Load persisted pools first
|
|
1451
|
-
const saved = this.store.read("__userpools__");
|
|
1452
|
-
if (saved) {
|
|
1453
|
-
for (const [id, data] of Object.entries(saved)) {
|
|
1454
|
-
data.Clients = new Map(Object.entries(data.Clients || {}));
|
|
1455
|
-
data.Groups = new Map(Object.entries(data.Groups || {}));
|
|
1456
|
-
data.IdentityProviders = new Map(Object.entries(data.IdentityProviders || {}));
|
|
1457
|
-
data.ResourceServers = new Map(Object.entries(data.ResourceServers || {}));
|
|
1458
|
-
this.userPools.set(id, data);
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// Create user pools from config if not already persisted
|
|
1463
|
-
if (this.config.cognito?.userPools) {
|
|
1464
|
-
let configChanged = false;
|
|
1465
|
-
const configPath = this.config._configPath;
|
|
1466
|
-
|
|
1467
|
-
for (let i = 0; i < this.config.cognito.userPools.length; i++) {
|
|
1468
|
-
const poolConfig = this.config.cognito.userPools[i];
|
|
1469
|
-
const existing = Array.from(this.userPools.values()).find((p) => p.Name === poolConfig.PoolName);
|
|
1470
|
-
|
|
1471
|
-
if (!existing) {
|
|
1472
|
-
const result = this.createUserPool(poolConfig);
|
|
1473
|
-
const poolId = result.UserPool.Id;
|
|
1474
|
-
logger.debug(`✅ User Pool criado a partir da config: ${poolConfig.PoolName} (${poolId})`);
|
|
1475
|
-
|
|
1476
|
-
// Copy LambdaTriggers from config onto the pool object
|
|
1477
|
-
const pool = this.userPools.get(poolId);
|
|
1478
|
-
pool.LambdaTriggers = poolConfig.LambdaTriggers || {};
|
|
1479
|
-
|
|
1480
|
-
// Auto-create a default client if not specified
|
|
1481
|
-
if (!poolConfig.ClientId) {
|
|
1482
|
-
const clientResult = this.createUserPoolClient({
|
|
1483
|
-
UserPoolId: poolId,
|
|
1484
|
-
ClientName: `${poolConfig.PoolName}-client`,
|
|
1485
|
-
GenerateSecret: false,
|
|
1486
|
-
});
|
|
1487
|
-
const clientId = clientResult.UserPoolClient.ClientId;
|
|
1488
|
-
this.config.cognito.userPools[i].ClientId = clientId;
|
|
1489
|
-
this.config.cognito.userPools[i].UserPoolId = poolId;
|
|
1490
|
-
configChanged = true;
|
|
1491
|
-
logger.debug(`✅ Client criado automaticamente: ${clientId}`);
|
|
1492
|
-
}
|
|
1493
|
-
} else {
|
|
1494
|
-
// Pool already exists — apply LambdaTriggers from config
|
|
1495
|
-
existing.LambdaTriggers = poolConfig.LambdaTriggers || {};
|
|
1496
|
-
|
|
1497
|
-
if (!poolConfig.ClientId) {
|
|
1498
|
-
// Pool exists but no clientId in config — write it back
|
|
1499
|
-
const firstClient = existing.Clients.size > 0 ? existing.Clients.values().next().value : null;
|
|
1500
|
-
if (firstClient) {
|
|
1501
|
-
this.config.cognito.userPools[i].ClientId = firstClient.ClientId;
|
|
1502
|
-
this.config.cognito.userPools[i].UserPoolId = existing.Id;
|
|
1503
|
-
configChanged = true;
|
|
1504
|
-
}
|
|
1505
|
-
} else if (!existing.Clients.has(poolConfig.ClientId)) {
|
|
1506
|
-
// ClientId is in config but not in the pool's Clients map — register it
|
|
1507
|
-
existing.Clients.set(poolConfig.ClientId, {
|
|
1508
|
-
ClientId: poolConfig.ClientId,
|
|
1509
|
-
ClientName: `${poolConfig.PoolName}-client`,
|
|
1510
|
-
ClientSecret: null,
|
|
1511
|
-
UserPoolId: existing.Id,
|
|
1512
|
-
RefreshTokenValidity: 30,
|
|
1513
|
-
AccessTokenValidity: 1,
|
|
1514
|
-
IdTokenValidity: 1,
|
|
1515
|
-
AllowedOAuthFlows: ['code'],
|
|
1516
|
-
AllowedOAuthScopes: ['openid', 'email', 'profile'],
|
|
1517
|
-
CallbackURLs: [],
|
|
1518
|
-
LogoutURLs: [],
|
|
1519
|
-
CreatedDate: new Date().toISOString(),
|
|
1520
|
-
LastModifiedDate: new Date().toISOString(),
|
|
1521
|
-
});
|
|
1522
|
-
this.persistUserPools();
|
|
1523
|
-
logger.debug(`✅ ClientId ${poolConfig.ClientId} registrado no pool ${existing.Id}`);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// Write clientId back to aws-local-simulator.json
|
|
1529
|
-
if (configChanged && configPath) {
|
|
1530
|
-
try {
|
|
1531
|
-
const fs = require("fs");
|
|
1532
|
-
const fileContent = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
1533
|
-
fileContent.cognito = fileContent.cognito || {};
|
|
1534
|
-
fileContent.cognito.userPools = this.config.cognito.userPools.map((p) => ({
|
|
1535
|
-
PoolName: p.PoolName,
|
|
1536
|
-
AutoVerifiedAttributes: p.AutoVerifiedAttributes,
|
|
1537
|
-
UserPoolId: p.UserPoolId,
|
|
1538
|
-
ClientId: p.ClientId,
|
|
1539
|
-
}));
|
|
1540
|
-
fs.writeFileSync(configPath, JSON.stringify(fileContent, null, 2));
|
|
1541
|
-
logger.info(`✅ ClientId gravado em: ${configPath}`);
|
|
1542
|
-
} catch (err) {
|
|
1543
|
-
logger.warn(`⚠️ Não foi possível gravar clientId no config: ${err.message}`);
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
loadIdentityPools() {
|
|
1550
|
-
const saved = this.store.read("__identitypools__");
|
|
1551
|
-
if (saved) {
|
|
1552
|
-
for (const [id, data] of Object.entries(saved)) {
|
|
1553
|
-
data.Identities = new Map(Object.entries(data.Identities || {}));
|
|
1554
|
-
this.identityPools.set(id, data);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
loadUsers() {
|
|
1560
|
-
const saved = this.store.read("__users__");
|
|
1561
|
-
if (saved) {
|
|
1562
|
-
for (const [id, user] of Object.entries(saved)) {
|
|
1563
|
-
this.users.set(id, user);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
loadSessions() {
|
|
1569
|
-
const saved = this.store.read("__sessions__");
|
|
1570
|
-
if (saved) {
|
|
1571
|
-
for (const [id, session] of Object.entries(saved)) {
|
|
1572
|
-
this.sessions.set(id, session);
|
|
1573
|
-
this.accessTokens.set(session.AccessToken, session);
|
|
1574
|
-
this.refreshTokens.set(session.RefreshToken, session);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
persistUserPools() {
|
|
1580
|
-
const poolsObj = {};
|
|
1581
|
-
for (const [id, pool] of this.userPools.entries()) {
|
|
1582
|
-
poolsObj[id] = {
|
|
1583
|
-
...pool,
|
|
1584
|
-
Clients: Object.fromEntries(pool.Clients),
|
|
1585
|
-
Groups: Object.fromEntries(pool.Groups),
|
|
1586
|
-
IdentityProviders: Object.fromEntries(pool.IdentityProviders),
|
|
1587
|
-
ResourceServers: Object.fromEntries(pool.ResourceServers),
|
|
1588
|
-
};
|
|
1589
|
-
}
|
|
1590
|
-
this.store.write("__userpools__", poolsObj);
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
persistIdentityPools() {
|
|
1594
|
-
const poolsObj = {};
|
|
1595
|
-
for (const [id, pool] of this.identityPools.entries()) {
|
|
1596
|
-
poolsObj[id] = {
|
|
1597
|
-
...pool,
|
|
1598
|
-
Identities: Object.fromEntries(pool.Identities),
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
this.store.write("__identitypools__", poolsObj);
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
persistUsers() {
|
|
1605
|
-
const usersObj = {};
|
|
1606
|
-
for (const [id, user] of this.users.entries()) {
|
|
1607
|
-
usersObj[id] = user;
|
|
1608
|
-
}
|
|
1609
|
-
this.store.write("__users__", usersObj);
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
persistSessions() {
|
|
1613
|
-
const sessionsObj = {};
|
|
1614
|
-
for (const [id, session] of this.sessions.entries()) {
|
|
1615
|
-
sessionsObj[id] = session;
|
|
1616
|
-
}
|
|
1617
|
-
this.store.write("__sessions__", sessionsObj);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
async reset() {
|
|
1621
|
-
this.userPools.clear();
|
|
1622
|
-
this.identityPools.clear();
|
|
1623
|
-
this.users.clear();
|
|
1624
|
-
this.sessions.clear();
|
|
1625
|
-
this.accessTokens.clear();
|
|
1626
|
-
this.refreshTokens.clear();
|
|
1627
|
-
|
|
1628
|
-
this.persistUserPools();
|
|
1629
|
-
this.persistIdentityPools();
|
|
1630
|
-
this.persistUsers();
|
|
1631
|
-
this.persistSessions();
|
|
1632
|
-
|
|
1633
|
-
logger.debug("Cognito: Todos os dados resetados");
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// ============ Stats ============
|
|
1637
|
-
|
|
1638
|
-
getUserPoolsCount() {
|
|
1639
|
-
return this.userPools.size;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
getTotalUsersCount() {
|
|
1643
|
-
return this.users.size;
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
getIdentityPoolsCount() {
|
|
1647
|
-
return this.identityPools.size;
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
getActiveSessionsCount() {
|
|
1651
|
-
return this.sessions.size;
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
module.exports = CognitoSimulator;
|
|
1
|
+
/**
|
|
2
|
+
* Cognito Simulator Core
|
|
3
|
+
* Simula User Pools, Identity Pools, Autenticação, Tokens JWT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const jwt = require("jsonwebtoken");
|
|
8
|
+
const { v4: uuidv4 } = require("uuid");
|
|
9
|
+
const logger = require("../../utils/logger");
|
|
10
|
+
const LocalStore = require("../../utils/local-store");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
13
|
+
|
|
14
|
+
class CognitoSimulator {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, "cognito");
|
|
18
|
+
this.store = new LocalStore(this.dataDir);
|
|
19
|
+
this.userPools = new Map();
|
|
20
|
+
this.identityPools = new Map();
|
|
21
|
+
this.users = new Map();
|
|
22
|
+
this.sessions = new Map();
|
|
23
|
+
this.refreshTokens = new Map();
|
|
24
|
+
this.accessTokens = new Map();
|
|
25
|
+
this.jwtSecret = crypto.randomBytes(64).toString("hex");
|
|
26
|
+
this.audit = new CloudTrailAudit("cognito-idp.amazonaws.com");
|
|
27
|
+
this.lambdaSimulator = null;
|
|
28
|
+
this.customAuthSessions = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setLambdaSimulator(lambdaSimulator) {
|
|
32
|
+
this.lambdaSimulator = lambdaSimulator;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_warnUnregisteredTriggers(pool) {
|
|
36
|
+
const triggers = pool.LambdaTriggers || {};
|
|
37
|
+
for (const [triggerName, fnName] of Object.entries(triggers)) {
|
|
38
|
+
if (fnName && !this.lambdaSimulator?.getLambda(fnName)) {
|
|
39
|
+
logger.warn(`⚠️ Cognito pool "${pool.Name}": trigger "${triggerName}" references Lambda "${fnName}" which is not registered`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_buildTriggerEvent(triggerSource, userPool, user, clientId, requestFields) {
|
|
45
|
+
return {
|
|
46
|
+
version: "1",
|
|
47
|
+
triggerSource,
|
|
48
|
+
region: "us-east-1",
|
|
49
|
+
userPoolId: userPool.Id,
|
|
50
|
+
userName: user.Username,
|
|
51
|
+
callerContext: {
|
|
52
|
+
awsSdkVersion: "aws-sdk-unknown-unknown",
|
|
53
|
+
clientId,
|
|
54
|
+
},
|
|
55
|
+
request: { ...requestFields },
|
|
56
|
+
response: {},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Returns userAttributes in the flat key/value format that real Cognito sends to triggers
|
|
61
|
+
_triggerUserAttributes(user) {
|
|
62
|
+
return {
|
|
63
|
+
sub: user.UserId,
|
|
64
|
+
"cognito:user_status": user.UserStatus,
|
|
65
|
+
...user.Attributes,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _invokeTrigger(userPool, triggerName, event) {
|
|
70
|
+
const fnName = userPool.LambdaTriggers?.[triggerName];
|
|
71
|
+
if (!fnName) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!this.lambdaSimulator || !this.lambdaSimulator.getLambda(fnName)) {
|
|
76
|
+
logger.warn(`⚠️ Cognito trigger "${triggerName}": Lambda "${fnName}" is not available, skipping`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = await this.lambdaSimulator.invoke(fnName, event, "RequestResponse");
|
|
81
|
+
return result.Payload;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async initialize() {
|
|
85
|
+
logger.debug("Inicializando Cognito Simulator...");
|
|
86
|
+
this.loadUserPools();
|
|
87
|
+
this.loadIdentityPools();
|
|
88
|
+
this.loadUsers();
|
|
89
|
+
this.loadSessions();
|
|
90
|
+
|
|
91
|
+
logger.debug(`✅ Cognito Simulator inicializado com ${this.userPools.size} user pools, ${this.identityPools.size} identity pools, ${this.users.size} usuários`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============ User Pool Operations ============
|
|
95
|
+
|
|
96
|
+
createUserPool(params) {
|
|
97
|
+
const { PoolName, Policies, LambdaConfig, AutoVerifiedAttributes, AliasAttributes, UsernameAttributes, MfaConfiguration, UserPoolId } = params;
|
|
98
|
+
|
|
99
|
+
const poolId = UserPoolId ? UserPoolId : `local_${PoolName}_${Date.now()}`;
|
|
100
|
+
const userPool = {
|
|
101
|
+
Id: poolId,
|
|
102
|
+
Name: PoolName,
|
|
103
|
+
Arn: `arn:aws:cognito:local:000000000000:userpool/${poolId}`,
|
|
104
|
+
Status: "ACTIVE",
|
|
105
|
+
CreationDate: new Date().toISOString(),
|
|
106
|
+
LastModifiedDate: new Date().toISOString(),
|
|
107
|
+
Policies: Policies || {
|
|
108
|
+
PasswordPolicy: {
|
|
109
|
+
MinimumLength: 8,
|
|
110
|
+
RequireUppercase: true,
|
|
111
|
+
RequireLowercase: true,
|
|
112
|
+
RequireNumbers: true,
|
|
113
|
+
RequireSymbols: false,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
LambdaConfig: LambdaConfig || {},
|
|
117
|
+
AutoVerifiedAttributes: AutoVerifiedAttributes || ["email"],
|
|
118
|
+
AliasAttributes: AliasAttributes || [],
|
|
119
|
+
UsernameAttributes: UsernameAttributes || ["email"],
|
|
120
|
+
MfaConfiguration: MfaConfiguration || "OFF",
|
|
121
|
+
EstimatedNumberOfUsers: 0,
|
|
122
|
+
Users: [],
|
|
123
|
+
Clients: new Map(),
|
|
124
|
+
Groups: new Map(),
|
|
125
|
+
IdentityProviders: new Map(),
|
|
126
|
+
ResourceServers: new Map(),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
this.userPools.set(poolId, userPool);
|
|
130
|
+
this.persistUserPools();
|
|
131
|
+
|
|
132
|
+
logger.debug(`✅ User Pool criado: ${PoolName} (${poolId})`);
|
|
133
|
+
this.audit.record({ eventName: "CreateUserPool", readOnly: false, resources: [{ ARN: userPool.Arn, type: "AWS::Cognito::UserPool" }], requestParameters: { poolName: PoolName } });
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
UserPool: {
|
|
137
|
+
Id: userPool.Id,
|
|
138
|
+
Name: userPool.Name,
|
|
139
|
+
Arn: userPool.Arn,
|
|
140
|
+
Status: userPool.Status,
|
|
141
|
+
CreationDate: userPool.CreationDate,
|
|
142
|
+
LastModifiedDate: userPool.LastModifiedDate,
|
|
143
|
+
MfaConfiguration: userPool.MfaConfiguration,
|
|
144
|
+
EstimatedNumberOfUsers: 0,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
listUserPools(params = {}) {
|
|
150
|
+
const { MaxResults = 60, NextToken } = params;
|
|
151
|
+
let userPools = Array.from(this.userPools.values());
|
|
152
|
+
|
|
153
|
+
if (NextToken) {
|
|
154
|
+
const startIndex = parseInt(NextToken);
|
|
155
|
+
userPools = userPools.slice(startIndex);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const results = userPools.slice(0, MaxResults);
|
|
159
|
+
const nextToken = results.length === MaxResults ? String(MaxResults) : null;
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
UserPools: results.map((pool) => ({
|
|
163
|
+
Id: pool.Id,
|
|
164
|
+
Name: pool.Name,
|
|
165
|
+
Arn: pool.Arn,
|
|
166
|
+
Status: pool.Status,
|
|
167
|
+
CreationDate: pool.CreationDate,
|
|
168
|
+
LastModifiedDate: pool.LastModifiedDate,
|
|
169
|
+
})),
|
|
170
|
+
NextToken: nextToken,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describeUserPool(params) {
|
|
175
|
+
const { UserPoolId } = params;
|
|
176
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
177
|
+
|
|
178
|
+
if (!userPool) {
|
|
179
|
+
throw new Error(`User pool ${UserPoolId} not found`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
UserPool: {
|
|
184
|
+
Id: userPool.Id,
|
|
185
|
+
Name: userPool.Name,
|
|
186
|
+
Arn: userPool.Arn,
|
|
187
|
+
Status: userPool.Status,
|
|
188
|
+
CreationDate: userPool.CreationDate,
|
|
189
|
+
LastModifiedDate: userPool.LastModifiedDate,
|
|
190
|
+
Policies: userPool.Policies,
|
|
191
|
+
LambdaConfig: userPool.LambdaConfig,
|
|
192
|
+
AutoVerifiedAttributes: userPool.AutoVerifiedAttributes,
|
|
193
|
+
AliasAttributes: userPool.AliasAttributes,
|
|
194
|
+
UsernameAttributes: userPool.UsernameAttributes,
|
|
195
|
+
MfaConfiguration: userPool.MfaConfiguration,
|
|
196
|
+
EstimatedNumberOfUsers: userPool.Users.length,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
listUsers(params = {}) {
|
|
202
|
+
const { UserPoolId, Filter, Limit = 60, PaginationToken } = params;
|
|
203
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
204
|
+
|
|
205
|
+
if (!userPool) {
|
|
206
|
+
throw new Error(`User pool ${UserPoolId} not found`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let users = Array.from(this.users.values()).filter((u) => u.UserPoolId === UserPoolId);
|
|
210
|
+
|
|
211
|
+
if (PaginationToken) {
|
|
212
|
+
const startIndex = parseInt(PaginationToken);
|
|
213
|
+
users = users.slice(startIndex);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const results = users.slice(0, Limit);
|
|
217
|
+
const nextToken = results.length === Limit && users.length > Limit ? String(Limit) : null;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
Users: results.map((u) => ({
|
|
221
|
+
Username: u.Username,
|
|
222
|
+
UserStatus: u.UserStatus,
|
|
223
|
+
Enabled: u.Enabled,
|
|
224
|
+
UserCreateDate: u.CreatedDate,
|
|
225
|
+
UserLastModifiedDate: u.LastModifiedDate,
|
|
226
|
+
Attributes: this.formatUserAttributes(u.Attributes),
|
|
227
|
+
})),
|
|
228
|
+
PaginationToken: nextToken,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
listUserPoolClients(params = {}) {
|
|
233
|
+
const { UserPoolId, MaxResults = 60, NextToken } = params;
|
|
234
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
235
|
+
if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
|
|
236
|
+
|
|
237
|
+
let clients = Array.from(userPool.Clients.values());
|
|
238
|
+
if (NextToken) clients = clients.slice(parseInt(NextToken));
|
|
239
|
+
const results = clients.slice(0, MaxResults);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
UserPoolClients: results.map((c) => ({
|
|
243
|
+
ClientId: c.ClientId,
|
|
244
|
+
ClientName: c.ClientName,
|
|
245
|
+
UserPoolId: c.UserPoolId,
|
|
246
|
+
})),
|
|
247
|
+
NextToken: results.length === MaxResults && clients.length > MaxResults ? String(MaxResults) : null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
describeUserPoolClient(params = {}) {
|
|
252
|
+
const { UserPoolId, ClientId } = params;
|
|
253
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
254
|
+
if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
|
|
255
|
+
const client = userPool.Clients.get(ClientId);
|
|
256
|
+
if (!client) throw new Error(`Client ${ClientId} not found`);
|
|
257
|
+
return { UserPoolClient: client };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
deleteUserPoolClient(params = {}) {
|
|
261
|
+
const { UserPoolId, ClientId } = params;
|
|
262
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
263
|
+
if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
|
|
264
|
+
userPool.Clients.delete(ClientId);
|
|
265
|
+
this.persistUserPools();
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
forgotPassword(params = {}) {
|
|
270
|
+
const { ClientId, Username } = params;
|
|
271
|
+
const userPool = this.findUserPoolByClientId(ClientId);
|
|
272
|
+
if (!userPool) throw new Error(`Client ${ClientId} not found`);
|
|
273
|
+
|
|
274
|
+
const user = this.findUserByUsername(Username, ClientId);
|
|
275
|
+
if (!user) throw new Error(`User not found: ${Username}`);
|
|
276
|
+
|
|
277
|
+
if (user.UserStatus !== "CONFIRMED") {
|
|
278
|
+
const err = new Error("Cannot reset password for the user as there is no registered/verified email or phone_number");
|
|
279
|
+
err.code = "InvalidParameterException";
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const resetCode = Math.floor(100000 + Math.random() * 900000).toString();
|
|
284
|
+
user.PasswordResetCode = resetCode;
|
|
285
|
+
this.persistUsers();
|
|
286
|
+
|
|
287
|
+
const userEmail = user.Attributes.email || Username;
|
|
288
|
+
logger.info(`📧 [COGNITO] Código de redefinição de senha para "${Username}": ${resetCode}`);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
CodeDeliveryDetails: {
|
|
292
|
+
Destination: userEmail,
|
|
293
|
+
DeliveryMedium: "EMAIL",
|
|
294
|
+
AttributeName: "email",
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
confirmForgotPassword(params = {}) {
|
|
300
|
+
const { ClientId, Username, ConfirmationCode, Password } = params;
|
|
301
|
+
const userPool = this.findUserPoolByClientId(ClientId);
|
|
302
|
+
if (!userPool) throw new Error(`Client ${ClientId} not found`);
|
|
303
|
+
|
|
304
|
+
const user = this.findUserByUsername(Username, ClientId);
|
|
305
|
+
if (!user) throw new Error(`User not found: ${Username}`);
|
|
306
|
+
|
|
307
|
+
if (user.UserStatus !== "CONFIRMED") {
|
|
308
|
+
const err = new Error("Cannot reset password for the user as there is no registered/verified email or phone_number");
|
|
309
|
+
err.code = "InvalidParameterException";
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (user.PasswordResetCode && user.PasswordResetCode !== ConfirmationCode) {
|
|
314
|
+
const err = new Error("Invalid verification code provided, please try again.");
|
|
315
|
+
err.code = "CodeMismatchException";
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
user.Password = this.hashPassword(Password);
|
|
320
|
+
delete user.PasswordResetCode;
|
|
321
|
+
this.persistUsers();
|
|
322
|
+
return {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
changePassword(params = {}) {
|
|
326
|
+
const { AccessToken, PreviousPassword, ProposedPassword } = params;
|
|
327
|
+
const session = this.accessTokens.get(AccessToken);
|
|
328
|
+
if (!session) throw new Error("Invalid access token");
|
|
329
|
+
const user = this.users.get(session.UserId);
|
|
330
|
+
if (!user) throw new Error("User not found");
|
|
331
|
+
if (!this.verifyPassword(PreviousPassword, user.Password)) throw new Error("Incorrect previous password");
|
|
332
|
+
user.Password = this.hashPassword(ProposedPassword);
|
|
333
|
+
this.persistUsers();
|
|
334
|
+
return {};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async respondToAuthChallenge(params = {}) {
|
|
338
|
+
const { ChallengeName } = params;
|
|
339
|
+
|
|
340
|
+
// NEW_PASSWORD_REQUIRED — user was created by admin and must set a permanent password
|
|
341
|
+
if (ChallengeName === "NEW_PASSWORD_REQUIRED") {
|
|
342
|
+
const session = this.customAuthSessions.get(params.Session);
|
|
343
|
+
if (!session || session.challenge !== "NEW_PASSWORD_REQUIRED") {
|
|
344
|
+
throw new Error("Invalid session token");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const newPassword = params.ChallengeResponses?.NEW_PASSWORD;
|
|
348
|
+
if (!newPassword) throw new Error("NEW_PASSWORD is required");
|
|
349
|
+
|
|
350
|
+
const user = this.users.get(session.userId);
|
|
351
|
+
const userPool = this.userPools.get(session.userPoolId);
|
|
352
|
+
if (!user || !userPool) throw new Error("Invalid session");
|
|
353
|
+
|
|
354
|
+
// 1. PreAuthentication — dispara antes de processar a nova senha
|
|
355
|
+
const preAuthEvent = this._buildTriggerEvent("PreAuthentication_Authentication", userPool, user, session.clientId, {
|
|
356
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
357
|
+
validationData: {},
|
|
358
|
+
});
|
|
359
|
+
await this._invokeTrigger(userPool, "PreAuthentication", preAuthEvent);
|
|
360
|
+
|
|
361
|
+
// 2. Aplica nova senha e confirma usuário
|
|
362
|
+
user.Password = this.hashPassword(newPassword);
|
|
363
|
+
user.UserStatus = "CONFIRMED";
|
|
364
|
+
user.LastModifiedDate = new Date().toISOString();
|
|
365
|
+
this.persistUsers();
|
|
366
|
+
this.customAuthSessions.delete(params.Session);
|
|
367
|
+
|
|
368
|
+
logger.debug(`🔑 Senha alterada e usuário confirmado: ${user.Username}`);
|
|
369
|
+
|
|
370
|
+
// 3. PostConfirmation — dispara após confirmação do usuário (troca de senha forçada)
|
|
371
|
+
const postConfirmEvent = this._buildTriggerEvent("PostConfirmation_ConfirmSignUp", userPool, user, session.clientId, {
|
|
372
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
373
|
+
});
|
|
374
|
+
try {
|
|
375
|
+
await this._invokeTrigger(userPool, "PostConfirmation", postConfirmEvent);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
logger.error(`PostConfirmation trigger error (ignored): ${err.message}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 4. PreTokenGeneration — antes de gerar tokens
|
|
381
|
+
const preTokenEvent = this._buildTriggerEvent("TokenGeneration_Authentication", userPool, user, session.clientId, {
|
|
382
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
383
|
+
groupConfiguration: { groupsToOverride: [], iamRolesToOverride: [], preferredRole: null },
|
|
384
|
+
});
|
|
385
|
+
const preTokenResponse = await this._invokeTrigger(userPool, "PreTokenGeneration", preTokenEvent);
|
|
386
|
+
const claimsOverride = preTokenResponse?.response?.claimsOverrideDetails || null;
|
|
387
|
+
|
|
388
|
+
// 5. Gera tokens
|
|
389
|
+
const accessToken = this.generateAccessToken(user, userPool, session.clientId);
|
|
390
|
+
const idToken = this.generateIdToken(user, userPool, session.clientId, claimsOverride);
|
|
391
|
+
const refreshToken = this.generateRefreshToken(user, userPool, session.clientId);
|
|
392
|
+
|
|
393
|
+
const sessionId = uuidv4();
|
|
394
|
+
const authSession = {
|
|
395
|
+
Id: sessionId,
|
|
396
|
+
UserId: user.UserId,
|
|
397
|
+
UserPoolId: userPool.Id,
|
|
398
|
+
ClientId: session.clientId,
|
|
399
|
+
AccessToken: accessToken,
|
|
400
|
+
IdToken: idToken,
|
|
401
|
+
RefreshToken: refreshToken,
|
|
402
|
+
CreatedAt: new Date().toISOString(),
|
|
403
|
+
ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
404
|
+
};
|
|
405
|
+
this.sessions.set(sessionId, authSession);
|
|
406
|
+
this.accessTokens.set(accessToken, authSession);
|
|
407
|
+
this.refreshTokens.set(refreshToken, authSession);
|
|
408
|
+
this.persistSessions();
|
|
409
|
+
|
|
410
|
+
// 6. PostAuthentication — após auth bem-sucedida (non-blocking)
|
|
411
|
+
const postAuthEvent = this._buildTriggerEvent("PostAuthentication_Authentication", userPool, user, session.clientId, {
|
|
412
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
413
|
+
newDeviceUsed: false,
|
|
414
|
+
});
|
|
415
|
+
try {
|
|
416
|
+
await this._invokeTrigger(userPool, "PostAuthentication", postAuthEvent);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
logger.error(`PostAuthentication trigger error (ignored): ${err.message}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
AuthenticationResult: {
|
|
423
|
+
AccessToken: accessToken,
|
|
424
|
+
IdToken: idToken,
|
|
425
|
+
RefreshToken: refreshToken,
|
|
426
|
+
TokenType: "Bearer",
|
|
427
|
+
ExpiresIn: 3600,
|
|
428
|
+
},
|
|
429
|
+
ChallengeName: null,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (ChallengeName === "CUSTOM_CHALLENGE") {
|
|
434
|
+
const session = this.customAuthSessions.get(params.Session);
|
|
435
|
+
if (!session) {
|
|
436
|
+
throw new Error("Invalid session token");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const user = this.users.get(session.userId);
|
|
440
|
+
const userPool = this.userPools.get(session.userPoolId);
|
|
441
|
+
|
|
442
|
+
// Invoke VerifyAuthChallengeResponse
|
|
443
|
+
const verifyEvent = this._buildTriggerEvent("VerifyAuthChallengeResponse_Authentication", userPool, user, session.clientId, {
|
|
444
|
+
challengeAnswer: params.ChallengeResponses?.ANSWER,
|
|
445
|
+
privateChallengeParameters: session.privateChallengeParameters,
|
|
446
|
+
});
|
|
447
|
+
const verifyResponse = await this._invokeTrigger(userPool, "VerifyAuthChallengeResponse", verifyEvent);
|
|
448
|
+
|
|
449
|
+
// Append challenge result to session
|
|
450
|
+
session.session.push({
|
|
451
|
+
challengeName: "CUSTOM_CHALLENGE",
|
|
452
|
+
challengeResult: verifyResponse?.response?.answerCorrect === true,
|
|
453
|
+
challengeMetadata: "",
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Invoke DefineAuthChallenge again with updated session
|
|
457
|
+
const defineEvent = this._buildTriggerEvent("DefineAuthChallenge_Authentication", userPool, user, session.clientId, {
|
|
458
|
+
session: session.session,
|
|
459
|
+
});
|
|
460
|
+
const defineResponse = await this._invokeTrigger(userPool, "DefineAuthChallenge", defineEvent);
|
|
461
|
+
|
|
462
|
+
if (defineResponse?.response?.issueTokens === true) {
|
|
463
|
+
// 1. PreTokenGeneration antes de gerar tokens
|
|
464
|
+
const preTokenEvent = this._buildTriggerEvent("TokenGeneration_Authentication", userPool, user, session.clientId, {
|
|
465
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
466
|
+
groupConfiguration: { groupsToOverride: [], iamRolesToOverride: [], preferredRole: null },
|
|
467
|
+
});
|
|
468
|
+
const preTokenResponse = await this._invokeTrigger(userPool, "PreTokenGeneration", preTokenEvent);
|
|
469
|
+
const claimsOverride = preTokenResponse?.response?.claimsOverrideDetails || null;
|
|
470
|
+
|
|
471
|
+
// 2. Gera tokens
|
|
472
|
+
const accessToken = this.generateAccessToken(user, userPool, session.clientId);
|
|
473
|
+
const idToken = this.generateIdToken(user, userPool, session.clientId, claimsOverride);
|
|
474
|
+
const refreshToken = this.generateRefreshToken(user, userPool, session.clientId);
|
|
475
|
+
|
|
476
|
+
const sessionId = uuidv4();
|
|
477
|
+
const authSession = {
|
|
478
|
+
Id: sessionId,
|
|
479
|
+
UserId: user.UserId,
|
|
480
|
+
UserPoolId: userPool.Id,
|
|
481
|
+
ClientId: session.clientId,
|
|
482
|
+
AccessToken: accessToken,
|
|
483
|
+
IdToken: idToken,
|
|
484
|
+
RefreshToken: refreshToken,
|
|
485
|
+
CreatedAt: new Date().toISOString(),
|
|
486
|
+
ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
this.sessions.set(sessionId, authSession);
|
|
490
|
+
this.accessTokens.set(accessToken, authSession);
|
|
491
|
+
this.refreshTokens.set(refreshToken, authSession);
|
|
492
|
+
this.persistSessions();
|
|
493
|
+
|
|
494
|
+
this.customAuthSessions.delete(params.Session);
|
|
495
|
+
|
|
496
|
+
// 3. PostAuthentication (non-blocking)
|
|
497
|
+
const postAuthEvent = this._buildTriggerEvent("PostAuthentication_Authentication", userPool, user, session.clientId, {
|
|
498
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
499
|
+
newDeviceUsed: false,
|
|
500
|
+
});
|
|
501
|
+
try {
|
|
502
|
+
await this._invokeTrigger(userPool, "PostAuthentication", postAuthEvent);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
logger.error(`PostAuthentication trigger error (ignored): ${err.message}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
AuthenticationResult: {
|
|
509
|
+
AccessToken: accessToken,
|
|
510
|
+
IdToken: idToken,
|
|
511
|
+
RefreshToken: refreshToken,
|
|
512
|
+
TokenType: "Bearer",
|
|
513
|
+
ExpiresIn: 3600,
|
|
514
|
+
},
|
|
515
|
+
ChallengeName: null,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Issue next challenge
|
|
520
|
+
const createEvent = this._buildTriggerEvent("CreateAuthChallenge_Authentication", userPool, user, session.clientId, {
|
|
521
|
+
challengeName: "CUSTOM_CHALLENGE",
|
|
522
|
+
session: session.session,
|
|
523
|
+
});
|
|
524
|
+
const createResponse = await this._invokeTrigger(userPool, "CreateAuthChallenge", createEvent);
|
|
525
|
+
|
|
526
|
+
// Update stored private challenge parameters for next round
|
|
527
|
+
session.privateChallengeParameters = createResponse?.response?.privateChallengeParameters || {};
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
ChallengeName: "CUSTOM_CHALLENGE",
|
|
531
|
+
ChallengeParameters: createResponse?.response?.publicChallengeParameters || {},
|
|
532
|
+
Session: params.Session,
|
|
533
|
+
AuthenticationResult: null,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return { AuthenticationResult: null, ChallengeName: null };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
revokeToken(params = {}) {
|
|
541
|
+
const { Token, ClientId } = params;
|
|
542
|
+
// Remove refresh token session if it exists
|
|
543
|
+
const session = this.refreshTokens.get(Token);
|
|
544
|
+
if (session) {
|
|
545
|
+
this.sessions.delete(session.Id);
|
|
546
|
+
this.accessTokens.delete(session.AccessToken);
|
|
547
|
+
this.refreshTokens.delete(Token);
|
|
548
|
+
this.persistSessions();
|
|
549
|
+
}
|
|
550
|
+
return {};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
globalSignOut(params = {}) {
|
|
554
|
+
const { AccessToken } = params;
|
|
555
|
+
const session = this.accessTokens.get(AccessToken);
|
|
556
|
+
if (session) {
|
|
557
|
+
this.sessions.delete(session.Id);
|
|
558
|
+
this.accessTokens.delete(AccessToken);
|
|
559
|
+
this.refreshTokens.delete(session.RefreshToken);
|
|
560
|
+
this.persistSessions();
|
|
561
|
+
}
|
|
562
|
+
return {};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
getUser(params = {}) {
|
|
566
|
+
const { AccessToken } = params;
|
|
567
|
+
const session = this.accessTokens.get(AccessToken);
|
|
568
|
+
if (!session) throw new Error("Invalid access token");
|
|
569
|
+
// Check session is still active
|
|
570
|
+
if (!this.sessions.has(session.Id)) throw new Error("Token has been revoked");
|
|
571
|
+
const user = this.users.get(session.UserId);
|
|
572
|
+
if (!user) throw new Error("User not found");
|
|
573
|
+
return {
|
|
574
|
+
Username: user.Username,
|
|
575
|
+
UserAttributes: this.formatUserAttributes(user.Attributes),
|
|
576
|
+
UserStatus: user.UserStatus,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
updateUserAttributes(params = {}) {
|
|
581
|
+
const { AccessToken, UserAttributes } = params;
|
|
582
|
+
const session = this.accessTokens.get(AccessToken);
|
|
583
|
+
if (!session) throw new Error("Invalid access token");
|
|
584
|
+
const user = this.users.get(session.UserId);
|
|
585
|
+
if (!user) throw new Error("User not found");
|
|
586
|
+
const updates = this.normalizeUserAttributes(UserAttributes || []);
|
|
587
|
+
Object.assign(user.Attributes, updates);
|
|
588
|
+
this.persistUsers();
|
|
589
|
+
return { CodeDeliveryDetailsList: [] };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
deleteUser(params = {}) {
|
|
593
|
+
const { AccessToken } = params;
|
|
594
|
+
const session = this.accessTokens.get(AccessToken);
|
|
595
|
+
if (!session) throw new Error("Invalid access token");
|
|
596
|
+
this.users.delete(session.UserId);
|
|
597
|
+
this.persistUsers();
|
|
598
|
+
return {};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
adminDisableUser(params = {}) {
|
|
602
|
+
const { UserPoolId, Username } = params;
|
|
603
|
+
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
604
|
+
if (!user) throw new Error(`User not found: ${Username}`);
|
|
605
|
+
user.Enabled = false;
|
|
606
|
+
this.persistUsers();
|
|
607
|
+
return {};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
adminEnableUser(params = {}) {
|
|
611
|
+
const { UserPoolId, Username } = params;
|
|
612
|
+
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
613
|
+
if (!user) throw new Error(`User not found: ${Username}`);
|
|
614
|
+
user.Enabled = true;
|
|
615
|
+
this.persistUsers();
|
|
616
|
+
return {};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
adminResetUserPassword(params = {}) {
|
|
620
|
+
const { UserPoolId, Username } = params;
|
|
621
|
+
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
622
|
+
if (!user) throw new Error(`User not found: ${Username}`);
|
|
623
|
+
user.UserStatus = "RESET_REQUIRED";
|
|
624
|
+
this.persistUsers();
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
adminUserGlobalSignOut(params = {}) {
|
|
629
|
+
const { UserPoolId, Username } = params;
|
|
630
|
+
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
631
|
+
if (!user) throw new Error(`User not found: ${Username}`);
|
|
632
|
+
// Invalidate all sessions for this user
|
|
633
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
634
|
+
if (session.UserId === user.UserId) {
|
|
635
|
+
this.accessTokens.delete(session.AccessToken);
|
|
636
|
+
this.refreshTokens.delete(session.RefreshToken);
|
|
637
|
+
this.sessions.delete(id);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
this.persistSessions();
|
|
641
|
+
return {};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
adminListGroupsForUser(params = {}) {
|
|
645
|
+
return { Groups: [], NextToken: null };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
deleteUserPool(params) {
|
|
649
|
+
const { UserPoolId } = params;
|
|
650
|
+
|
|
651
|
+
if (!this.userPools.has(UserPoolId)) {
|
|
652
|
+
throw new Error(`User pool ${UserPoolId} not found`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
this.userPools.delete(UserPoolId);
|
|
656
|
+
this.persistUserPools();
|
|
657
|
+
|
|
658
|
+
return {};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============ User Pool Client Operations ============
|
|
662
|
+
|
|
663
|
+
createUserPoolClient(params) {
|
|
664
|
+
const { UserPoolId, ClientName, GenerateSecret, RefreshTokenValidity, AccessTokenValidity, IdTokenValidity, AllowedOAuthFlows, AllowedOAuthScopes, CallbackURLs, LogoutURLs } = params;
|
|
665
|
+
|
|
666
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
667
|
+
if (!userPool) {
|
|
668
|
+
throw new Error(`User pool ${UserPoolId} not found`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const clientId = crypto.randomBytes(20).toString("hex");
|
|
672
|
+
const clientSecret = GenerateSecret ? crypto.randomBytes(32).toString("hex") : null;
|
|
673
|
+
|
|
674
|
+
const client = {
|
|
675
|
+
ClientId: clientId,
|
|
676
|
+
ClientName: ClientName,
|
|
677
|
+
ClientSecret: clientSecret,
|
|
678
|
+
UserPoolId: UserPoolId,
|
|
679
|
+
RefreshTokenValidity: RefreshTokenValidity || 30,
|
|
680
|
+
AccessTokenValidity: AccessTokenValidity || 1,
|
|
681
|
+
IdTokenValidity: IdTokenValidity || 1,
|
|
682
|
+
AllowedOAuthFlows: AllowedOAuthFlows || ["code"],
|
|
683
|
+
AllowedOAuthScopes: AllowedOAuthScopes || ["openid", "email", "profile"],
|
|
684
|
+
CallbackURLs: CallbackURLs || [],
|
|
685
|
+
LogoutURLs: LogoutURLs || [],
|
|
686
|
+
CreatedDate: new Date().toISOString(),
|
|
687
|
+
LastModifiedDate: new Date().toISOString(),
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
userPool.Clients.set(clientId, client);
|
|
691
|
+
this.persistUserPools();
|
|
692
|
+
|
|
693
|
+
logger.debug(`✅ User Pool Client criado: ${ClientName} (${clientId})`);
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
UserPoolClient: {
|
|
697
|
+
ClientId: client.ClientId,
|
|
698
|
+
ClientName: client.ClientName,
|
|
699
|
+
ClientSecret: client.ClientSecret,
|
|
700
|
+
UserPoolId: client.UserPoolId,
|
|
701
|
+
RefreshTokenValidity: client.RefreshTokenValidity,
|
|
702
|
+
AccessTokenValidity: client.AccessTokenValidity,
|
|
703
|
+
IdTokenValidity: client.IdTokenValidity,
|
|
704
|
+
AllowedOAuthFlows: client.AllowedOAuthFlows,
|
|
705
|
+
AllowedOAuthScopes: client.AllowedOAuthScopes,
|
|
706
|
+
CallbackURLs: client.CallbackURLs,
|
|
707
|
+
LogoutURLs: client.LogoutURLs,
|
|
708
|
+
CreationDate: client.CreatedDate,
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ============ User Operations ============
|
|
714
|
+
|
|
715
|
+
async signUp(params = {}) {
|
|
716
|
+
const { ClientId, Username, Password, UserAttributes, ValidationData } = params;
|
|
717
|
+
|
|
718
|
+
// Encontra o user pool pelo client id
|
|
719
|
+
const userPool = this.findUserPoolByClientId(ClientId);
|
|
720
|
+
if (!userPool) {
|
|
721
|
+
throw new Error(`Client ${ClientId} not found`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Verifica se usuário já existe
|
|
725
|
+
const existingUser = Array.from(this.users.values()).find((u) => u.Username === Username && u.UserPoolId === userPool.Id);
|
|
726
|
+
|
|
727
|
+
if (existingUser) {
|
|
728
|
+
throw new Error(`User already exists: ${Username}`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const userId = uuidv4();
|
|
732
|
+
const confirmationCode = Math.floor(100000 + Math.random() * 900000).toString();
|
|
733
|
+
|
|
734
|
+
const user = {
|
|
735
|
+
Username: Username,
|
|
736
|
+
UserPoolId: userPool.Id,
|
|
737
|
+
UserId: userId,
|
|
738
|
+
Attributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
739
|
+
Enabled: true,
|
|
740
|
+
UserStatus: "UNCONFIRMED",
|
|
741
|
+
CreatedDate: new Date().toISOString(),
|
|
742
|
+
LastModifiedDate: new Date().toISOString(),
|
|
743
|
+
Password: this.hashPassword(Password),
|
|
744
|
+
ConfirmationCode: confirmationCode,
|
|
745
|
+
MfaOptions: [],
|
|
746
|
+
PreferredMfaSetting: null,
|
|
747
|
+
UserMFASettingList: [],
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// Invoke PreSignUp trigger before persisting the user
|
|
751
|
+
const event = this._buildTriggerEvent("PreSignUp_SignUp", userPool, user, ClientId, {
|
|
752
|
+
userAttributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
753
|
+
validationData: ValidationData || {},
|
|
754
|
+
clientMetadata: {},
|
|
755
|
+
});
|
|
756
|
+
const triggerResponse = await this._invokeTrigger(userPool, "PreSignUp", event);
|
|
757
|
+
|
|
758
|
+
if (triggerResponse !== null) {
|
|
759
|
+
if (triggerResponse.response?.autoConfirmUser === true) {
|
|
760
|
+
user.UserStatus = "CONFIRMED";
|
|
761
|
+
delete user.ConfirmationCode;
|
|
762
|
+
}
|
|
763
|
+
if (triggerResponse.response?.autoVerifyEmail === true) {
|
|
764
|
+
user.Attributes.email_verified = "true";
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
this.users.set(userId, user);
|
|
769
|
+
userPool.Users.push(userId);
|
|
770
|
+
userPool.EstimatedNumberOfUsers++;
|
|
771
|
+
this.persistUsers();
|
|
772
|
+
this.persistUserPools();
|
|
773
|
+
|
|
774
|
+
const userEmail = user.Attributes.email || Username;
|
|
775
|
+
|
|
776
|
+
if (user.UserStatus === "UNCONFIRMED") {
|
|
777
|
+
logger.info(`📧 [COGNITO] Código de confirmação para "${Username}": ${confirmationCode}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
logger.debug(`✅ Usuário criado: ${Username} (${userId}) — status: ${user.UserStatus}`);
|
|
781
|
+
|
|
782
|
+
// PostConfirmation — dispara se usuário foi auto-confirmado pelo PreSignUp
|
|
783
|
+
if (user.UserStatus === "CONFIRMED") {
|
|
784
|
+
const postConfirmEvent = this._buildTriggerEvent("PostConfirmation_ConfirmSignUp", userPool, user, ClientId, {
|
|
785
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
786
|
+
});
|
|
787
|
+
try {
|
|
788
|
+
await this._invokeTrigger(userPool, "PostConfirmation", postConfirmEvent);
|
|
789
|
+
} catch (err) {
|
|
790
|
+
logger.error(`PostConfirmation trigger error (ignored): ${err.message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return {
|
|
795
|
+
UserConfirmed: user.UserStatus === "CONFIRMED",
|
|
796
|
+
UserSub: userId,
|
|
797
|
+
CodeDeliveryDetails: user.UserStatus === "UNCONFIRMED"
|
|
798
|
+
? { Destination: userEmail, DeliveryMedium: "EMAIL", AttributeName: "email" }
|
|
799
|
+
: null,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async confirmSignUp(params) {
|
|
804
|
+
const { ClientId, Username, ConfirmationCode } = params;
|
|
805
|
+
|
|
806
|
+
const userPool = this.findUserPoolByClientId(ClientId);
|
|
807
|
+
if (!userPool) {
|
|
808
|
+
throw new Error(`Client ${ClientId} not found`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const user = this.findUserByUsername(Username, ClientId);
|
|
812
|
+
if (!user) {
|
|
813
|
+
throw new Error(`User not found: ${Username}`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (user.ConfirmationCode && user.ConfirmationCode !== ConfirmationCode) {
|
|
817
|
+
throw new Error("Invalid verification code provided, please try again.");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
user.UserStatus = "CONFIRMED";
|
|
821
|
+
user.LastModifiedDate = new Date().toISOString();
|
|
822
|
+
delete user.ConfirmationCode;
|
|
823
|
+
this.persistUsers();
|
|
824
|
+
|
|
825
|
+
const event = this._buildTriggerEvent("PostConfirmation_ConfirmSignUp", userPool, user, ClientId, {
|
|
826
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
827
|
+
});
|
|
828
|
+
try {
|
|
829
|
+
await this._invokeTrigger(userPool, "PostConfirmation", event);
|
|
830
|
+
} catch (err) {
|
|
831
|
+
logger.error(`PostConfirmation trigger error (ignored): ${err.message}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return {};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async initiateAuth(params) {
|
|
838
|
+
const { AuthFlow, ClientId, AuthParameters } = params;
|
|
839
|
+
const userPool = this.findUserPoolByClientId(ClientId);
|
|
840
|
+
|
|
841
|
+
if (!userPool) {
|
|
842
|
+
throw new Error(`Client ${ClientId} not found`);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// CUSTOM_AUTH flow — no password check, challenge-based
|
|
846
|
+
if (AuthFlow === "CUSTOM_AUTH") {
|
|
847
|
+
const username = AuthParameters.USERNAME;
|
|
848
|
+
const user = this.findUserByUsername(username, ClientId);
|
|
849
|
+
if (!user) {
|
|
850
|
+
throw new Error(`User not found: ${username}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// 1. PreAuthentication — dispara antes de qualquer challenge
|
|
854
|
+
const preAuthEvent = this._buildTriggerEvent("PreAuthentication_Authentication", userPool, user, ClientId, {
|
|
855
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
856
|
+
validationData: AuthParameters.ValidationData || {},
|
|
857
|
+
});
|
|
858
|
+
await this._invokeTrigger(userPool, "PreAuthentication", preAuthEvent);
|
|
859
|
+
|
|
860
|
+
// 2. DefineAuthChallenge — define qual challenge usar
|
|
861
|
+
const defineEvent = this._buildTriggerEvent("DefineAuthChallenge_Authentication", userPool, user, ClientId, {
|
|
862
|
+
session: [],
|
|
863
|
+
});
|
|
864
|
+
const defineResponse = await this._invokeTrigger(userPool, "DefineAuthChallenge", defineEvent);
|
|
865
|
+
|
|
866
|
+
if (defineResponse?.response?.challengeName === "CUSTOM_CHALLENGE") {
|
|
867
|
+
const createEvent = this._buildTriggerEvent("CreateAuthChallenge_Authentication", userPool, user, ClientId, {
|
|
868
|
+
challengeName: "CUSTOM_CHALLENGE",
|
|
869
|
+
session: [],
|
|
870
|
+
});
|
|
871
|
+
const createResponse = await this._invokeTrigger(userPool, "CreateAuthChallenge", createEvent);
|
|
872
|
+
|
|
873
|
+
const sessionToken = uuidv4();
|
|
874
|
+
this.customAuthSessions.set(sessionToken, {
|
|
875
|
+
sessionToken,
|
|
876
|
+
userId: user.UserId,
|
|
877
|
+
userPoolId: userPool.Id,
|
|
878
|
+
clientId: ClientId,
|
|
879
|
+
session: [],
|
|
880
|
+
privateChallengeParameters: createResponse?.response?.privateChallengeParameters || {},
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
ChallengeName: "CUSTOM_CHALLENGE",
|
|
885
|
+
ChallengeParameters: createResponse?.response?.publicChallengeParameters || {},
|
|
886
|
+
Session: sessionToken,
|
|
887
|
+
AuthenticationResult: null,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// DefineAuthChallenge did not return CUSTOM_CHALLENGE — nothing to do
|
|
892
|
+
return {
|
|
893
|
+
ChallengeName: null,
|
|
894
|
+
ChallengeParameters: {},
|
|
895
|
+
Session: null,
|
|
896
|
+
AuthenticationResult: null,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const username = AuthParameters.USERNAME;
|
|
901
|
+
const password = AuthParameters.PASSWORD;
|
|
902
|
+
|
|
903
|
+
const user = this.findUserByUsername(username, ClientId);
|
|
904
|
+
if (!user) {
|
|
905
|
+
throw new Error(`User not found: ${username}`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// FORCE_CHANGE_PASSWORD — validate temp password then return NEW_PASSWORD_REQUIRED challenge
|
|
909
|
+
if (user.UserStatus === "FORCE_CHANGE_PASSWORD") {
|
|
910
|
+
if (!this.verifyPassword(password, user.Password)) {
|
|
911
|
+
throw new Error("Incorrect username or password");
|
|
912
|
+
}
|
|
913
|
+
const sessionToken = uuidv4();
|
|
914
|
+
this.customAuthSessions.set(sessionToken, {
|
|
915
|
+
sessionToken,
|
|
916
|
+
userId: user.UserId,
|
|
917
|
+
userPoolId: userPool.Id,
|
|
918
|
+
clientId: ClientId,
|
|
919
|
+
challenge: "NEW_PASSWORD_REQUIRED",
|
|
920
|
+
});
|
|
921
|
+
return {
|
|
922
|
+
ChallengeName: "NEW_PASSWORD_REQUIRED",
|
|
923
|
+
ChallengeParameters: {
|
|
924
|
+
USER_ID_FOR_SRP: user.Username,
|
|
925
|
+
requiredAttributes: "[]",
|
|
926
|
+
userAttributes: JSON.stringify(this._triggerUserAttributes(user)),
|
|
927
|
+
},
|
|
928
|
+
Session: sessionToken,
|
|
929
|
+
AuthenticationResult: null,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (user.UserStatus !== "CONFIRMED") {
|
|
934
|
+
// Valida senha antes de revelar o status — igual ao Cognito real
|
|
935
|
+
if (!this.verifyPassword(password, user.Password)) {
|
|
936
|
+
throw new Error("Incorrect username or password");
|
|
937
|
+
}
|
|
938
|
+
const err = new Error("User is not confirmed.");
|
|
939
|
+
err.code = "UserNotConfirmedException";
|
|
940
|
+
throw err;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// 1. PreAuthentication — dispara antes de validar senha
|
|
944
|
+
const preAuthEvent = this._buildTriggerEvent("PreAuthentication_Authentication", userPool, user, ClientId, {
|
|
945
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
946
|
+
validationData: AuthParameters.ValidationData || {},
|
|
947
|
+
});
|
|
948
|
+
await this._invokeTrigger(userPool, "PreAuthentication", preAuthEvent);
|
|
949
|
+
|
|
950
|
+
// 2. Valida senha
|
|
951
|
+
if (!this.verifyPassword(password, user.Password)) {
|
|
952
|
+
throw new Error("Incorrect username or password");
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// 3. PreTokenGeneration — dispara antes de gerar tokens, pode sobrescrever claims
|
|
956
|
+
const preTokenEvent = this._buildTriggerEvent("TokenGeneration_Authentication", userPool, user, ClientId, {
|
|
957
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
958
|
+
groupConfiguration: { groupsToOverride: [], iamRolesToOverride: [], preferredRole: null },
|
|
959
|
+
});
|
|
960
|
+
const preTokenResponse = await this._invokeTrigger(userPool, "PreTokenGeneration", preTokenEvent);
|
|
961
|
+
const claimsOverride = preTokenResponse?.response?.claimsOverrideDetails || null;
|
|
962
|
+
|
|
963
|
+
// 4. Gera tokens com claims override se houver
|
|
964
|
+
const accessToken = this.generateAccessToken(user, userPool, ClientId);
|
|
965
|
+
const idToken = this.generateIdToken(user, userPool, ClientId, claimsOverride);
|
|
966
|
+
const refreshToken = this.generateRefreshToken(user, userPool, ClientId);
|
|
967
|
+
|
|
968
|
+
const sessionId = uuidv4();
|
|
969
|
+
const session = {
|
|
970
|
+
Id: sessionId,
|
|
971
|
+
UserId: user.UserId,
|
|
972
|
+
UserPoolId: userPool.Id,
|
|
973
|
+
ClientId: ClientId,
|
|
974
|
+
AccessToken: accessToken,
|
|
975
|
+
IdToken: idToken,
|
|
976
|
+
RefreshToken: refreshToken,
|
|
977
|
+
CreatedAt: new Date().toISOString(),
|
|
978
|
+
ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
this.sessions.set(sessionId, session);
|
|
982
|
+
this.accessTokens.set(accessToken, session);
|
|
983
|
+
this.refreshTokens.set(refreshToken, session);
|
|
984
|
+
this.persistSessions();
|
|
985
|
+
|
|
986
|
+
// 5. PostAuthentication — dispara após auth bem-sucedida (não bloqueia)
|
|
987
|
+
const postAuthEvent = this._buildTriggerEvent("PostAuthentication_Authentication", userPool, user, ClientId, {
|
|
988
|
+
userAttributes: this._triggerUserAttributes(user),
|
|
989
|
+
newDeviceUsed: false,
|
|
990
|
+
});
|
|
991
|
+
try {
|
|
992
|
+
await this._invokeTrigger(userPool, "PostAuthentication", postAuthEvent);
|
|
993
|
+
} catch (err) {
|
|
994
|
+
logger.error(`PostAuthentication trigger error (ignored): ${err.message}`);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
logger.debug(`🔐 Usuário autenticado: ${username}`);
|
|
998
|
+
this.audit.record({
|
|
999
|
+
eventName: "InitiateAuth",
|
|
1000
|
+
readOnly: false,
|
|
1001
|
+
resources: [{ ARN: userPool.Arn, type: "AWS::Cognito::UserPool" }],
|
|
1002
|
+
requestParameters: { clientId: ClientId, authFlow: AuthFlow },
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
return {
|
|
1006
|
+
AuthenticationResult: {
|
|
1007
|
+
AccessToken: accessToken,
|
|
1008
|
+
IdToken: idToken,
|
|
1009
|
+
RefreshToken: refreshToken,
|
|
1010
|
+
TokenType: "Bearer",
|
|
1011
|
+
ExpiresIn: 3600,
|
|
1012
|
+
},
|
|
1013
|
+
ChallengeName: null,
|
|
1014
|
+
Session: null,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
getToken(params) {
|
|
1019
|
+
const { AuthFlow, ClientId, AuthParameters } = params;
|
|
1020
|
+
|
|
1021
|
+
if (AuthFlow === "REFRESH_TOKEN_AUTH") {
|
|
1022
|
+
const refreshToken = AuthParameters.REFRESH_TOKEN;
|
|
1023
|
+
const session = this.refreshTokens.get(refreshToken);
|
|
1024
|
+
|
|
1025
|
+
if (!session) {
|
|
1026
|
+
throw new Error("Invalid refresh token");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const user = this.users.get(session.UserId);
|
|
1030
|
+
const userPool = this.userPools.get(session.UserPoolId);
|
|
1031
|
+
|
|
1032
|
+
if (!user || !userPool) {
|
|
1033
|
+
throw new Error("Invalid session");
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Gera novos tokens
|
|
1037
|
+
const newAccessToken = this.generateAccessToken(user, userPool, session.ClientId);
|
|
1038
|
+
const newIdToken = this.generateIdToken(user, userPool, session.ClientId);
|
|
1039
|
+
|
|
1040
|
+
session.AccessToken = newAccessToken;
|
|
1041
|
+
session.IdToken = newIdToken;
|
|
1042
|
+
session.ExpiresAt = new Date(Date.now() + 3600000).toISOString();
|
|
1043
|
+
|
|
1044
|
+
this.accessTokens.set(newAccessToken, session);
|
|
1045
|
+
this.persistSessions();
|
|
1046
|
+
|
|
1047
|
+
return {
|
|
1048
|
+
AuthenticationResult: {
|
|
1049
|
+
AccessToken: newAccessToken,
|
|
1050
|
+
IdToken: newIdToken,
|
|
1051
|
+
TokenType: "Bearer",
|
|
1052
|
+
ExpiresIn: 3600,
|
|
1053
|
+
},
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
throw new Error(`Unsupported AuthFlow: ${AuthFlow}`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ============ Token Management ============
|
|
1061
|
+
|
|
1062
|
+
generateAccessToken(user, userPool, clientId) {
|
|
1063
|
+
const payload = {
|
|
1064
|
+
sub: user.UserId,
|
|
1065
|
+
token_use: "access",
|
|
1066
|
+
client_id: clientId,
|
|
1067
|
+
username: user.Username,
|
|
1068
|
+
scope: "aws.cognito.signin.user.admin",
|
|
1069
|
+
iss: `https://cognito-idp.local/${userPool.Id}`,
|
|
1070
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
1071
|
+
iat: Math.floor(Date.now() / 1000),
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
return jwt.sign(payload, this.jwtSecret, { algorithm: "HS256" });
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
generateIdToken(user, userPool, clientId, claimsOverride = null) {
|
|
1078
|
+
const payload = {
|
|
1079
|
+
sub: user.UserId,
|
|
1080
|
+
token_use: "id",
|
|
1081
|
+
client_id: clientId,
|
|
1082
|
+
email: user.Attributes.email,
|
|
1083
|
+
email_verified: user.Attributes.email_verified || true,
|
|
1084
|
+
username: user.Username,
|
|
1085
|
+
iss: `https://cognito-idp.local/${userPool.Id}`,
|
|
1086
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
1087
|
+
iat: Math.floor(Date.now() / 1000),
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// Adiciona outros atributos do usuário
|
|
1091
|
+
for (const [key, value] of Object.entries(user.Attributes)) {
|
|
1092
|
+
if (key !== "email" && key !== "email_verified") {
|
|
1093
|
+
payload[key] = value;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Apply PreTokenGeneration claims override
|
|
1098
|
+
if (claimsOverride !== null) {
|
|
1099
|
+
if (claimsOverride.claimsToAddOrOverride) {
|
|
1100
|
+
Object.assign(payload, claimsOverride.claimsToAddOrOverride);
|
|
1101
|
+
}
|
|
1102
|
+
if (Array.isArray(claimsOverride.claimsToSuppress)) {
|
|
1103
|
+
for (const key of claimsOverride.claimsToSuppress) {
|
|
1104
|
+
delete payload[key];
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
return jwt.sign(payload, this.jwtSecret, { algorithm: "HS256" });
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
generateRefreshToken(user, userPool, clientId) {
|
|
1113
|
+
const payload = {
|
|
1114
|
+
sub: user.UserId,
|
|
1115
|
+
token_use: "refresh",
|
|
1116
|
+
client_id: clientId,
|
|
1117
|
+
username: user.Username,
|
|
1118
|
+
iss: `https://cognito-idp.local/${userPool.Id}`,
|
|
1119
|
+
exp: Math.floor(Date.now() / 1000) + 2592000, // 30 dias
|
|
1120
|
+
iat: Math.floor(Date.now() / 1000),
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
return jwt.sign(payload, this.jwtSecret, { algorithm: "HS256" });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
verifyAccessToken(token) {
|
|
1127
|
+
try {
|
|
1128
|
+
const decoded = jwt.verify(token, this.jwtSecret);
|
|
1129
|
+
const session = this.accessTokens.get(token);
|
|
1130
|
+
|
|
1131
|
+
if (!session || session.ExpiresAt < new Date().toISOString()) {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return decoded;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ============ Admin Operations ============
|
|
1142
|
+
|
|
1143
|
+
adminGetUser(params) {
|
|
1144
|
+
const { UserPoolId, Username } = params;
|
|
1145
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
1146
|
+
|
|
1147
|
+
if (!userPool) {
|
|
1148
|
+
throw new Error(`User pool ${UserPoolId} not found`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
1152
|
+
if (!user) {
|
|
1153
|
+
throw new Error(`User not found: ${Username}`);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return {
|
|
1157
|
+
Username: user.Username,
|
|
1158
|
+
UserAttributes: this.formatUserAttributes(user.Attributes),
|
|
1159
|
+
UserCreateDate: user.CreatedDate,
|
|
1160
|
+
UserLastModifiedDate: user.LastModifiedDate,
|
|
1161
|
+
Enabled: user.Enabled,
|
|
1162
|
+
UserStatus: user.UserStatus,
|
|
1163
|
+
MFAOptions: user.MfaOptions,
|
|
1164
|
+
PreferredMfaSetting: user.PreferredMfaSetting,
|
|
1165
|
+
UserMFASettingList: user.UserMFASettingList,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
adminCreateUser(params) {
|
|
1170
|
+
const { UserPoolId, Username, UserAttributes, TemporaryPassword, DesiredDeliveryMediums } = params;
|
|
1171
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
1172
|
+
|
|
1173
|
+
if (!userPool) {
|
|
1174
|
+
throw new Error(`User pool ${UserPoolId} not found`);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const tempPassword = TemporaryPassword || this._generateTemporaryPassword();
|
|
1178
|
+
|
|
1179
|
+
const userId = uuidv4();
|
|
1180
|
+
const user = {
|
|
1181
|
+
Username: Username,
|
|
1182
|
+
UserPoolId: UserPoolId,
|
|
1183
|
+
UserId: userId,
|
|
1184
|
+
Attributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
1185
|
+
Enabled: true,
|
|
1186
|
+
UserStatus: "FORCE_CHANGE_PASSWORD",
|
|
1187
|
+
CreatedDate: new Date().toISOString(),
|
|
1188
|
+
LastModifiedDate: new Date().toISOString(),
|
|
1189
|
+
Password: this.hashPassword(tempPassword),
|
|
1190
|
+
MfaOptions: [],
|
|
1191
|
+
PreferredMfaSetting: null,
|
|
1192
|
+
UserMFASettingList: [],
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
// PreSignUp trigger — dispara antes de criar o usuário (admin context)
|
|
1196
|
+
const preSignUpEvent = this._buildTriggerEvent("PreSignUp_AdminCreateUser", userPool, user, "ADMIN", {
|
|
1197
|
+
userAttributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
1198
|
+
validationData: {},
|
|
1199
|
+
clientMetadata: {},
|
|
1200
|
+
});
|
|
1201
|
+
// Fire and forget — AdminCreateUser PreSignUp errors are non-blocking in local sim
|
|
1202
|
+
this._invokeTrigger(userPool, "PreSignUp", preSignUpEvent).catch((err) => {
|
|
1203
|
+
logger.warn(`PreSignUp trigger error on AdminCreateUser (ignored): ${err.message}`);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
this.users.set(userId, user);
|
|
1207
|
+
userPool.Users.push(userId);
|
|
1208
|
+
userPool.EstimatedNumberOfUsers++;
|
|
1209
|
+
this.persistUsers();
|
|
1210
|
+
this.persistUserPools();
|
|
1211
|
+
|
|
1212
|
+
logger.info(`👤 Usuário criado: ${Username} | Senha temporária: ${tempPassword}`);
|
|
1213
|
+
|
|
1214
|
+
return {
|
|
1215
|
+
User: {
|
|
1216
|
+
Username: user.Username,
|
|
1217
|
+
UserAttributes: this.formatUserAttributes(user.Attributes),
|
|
1218
|
+
UserCreateDate: user.CreatedDate,
|
|
1219
|
+
UserLastModifiedDate: user.LastModifiedDate,
|
|
1220
|
+
Enabled: user.Enabled,
|
|
1221
|
+
UserStatus: user.UserStatus,
|
|
1222
|
+
TemporaryPassword: tempPassword,
|
|
1223
|
+
},
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
_generateTemporaryPassword() {
|
|
1228
|
+
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
1229
|
+
const lower = "abcdefghijklmnopqrstuvwxyz";
|
|
1230
|
+
const digits = "0123456789";
|
|
1231
|
+
const special = "!@#$%^&*";
|
|
1232
|
+
const all = upper + lower + digits + special;
|
|
1233
|
+
const rand = (set) => set[Math.floor(Math.random() * set.length)];
|
|
1234
|
+
const password = [rand(upper), rand(lower), rand(digits), rand(special)];
|
|
1235
|
+
for (let i = 4; i < 10; i++) password.push(rand(all));
|
|
1236
|
+
return password.sort(() => Math.random() - 0.5).join("");
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
adminSetUserPassword(params) {
|
|
1240
|
+
const { UserPoolId, Username, Password, Permanent } = params;
|
|
1241
|
+
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
1242
|
+
|
|
1243
|
+
if (!user) {
|
|
1244
|
+
throw new Error(`User not found: ${Username}`);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
user.Password = this.hashPassword(Password);
|
|
1248
|
+
if (Permanent) {
|
|
1249
|
+
user.UserStatus = "CONFIRMED";
|
|
1250
|
+
}
|
|
1251
|
+
user.LastModifiedDate = new Date().toISOString();
|
|
1252
|
+
this.persistUsers();
|
|
1253
|
+
|
|
1254
|
+
return {};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
adminDeleteUser(params) {
|
|
1258
|
+
const { UserPoolId, Username } = params;
|
|
1259
|
+
const user = this.findUserByUsername(Username, null, UserPoolId);
|
|
1260
|
+
|
|
1261
|
+
if (!user) {
|
|
1262
|
+
throw new Error(`User not found: ${Username}`);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
1266
|
+
if (userPool) {
|
|
1267
|
+
const index = userPool.Users.indexOf(user.UserId);
|
|
1268
|
+
if (index !== -1) {
|
|
1269
|
+
userPool.Users.splice(index, 1);
|
|
1270
|
+
userPool.EstimatedNumberOfUsers--;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
this.users.delete(user.UserId);
|
|
1275
|
+
this.persistUsers();
|
|
1276
|
+
this.persistUserPools();
|
|
1277
|
+
|
|
1278
|
+
return {};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// ============ Helper Methods ============
|
|
1282
|
+
|
|
1283
|
+
findUserPoolByClientId(clientId) {
|
|
1284
|
+
for (const userPool of this.userPools.values()) {
|
|
1285
|
+
if (userPool.Clients.has(clientId)) {
|
|
1286
|
+
return userPool;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
findUserByUsername(username, clientId = null, userPoolId = null) {
|
|
1293
|
+
let targetUserPoolId = userPoolId;
|
|
1294
|
+
|
|
1295
|
+
if (clientId && !targetUserPoolId) {
|
|
1296
|
+
const userPool = this.findUserPoolByClientId(clientId);
|
|
1297
|
+
if (userPool) {
|
|
1298
|
+
targetUserPoolId = userPool.Id;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
for (const user of this.users.values()) {
|
|
1303
|
+
if (user.UserPoolId !== targetUserPoolId) continue;
|
|
1304
|
+
// Match by Username or by email attribute (when UsernameAttributes includes 'email')
|
|
1305
|
+
if (user.Username === username) return user;
|
|
1306
|
+
if (user.Attributes?.email === username) return user;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
normalizeUserAttributes(attributes) {
|
|
1313
|
+
const normalized = {};
|
|
1314
|
+
for (const attr of attributes) {
|
|
1315
|
+
normalized[attr.Name] = attr.Value;
|
|
1316
|
+
}
|
|
1317
|
+
return normalized;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
formatUserAttributes(attributes) {
|
|
1321
|
+
return Object.entries(attributes).map(([Name, Value]) => ({ Name, Value }));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
hashPassword(password) {
|
|
1325
|
+
// Simulação de hash (não usar em produção real)
|
|
1326
|
+
return crypto.createHash("sha256").update(password).digest("hex");
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
verifyPassword(password, hash) {
|
|
1330
|
+
return this.hashPassword(password) === hash;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// ============ Identity Pool Operations ============
|
|
1334
|
+
|
|
1335
|
+
createIdentityPool(params) {
|
|
1336
|
+
const { IdentityPoolName, AllowUnauthenticatedIdentities, SupportedLoginProviders, CognitoIdentityProviders } = params;
|
|
1337
|
+
|
|
1338
|
+
const identityPoolId = `local:${IdentityPoolName}_${Date.now()}`;
|
|
1339
|
+
const identityPool = {
|
|
1340
|
+
IdentityPoolId: identityPoolId,
|
|
1341
|
+
IdentityPoolName: IdentityPoolName,
|
|
1342
|
+
AllowUnauthenticatedIdentities: AllowUnauthenticatedIdentities || false,
|
|
1343
|
+
SupportedLoginProviders: SupportedLoginProviders || {},
|
|
1344
|
+
CognitoIdentityProviders: CognitoIdentityProviders || [],
|
|
1345
|
+
Identities: new Map(),
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
this.identityPools.set(identityPoolId, identityPool);
|
|
1349
|
+
this.persistIdentityPools();
|
|
1350
|
+
|
|
1351
|
+
logger.debug(`✅ Identity Pool criado: ${IdentityPoolName} (${identityPoolId})`);
|
|
1352
|
+
|
|
1353
|
+
return {
|
|
1354
|
+
IdentityPoolId: identityPoolId,
|
|
1355
|
+
IdentityPoolName: identityPoolName,
|
|
1356
|
+
AllowUnauthenticatedIdentities: identityPool.AllowUnauthenticatedIdentities,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
getId(params) {
|
|
1361
|
+
const { IdentityPoolId, Logins } = params;
|
|
1362
|
+
const identityPool = this.identityPools.get(IdentityPoolId);
|
|
1363
|
+
|
|
1364
|
+
if (!identityPool) {
|
|
1365
|
+
throw new Error(`Identity pool ${IdentityPoolId} not found`);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
let identityId = null;
|
|
1369
|
+
|
|
1370
|
+
if (Logins) {
|
|
1371
|
+
// Procura identidade existente com os logins fornecidos
|
|
1372
|
+
for (const [id, identity] of identityPool.Identities) {
|
|
1373
|
+
if (identity.Logins && this.matchesLogins(identity.Logins, Logins)) {
|
|
1374
|
+
identityId = id;
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (!identityId) {
|
|
1381
|
+
identityId = uuidv4();
|
|
1382
|
+
identityPool.Identities.set(identityId, {
|
|
1383
|
+
IdentityId: identityId,
|
|
1384
|
+
Logins: Logins || {},
|
|
1385
|
+
CreationDate: new Date().toISOString(),
|
|
1386
|
+
LastModifiedDate: new Date().toISOString(),
|
|
1387
|
+
});
|
|
1388
|
+
this.persistIdentityPools();
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return {
|
|
1392
|
+
IdentityId: identityId,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
getCredentialsForIdentity(params) {
|
|
1397
|
+
const { IdentityId, Logins } = params;
|
|
1398
|
+
const identityPool = this.findIdentityPoolByIdentityId(IdentityId);
|
|
1399
|
+
|
|
1400
|
+
if (!identityPool) {
|
|
1401
|
+
throw new Error(`Identity ${IdentityId} not found`);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const identity = identityPool.Identities.get(IdentityId);
|
|
1405
|
+
if (!identity) {
|
|
1406
|
+
throw new Error(`Identity ${IdentityId} not found in pool`);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Gera credenciais temporárias (simuladas)
|
|
1410
|
+
const credentials = {
|
|
1411
|
+
AccessKeyId: `AKIA${crypto.randomBytes(16).toString("hex").toUpperCase()}`,
|
|
1412
|
+
SecretKey: crypto.randomBytes(32).toString("hex"),
|
|
1413
|
+
SessionToken: crypto.randomBytes(64).toString("base64"),
|
|
1414
|
+
Expiration: new Date(Date.now() + 3600000).toISOString(),
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
return {
|
|
1418
|
+
Credentials: credentials,
|
|
1419
|
+
IdentityId: IdentityId,
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
findIdentityPoolByIdentityId(identityId) {
|
|
1424
|
+
for (const pool of this.identityPools.values()) {
|
|
1425
|
+
if (pool.Identities.has(identityId)) {
|
|
1426
|
+
return pool;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
matchesLogins(existingLogins, newLogins) {
|
|
1433
|
+
const existingKeys = Object.keys(existingLogins);
|
|
1434
|
+
const newKeys = Object.keys(newLogins);
|
|
1435
|
+
|
|
1436
|
+
if (existingKeys.length !== newKeys.length) return false;
|
|
1437
|
+
|
|
1438
|
+
for (const key of existingKeys) {
|
|
1439
|
+
if (existingLogins[key] !== newLogins[key]) {
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// ============ Persistence ============
|
|
1448
|
+
|
|
1449
|
+
loadUserPools() {
|
|
1450
|
+
// Load persisted pools first
|
|
1451
|
+
const saved = this.store.read("__userpools__");
|
|
1452
|
+
if (saved) {
|
|
1453
|
+
for (const [id, data] of Object.entries(saved)) {
|
|
1454
|
+
data.Clients = new Map(Object.entries(data.Clients || {}));
|
|
1455
|
+
data.Groups = new Map(Object.entries(data.Groups || {}));
|
|
1456
|
+
data.IdentityProviders = new Map(Object.entries(data.IdentityProviders || {}));
|
|
1457
|
+
data.ResourceServers = new Map(Object.entries(data.ResourceServers || {}));
|
|
1458
|
+
this.userPools.set(id, data);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Create user pools from config if not already persisted
|
|
1463
|
+
if (this.config.cognito?.userPools) {
|
|
1464
|
+
let configChanged = false;
|
|
1465
|
+
const configPath = this.config._configPath;
|
|
1466
|
+
|
|
1467
|
+
for (let i = 0; i < this.config.cognito.userPools.length; i++) {
|
|
1468
|
+
const poolConfig = this.config.cognito.userPools[i];
|
|
1469
|
+
const existing = Array.from(this.userPools.values()).find((p) => p.Name === poolConfig.PoolName);
|
|
1470
|
+
|
|
1471
|
+
if (!existing) {
|
|
1472
|
+
const result = this.createUserPool(poolConfig);
|
|
1473
|
+
const poolId = result.UserPool.Id;
|
|
1474
|
+
logger.debug(`✅ User Pool criado a partir da config: ${poolConfig.PoolName} (${poolId})`);
|
|
1475
|
+
|
|
1476
|
+
// Copy LambdaTriggers from config onto the pool object
|
|
1477
|
+
const pool = this.userPools.get(poolId);
|
|
1478
|
+
pool.LambdaTriggers = poolConfig.LambdaTriggers || {};
|
|
1479
|
+
|
|
1480
|
+
// Auto-create a default client if not specified
|
|
1481
|
+
if (!poolConfig.ClientId) {
|
|
1482
|
+
const clientResult = this.createUserPoolClient({
|
|
1483
|
+
UserPoolId: poolId,
|
|
1484
|
+
ClientName: `${poolConfig.PoolName}-client`,
|
|
1485
|
+
GenerateSecret: false,
|
|
1486
|
+
});
|
|
1487
|
+
const clientId = clientResult.UserPoolClient.ClientId;
|
|
1488
|
+
this.config.cognito.userPools[i].ClientId = clientId;
|
|
1489
|
+
this.config.cognito.userPools[i].UserPoolId = poolId;
|
|
1490
|
+
configChanged = true;
|
|
1491
|
+
logger.debug(`✅ Client criado automaticamente: ${clientId}`);
|
|
1492
|
+
}
|
|
1493
|
+
} else {
|
|
1494
|
+
// Pool already exists — apply LambdaTriggers from config
|
|
1495
|
+
existing.LambdaTriggers = poolConfig.LambdaTriggers || {};
|
|
1496
|
+
|
|
1497
|
+
if (!poolConfig.ClientId) {
|
|
1498
|
+
// Pool exists but no clientId in config — write it back
|
|
1499
|
+
const firstClient = existing.Clients.size > 0 ? existing.Clients.values().next().value : null;
|
|
1500
|
+
if (firstClient) {
|
|
1501
|
+
this.config.cognito.userPools[i].ClientId = firstClient.ClientId;
|
|
1502
|
+
this.config.cognito.userPools[i].UserPoolId = existing.Id;
|
|
1503
|
+
configChanged = true;
|
|
1504
|
+
}
|
|
1505
|
+
} else if (!existing.Clients.has(poolConfig.ClientId)) {
|
|
1506
|
+
// ClientId is in config but not in the pool's Clients map — register it
|
|
1507
|
+
existing.Clients.set(poolConfig.ClientId, {
|
|
1508
|
+
ClientId: poolConfig.ClientId,
|
|
1509
|
+
ClientName: `${poolConfig.PoolName}-client`,
|
|
1510
|
+
ClientSecret: null,
|
|
1511
|
+
UserPoolId: existing.Id,
|
|
1512
|
+
RefreshTokenValidity: 30,
|
|
1513
|
+
AccessTokenValidity: 1,
|
|
1514
|
+
IdTokenValidity: 1,
|
|
1515
|
+
AllowedOAuthFlows: ['code'],
|
|
1516
|
+
AllowedOAuthScopes: ['openid', 'email', 'profile'],
|
|
1517
|
+
CallbackURLs: [],
|
|
1518
|
+
LogoutURLs: [],
|
|
1519
|
+
CreatedDate: new Date().toISOString(),
|
|
1520
|
+
LastModifiedDate: new Date().toISOString(),
|
|
1521
|
+
});
|
|
1522
|
+
this.persistUserPools();
|
|
1523
|
+
logger.debug(`✅ ClientId ${poolConfig.ClientId} registrado no pool ${existing.Id}`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Write clientId back to aws-local-simulator.json
|
|
1529
|
+
if (configChanged && configPath) {
|
|
1530
|
+
try {
|
|
1531
|
+
const fs = require("fs");
|
|
1532
|
+
const fileContent = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
1533
|
+
fileContent.cognito = fileContent.cognito || {};
|
|
1534
|
+
fileContent.cognito.userPools = this.config.cognito.userPools.map((p) => ({
|
|
1535
|
+
PoolName: p.PoolName,
|
|
1536
|
+
AutoVerifiedAttributes: p.AutoVerifiedAttributes,
|
|
1537
|
+
UserPoolId: p.UserPoolId,
|
|
1538
|
+
ClientId: p.ClientId,
|
|
1539
|
+
}));
|
|
1540
|
+
fs.writeFileSync(configPath, JSON.stringify(fileContent, null, 2));
|
|
1541
|
+
logger.info(`✅ ClientId gravado em: ${configPath}`);
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
logger.warn(`⚠️ Não foi possível gravar clientId no config: ${err.message}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
loadIdentityPools() {
|
|
1550
|
+
const saved = this.store.read("__identitypools__");
|
|
1551
|
+
if (saved) {
|
|
1552
|
+
for (const [id, data] of Object.entries(saved)) {
|
|
1553
|
+
data.Identities = new Map(Object.entries(data.Identities || {}));
|
|
1554
|
+
this.identityPools.set(id, data);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
loadUsers() {
|
|
1560
|
+
const saved = this.store.read("__users__");
|
|
1561
|
+
if (saved) {
|
|
1562
|
+
for (const [id, user] of Object.entries(saved)) {
|
|
1563
|
+
this.users.set(id, user);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
loadSessions() {
|
|
1569
|
+
const saved = this.store.read("__sessions__");
|
|
1570
|
+
if (saved) {
|
|
1571
|
+
for (const [id, session] of Object.entries(saved)) {
|
|
1572
|
+
this.sessions.set(id, session);
|
|
1573
|
+
this.accessTokens.set(session.AccessToken, session);
|
|
1574
|
+
this.refreshTokens.set(session.RefreshToken, session);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
persistUserPools() {
|
|
1580
|
+
const poolsObj = {};
|
|
1581
|
+
for (const [id, pool] of this.userPools.entries()) {
|
|
1582
|
+
poolsObj[id] = {
|
|
1583
|
+
...pool,
|
|
1584
|
+
Clients: Object.fromEntries(pool.Clients),
|
|
1585
|
+
Groups: Object.fromEntries(pool.Groups),
|
|
1586
|
+
IdentityProviders: Object.fromEntries(pool.IdentityProviders),
|
|
1587
|
+
ResourceServers: Object.fromEntries(pool.ResourceServers),
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
this.store.write("__userpools__", poolsObj);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
persistIdentityPools() {
|
|
1594
|
+
const poolsObj = {};
|
|
1595
|
+
for (const [id, pool] of this.identityPools.entries()) {
|
|
1596
|
+
poolsObj[id] = {
|
|
1597
|
+
...pool,
|
|
1598
|
+
Identities: Object.fromEntries(pool.Identities),
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
this.store.write("__identitypools__", poolsObj);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
persistUsers() {
|
|
1605
|
+
const usersObj = {};
|
|
1606
|
+
for (const [id, user] of this.users.entries()) {
|
|
1607
|
+
usersObj[id] = user;
|
|
1608
|
+
}
|
|
1609
|
+
this.store.write("__users__", usersObj);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
persistSessions() {
|
|
1613
|
+
const sessionsObj = {};
|
|
1614
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
1615
|
+
sessionsObj[id] = session;
|
|
1616
|
+
}
|
|
1617
|
+
this.store.write("__sessions__", sessionsObj);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
async reset() {
|
|
1621
|
+
this.userPools.clear();
|
|
1622
|
+
this.identityPools.clear();
|
|
1623
|
+
this.users.clear();
|
|
1624
|
+
this.sessions.clear();
|
|
1625
|
+
this.accessTokens.clear();
|
|
1626
|
+
this.refreshTokens.clear();
|
|
1627
|
+
|
|
1628
|
+
this.persistUserPools();
|
|
1629
|
+
this.persistIdentityPools();
|
|
1630
|
+
this.persistUsers();
|
|
1631
|
+
this.persistSessions();
|
|
1632
|
+
|
|
1633
|
+
logger.debug("Cognito: Todos os dados resetados");
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// ============ Stats ============
|
|
1637
|
+
|
|
1638
|
+
getUserPoolsCount() {
|
|
1639
|
+
return this.userPools.size;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
getTotalUsersCount() {
|
|
1643
|
+
return this.users.size;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
getIdentityPoolsCount() {
|
|
1647
|
+
return this.identityPools.size;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
getActiveSessionsCount() {
|
|
1651
|
+
return this.sessions.size;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
module.exports = CognitoSimulator;
|