@onivoro/server-aws-sts 24.30.12 → 24.30.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +142 -700
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @onivoro/server-aws-sts
2
2
 
3
- A NestJS module for integrating with AWS STS (Security Token Service), providing temporary credential management, role assumption, identity verification, and cross-account access capabilities for your server applications.
3
+ AWS STS integration for NestJS applications.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,823 +8,265 @@ A NestJS module for integrating with AWS STS (Security Token Service), providing
8
8
  npm install @onivoro/server-aws-sts
9
9
  ```
10
10
 
11
- ## Features
11
+ ## Overview
12
12
 
13
- - **Identity Verification**: Get caller identity and account information
14
- - **Role Assumption**: Assume IAM roles for temporary access
15
- - **Cross-Account Access**: Access resources across different AWS accounts
16
- - **Temporary Credentials**: Generate and manage temporary AWS credentials
17
- - **Session Management**: Create and manage temporary security sessions
18
- - **Token Management**: Handle session tokens and their expiration
19
- - **MFA Support**: Support for multi-factor authentication in role assumption
20
- - **External ID Validation**: Secure cross-account access with external IDs
13
+ This library provides a minimal AWS STS (Security Token Service) integration for NestJS applications, offering account ID retrieval functionality.
21
14
 
22
- ## Quick Start
23
-
24
- ### 1. Module Configuration
15
+ ## Module Setup
25
16
 
26
17
  ```typescript
18
+ import { Module } from '@nestjs/common';
27
19
  import { ServerAwsStsModule } from '@onivoro/server-aws-sts';
28
20
 
29
21
  @Module({
30
22
  imports: [
31
- ServerAwsStsModule.configure({
32
- AWS_REGION: 'us-east-1',
33
- AWS_PROFILE: process.env.AWS_PROFILE || 'default',
34
- }),
35
- ],
23
+ ServerAwsStsModule.configure()
24
+ ]
36
25
  })
37
26
  export class AppModule {}
38
27
  ```
39
28
 
40
- ### 2. Basic Usage
41
-
42
- ```typescript
43
- import { StsService } from '@onivoro/server-aws-sts';
44
-
45
- @Injectable()
46
- export class IdentityService {
47
- constructor(private stsService: StsService) {}
48
-
49
- async getCurrentAccount() {
50
- const accountId = await this.stsService.getAccountId();
51
- return accountId;
52
- }
53
-
54
- async verifyIdentity() {
55
- const identity = await this.stsService.stsClient.send(new GetCallerIdentityCommand({}));
56
- return {
57
- account: identity.Account,
58
- arn: identity.Arn,
59
- userId: identity.UserId
60
- };
61
- }
62
- }
63
- ```
64
-
65
29
  ## Configuration
66
30
 
67
- ### ServerAwsStsConfig
31
+ The module uses environment-based configuration:
68
32
 
69
33
  ```typescript
70
- import { ServerAwsStsConfig } from '@onivoro/server-aws-sts';
71
-
72
- export class AppStsConfig extends ServerAwsStsConfig {
73
- AWS_REGION = process.env.AWS_REGION || 'us-east-1';
74
- AWS_PROFILE = process.env.AWS_PROFILE || 'default';
75
- DEFAULT_SESSION_DURATION = parseInt(process.env.STS_SESSION_DURATION) || 3600; // 1 hour
76
- MAX_SESSION_DURATION = parseInt(process.env.STS_MAX_SESSION_DURATION) || 43200; // 12 hours
77
- EXTERNAL_ID = process.env.STS_EXTERNAL_ID; // For cross-account access
34
+ export class ServerAwsStsConfig {
35
+ AWS_REGION: string;
36
+ AWS_PROFILE?: string; // Optional AWS profile
78
37
  }
79
38
  ```
80
39
 
81
- ### Environment Variables
82
-
83
- ```bash
84
- # AWS Configuration
85
- AWS_REGION=us-east-1
86
- AWS_PROFILE=default
87
-
88
- # STS Configuration
89
- STS_SESSION_DURATION=3600
90
- STS_MAX_SESSION_DURATION=43200
91
- STS_EXTERNAL_ID=unique-external-identifier
92
- ```
93
-
94
- ## Services
40
+ ## Service
95
41
 
96
42
  ### StsService
97
43
 
98
- The main service for STS operations:
44
+ The service provides a single method for retrieving the AWS account ID:
99
45
 
100
46
  ```typescript
47
+ import { Injectable } from '@nestjs/common';
101
48
  import { StsService } from '@onivoro/server-aws-sts';
102
49
 
103
50
  @Injectable()
104
- export class CredentialManagementService {
105
- constructor(private stsService: StsService) {}
51
+ export class AccountService {
52
+ constructor(private readonly stsService: StsService) {}
106
53
 
107
- async getCurrentAccountInfo() {
54
+ async getCurrentAccountId() {
108
55
  const accountId = await this.stsService.getAccountId();
109
- const identity = await this.stsService.stsClient.send(new GetCallerIdentityCommand({}));
110
-
111
- return {
112
- accountId,
113
- arn: identity.Arn,
114
- userId: identity.UserId,
115
- type: this.getIdentityType(identity.Arn!)
116
- };
117
- }
118
-
119
- private getIdentityType(arn: string): string {
120
- if (arn.includes(':user/')) return 'IAM User';
121
- if (arn.includes(':role/')) return 'IAM Role';
122
- if (arn.includes(':assumed-role/')) return 'Assumed Role';
123
- if (arn.includes(':federated-user/')) return 'Federated User';
124
- return 'Unknown';
56
+ console.log(`Current AWS Account: ${accountId}`);
57
+ return accountId;
125
58
  }
126
59
  }
127
60
  ```
128
61
 
129
- ## Usage Examples
62
+ ## Available Method
130
63
 
131
- ### Role Assumption Service
64
+ - **getAccountId()** - Retrieves the AWS account ID for the current credentials
65
+
66
+ ## Direct Client Access
67
+
68
+ The service exposes the underlying STS client for advanced operations:
132
69
 
133
70
  ```typescript
71
+ import { Injectable } from '@nestjs/common';
134
72
  import { StsService } from '@onivoro/server-aws-sts';
135
- import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
73
+ import {
74
+ AssumeRoleCommand,
75
+ GetSessionTokenCommand,
76
+ GetAccessKeyInfoCommand
77
+ } from '@aws-sdk/client-sts';
136
78
 
137
79
  @Injectable()
138
- export class RoleAssumptionService {
139
- constructor(private stsService: StsService) {}
80
+ export class AdvancedStsService {
81
+ constructor(private readonly stsService: StsService) {}
140
82
 
141
- async assumeRole(roleArn: string, sessionName: string, durationSeconds?: number) {
83
+ // Assume a role
84
+ async assumeRole(roleArn: string, sessionName: string) {
142
85
  const command = new AssumeRoleCommand({
143
86
  RoleArn: roleArn,
144
87
  RoleSessionName: sessionName,
145
- DurationSeconds: durationSeconds || 3600
88
+ DurationSeconds: 3600 // 1 hour
146
89
  });
147
-
148
- const response = await this.stsService.stsClient.send(command);
149
90
 
150
- if (!response.Credentials) {
151
- throw new Error('Failed to assume role');
152
- }
153
-
154
- return {
155
- accessKeyId: response.Credentials.AccessKeyId!,
156
- secretAccessKey: response.Credentials.SecretAccessKey!,
157
- sessionToken: response.Credentials.SessionToken!,
158
- expiration: response.Credentials.Expiration!,
159
- assumedRoleUser: response.AssumedRoleUser
160
- };
161
- }
162
-
163
- async assumeRoleWithExternalId(roleArn: string, sessionName: string, externalId: string, durationSeconds?: number) {
164
- const command = new AssumeRoleCommand({
165
- RoleArn: roleArn,
166
- RoleSessionName: sessionName,
167
- ExternalId: externalId,
168
- DurationSeconds: durationSeconds || 3600
169
- });
170
-
171
- return this.executeAssumeRole(command);
91
+ const response = await this.stsService.stsClient.send(command);
92
+ return response.Credentials;
172
93
  }
173
94
 
174
- async assumeRoleWithMFA(roleArn: string, sessionName: string, mfaDeviceArn: string, mfaToken: string) {
175
- const command = new AssumeRoleCommand({
176
- RoleArn: roleArn,
177
- RoleSessionName: sessionName,
178
- SerialNumber: mfaDeviceArn,
179
- TokenCode: mfaToken,
180
- DurationSeconds: 3600
95
+ // Get temporary session token
96
+ async getSessionToken(durationSeconds: number = 3600) {
97
+ const command = new GetSessionTokenCommand({
98
+ DurationSeconds: durationSeconds
181
99
  });
182
-
183
- return this.executeAssumeRole(command);
100
+
101
+ const response = await this.stsService.stsClient.send(command);
102
+ return response.Credentials;
184
103
  }
185
104
 
186
- async assumeRoleWithWebIdentity(roleArn: string, sessionName: string, webIdentityToken: string, providerId?: string) {
187
- const command = new AssumeRoleWithWebIdentityCommand({
188
- RoleArn: roleArn,
189
- RoleSessionName: sessionName,
190
- WebIdentityToken: webIdentityToken,
191
- ProviderId: providerId,
192
- DurationSeconds: 3600
105
+ // Get access key info
106
+ async getAccessKeyInfo(accessKeyId: string) {
107
+ const command = new GetAccessKeyInfoCommand({
108
+ AccessKeyId: accessKeyId
193
109
  });
194
-
195
- const response = await this.stsService.stsClient.send(command);
196
110
 
197
- if (!response.Credentials) {
198
- throw new Error('Failed to assume role with web identity');
199
- }
200
-
201
- return {
202
- accessKeyId: response.Credentials.AccessKeyId!,
203
- secretAccessKey: response.Credentials.SecretAccessKey!,
204
- sessionToken: response.Credentials.SessionToken!,
205
- expiration: response.Credentials.Expiration!,
206
- assumedRoleUser: response.AssumedRoleUser,
207
- audience: response.Audience,
208
- provider: response.Provider
209
- };
111
+ return await this.stsService.stsClient.send(command);
210
112
  }
211
113
 
212
- private async executeAssumeRole(command: AssumeRoleCommand) {
114
+ // Get caller identity (alternative to getAccountId)
115
+ async getCallerIdentity() {
116
+ const command = new GetCallerIdentityCommand({});
213
117
  const response = await this.stsService.stsClient.send(command);
214
118
 
215
- if (!response.Credentials) {
216
- throw new Error('Failed to assume role');
217
- }
218
-
219
119
  return {
220
- accessKeyId: response.Credentials.AccessKeyId!,
221
- secretAccessKey: response.Credentials.SecretAccessKey!,
222
- sessionToken: response.Credentials.SessionToken!,
223
- expiration: response.Credentials.Expiration!,
224
- assumedRoleUser: response.AssumedRoleUser
120
+ accountId: response.Account,
121
+ arn: response.Arn,
122
+ userId: response.UserId
225
123
  };
226
124
  }
227
125
  }
228
126
  ```
229
127
 
230
- ### Cross-Account Access Service
128
+ ## Complete Example
231
129
 
232
130
  ```typescript
233
- import { StsService } from '@onivoro/server-aws-sts';
131
+ import { Module, Injectable, Controller, Get, Post, Body } from '@nestjs/common';
132
+ import { ServerAwsStsModule, StsService } from '@onivoro/server-aws-sts';
133
+ import { AssumeRoleCommand } from '@aws-sdk/client-sts';
134
+
135
+ @Module({
136
+ imports: [ServerAwsStsModule.configure()],
137
+ controllers: [SecurityController],
138
+ providers: [SecurityService]
139
+ })
140
+ export class SecurityModule {}
234
141
 
235
142
  @Injectable()
236
- export class CrossAccountService {
237
- constructor(private stsService: StsService) {}
238
-
239
- async accessCrossAccountResource(
240
- targetAccountId: string,
241
- roleName: string,
242
- externalId?: string,
243
- sessionDuration: number = 3600
244
- ) {
143
+ export class SecurityService {
144
+ constructor(private readonly stsService: StsService) {}
145
+
146
+ async getCrossAccountCredentials(targetAccountId: string, roleName: string) {
147
+ const currentAccountId = await this.stsService.getAccountId();
245
148
  const roleArn = `arn:aws:iam::${targetAccountId}:role/${roleName}`;
246
- const sessionName = `cross-account-${Date.now()}`;
149
+ const sessionName = `cross-account-${currentAccountId}-${Date.now()}`;
247
150
 
248
151
  const command = new AssumeRoleCommand({
249
152
  RoleArn: roleArn,
250
153
  RoleSessionName: sessionName,
251
- DurationSeconds: sessionDuration,
252
- ...(externalId && { ExternalId: externalId })
154
+ DurationSeconds: 3600,
155
+ ExternalId: process.env.EXTERNAL_ID // If required by trust policy
253
156
  });
254
157
 
255
- const response = await this.stsService.stsClient.send(command);
256
-
257
- if (!response.Credentials) {
258
- throw new Error(`Failed to assume role in account ${targetAccountId}`);
259
- }
260
-
261
- return {
262
- credentials: {
263
- accessKeyId: response.Credentials.AccessKeyId!,
264
- secretAccessKey: response.Credentials.SecretAccessKey!,
265
- sessionToken: response.Credentials.SessionToken!
266
- },
267
- expiration: response.Credentials.Expiration!,
268
- targetAccount: targetAccountId,
269
- assumedRole: roleArn
270
- };
271
- }
272
-
273
- async createCrossAccountClient<T>(
274
- clientClass: new (config: any) => T,
275
- targetAccountId: string,
276
- roleName: string,
277
- region: string,
278
- externalId?: string
279
- ): Promise<T> {
280
- const crossAccountAccess = await this.accessCrossAccountResource(
281
- targetAccountId,
282
- roleName,
283
- externalId
284
- );
285
-
286
- return new clientClass({
287
- region,
288
- credentials: crossAccountAccess.credentials
289
- });
290
- }
291
-
292
- async validateCrossAccountAccess(targetAccountId: string, roleName: string, externalId?: string): Promise<boolean> {
293
158
  try {
294
- const access = await this.accessCrossAccountResource(targetAccountId, roleName, externalId, 900); // 15 minutes
295
-
296
- // Test the credentials by calling GetCallerIdentity
297
- const testStsClient = new STSClient({
298
- credentials: access.credentials
299
- });
300
-
301
- const identity = await testStsClient.send(new GetCallerIdentityCommand({}));
302
-
303
- return identity.Account === targetAccountId;
159
+ const response = await this.stsService.stsClient.send(command);
160
+ return {
161
+ accessKeyId: response.Credentials.AccessKeyId,
162
+ secretAccessKey: response.Credentials.SecretAccessKey,
163
+ sessionToken: response.Credentials.SessionToken,
164
+ expiration: response.Credentials.Expiration
165
+ };
304
166
  } catch (error) {
305
- console.error('Cross-account access validation failed:', error);
306
- return false;
307
- }
308
- }
309
- }
310
- ```
311
-
312
- ### Session Management Service
313
-
314
- ```typescript
315
- import { StsService } from '@onivoro/server-aws-sts';
316
- import { GetSessionTokenCommand } from '@aws-sdk/client-sts';
317
-
318
- @Injectable()
319
- export class SessionManagementService {
320
- private activeSessions = new Map<string, SessionInfo>();
321
-
322
- constructor(private stsService: StsService) {}
323
-
324
- async createSession(sessionName: string, durationSeconds?: number, mfaDevice?: string, mfaToken?: string) {
325
- const command = new GetSessionTokenCommand({
326
- DurationSeconds: durationSeconds || 3600,
327
- ...(mfaDevice && mfaToken && {
328
- SerialNumber: mfaDevice,
329
- TokenCode: mfaToken
330
- })
331
- });
332
-
333
- const response = await this.stsService.stsClient.send(command);
334
-
335
- if (!response.Credentials) {
336
- throw new Error('Failed to create session token');
337
- }
338
-
339
- const sessionInfo: SessionInfo = {
340
- sessionName,
341
- credentials: {
342
- accessKeyId: response.Credentials.AccessKeyId!,
343
- secretAccessKey: response.Credentials.SecretAccessKey!,
344
- sessionToken: response.Credentials.SessionToken!
345
- },
346
- expiration: response.Credentials.Expiration!,
347
- createdAt: new Date(),
348
- mfaUsed: !!(mfaDevice && mfaToken)
349
- };
350
-
351
- this.activeSessions.set(sessionName, sessionInfo);
352
-
353
- return sessionInfo;
354
- }
355
-
356
- async refreshSession(sessionName: string, durationSeconds?: number) {
357
- const existingSession = this.activeSessions.get(sessionName);
358
-
359
- if (!existingSession) {
360
- throw new Error(`Session ${sessionName} not found`);
361
- }
362
-
363
- // Create new session with same parameters
364
- return this.createSession(sessionName, durationSeconds);
365
- }
366
-
367
- async getSession(sessionName: string): Promise<SessionInfo | undefined> {
368
- const session = this.activeSessions.get(sessionName);
369
-
370
- if (session && this.isSessionExpired(session)) {
371
- this.activeSessions.delete(sessionName);
372
- return undefined;
373
- }
374
-
375
- return session;
376
- }
377
-
378
- async revokeSession(sessionName: string) {
379
- this.activeSessions.delete(sessionName);
380
- }
381
-
382
- async getActiveSessions(): Promise<SessionInfo[]> {
383
- const activeSessions = [];
384
-
385
- for (const [name, session] of this.activeSessions.entries()) {
386
- if (this.isSessionExpired(session)) {
387
- this.activeSessions.delete(name);
388
- } else {
389
- activeSessions.push(session);
390
- }
391
- }
392
-
393
- return activeSessions;
394
- }
395
-
396
- private isSessionExpired(session: SessionInfo): boolean {
397
- return new Date() >= session.expiration;
398
- }
399
-
400
- async cleanupExpiredSessions() {
401
- const expired = [];
402
-
403
- for (const [name, session] of this.activeSessions.entries()) {
404
- if (this.isSessionExpired(session)) {
405
- expired.push(name);
406
- this.activeSessions.delete(name);
407
- }
408
- }
409
-
410
- console.log(`Cleaned up ${expired.length} expired sessions`);
411
- return expired;
412
- }
413
- }
414
- ```
415
-
416
- ### Federated Identity Service
417
-
418
- ```typescript
419
- import { StsService } from '@onivoro/server-aws-sts';
420
- import { AssumeRoleWithSAMLCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
421
-
422
- @Injectable()
423
- export class FederatedIdentityService {
424
- constructor(private stsService: StsService) {}
425
-
426
- async assumeRoleWithSAML(
427
- roleArn: string,
428
- principalArn: string,
429
- samlAssertion: string,
430
- sessionName?: string
431
- ) {
432
- const command = new AssumeRoleWithSAMLCommand({
433
- RoleArn: roleArn,
434
- PrincipalArn: principalArn,
435
- SAMLAssertion: samlAssertion,
436
- DurationSeconds: 3600
437
- });
438
-
439
- const response = await this.stsService.stsClient.send(command);
440
-
441
- if (!response.Credentials) {
442
- throw new Error('Failed to assume role with SAML');
443
- }
444
-
445
- return {
446
- credentials: {
447
- accessKeyId: response.Credentials.AccessKeyId!,
448
- secretAccessKey: response.Credentials.SecretAccessKey!,
449
- sessionToken: response.Credentials.SessionToken!
450
- },
451
- expiration: response.Credentials.Expiration!,
452
- assumedRoleUser: response.AssumedRoleUser,
453
- audience: response.Audience,
454
- issuer: response.Issuer,
455
- subject: response.Subject,
456
- subjectType: response.SubjectType
457
- };
458
- }
459
-
460
- async assumeRoleWithOIDC(
461
- roleArn: string,
462
- webIdentityToken: string,
463
- providerId?: string,
464
- sessionName?: string
465
- ) {
466
- const command = new AssumeRoleWithWebIdentityCommand({
467
- RoleArn: roleArn,
468
- RoleSessionName: sessionName || `oidc-session-${Date.now()}`,
469
- WebIdentityToken: webIdentityToken,
470
- ProviderId: providerId,
471
- DurationSeconds: 3600
472
- });
473
-
474
- const response = await this.stsService.stsClient.send(command);
475
-
476
- if (!response.Credentials) {
477
- throw new Error('Failed to assume role with OIDC');
167
+ console.error('Failed to assume role:', error);
168
+ throw error;
478
169
  }
479
-
480
- return {
481
- credentials: {
482
- accessKeyId: response.Credentials.AccessKeyId!,
483
- secretAccessKey: response.Credentials.SecretAccessKey!,
484
- sessionToken: response.Credentials.SessionToken!
485
- },
486
- expiration: response.Credentials.Expiration!,
487
- assumedRoleUser: response.AssumedRoleUser,
488
- audience: response.Audience,
489
- provider: response.Provider
490
- };
491
170
  }
492
171
 
493
- async createFederatedSession(identityProvider: IdentityProvider, token: string, roleArn: string) {
494
- switch (identityProvider.type) {
495
- case 'SAML':
496
- return this.assumeRoleWithSAML(
497
- roleArn,
498
- identityProvider.principalArn!,
499
- token
500
- );
501
- case 'OIDC':
502
- return this.assumeRoleWithOIDC(
503
- roleArn,
504
- token,
505
- identityProvider.providerId
506
- );
507
- default:
508
- throw new Error(`Unsupported identity provider type: ${identityProvider.type}`);
509
- }
510
- }
511
- }
512
- ```
513
-
514
- ### Token Validation Service
515
-
516
- ```typescript
517
- import { StsService } from '@onivoro/server-aws-sts';
518
-
519
- @Injectable()
520
- export class TokenValidationService {
521
- constructor(private stsService: StsService) {}
522
-
523
- async validateCredentials(credentials: AWSCredentials): Promise<ValidationResult> {
172
+ async validateCurrentCredentials() {
524
173
  try {
525
- const testStsClient = new STSClient({
526
- credentials: {
527
- accessKeyId: credentials.accessKeyId,
528
- secretAccessKey: credentials.secretAccessKey,
529
- sessionToken: credentials.sessionToken
530
- }
531
- });
532
-
533
- const identity = await testStsClient.send(new GetCallerIdentityCommand({}));
534
-
174
+ const accountId = await this.stsService.getAccountId();
535
175
  return {
536
176
  valid: true,
537
- identity: {
538
- account: identity.Account!,
539
- arn: identity.Arn!,
540
- userId: identity.UserId!
541
- },
542
- expiresAt: credentials.expiration
177
+ accountId
543
178
  };
544
- } catch (error: any) {
179
+ } catch (error) {
545
180
  return {
546
181
  valid: false,
547
- error: error.message,
548
- errorCode: error.name
182
+ error: error.message
549
183
  };
550
184
  }
551
185
  }
552
-
553
- async checkTokenExpiration(expiration: Date, bufferMinutes: number = 5): Promise<ExpirationCheck> {
554
- const now = new Date();
555
- const bufferTime = new Date(expiration.getTime() - (bufferMinutes * 60 * 1000));
556
-
557
- return {
558
- isExpired: now >= expiration,
559
- isNearExpiration: now >= bufferTime,
560
- remainingMinutes: Math.max(0, Math.floor((expiration.getTime() - now.getTime()) / (60 * 1000))),
561
- expiration
562
- };
563
- }
564
-
565
- async getIdentityFromToken(credentials: AWSCredentials) {
566
- const validation = await this.validateCredentials(credentials);
567
-
568
- if (!validation.valid) {
569
- throw new Error(`Invalid credentials: ${validation.error}`);
570
- }
571
-
572
- return validation.identity;
573
- }
574
-
575
- async isTemporaryCredential(credentials: AWSCredentials): Promise<boolean> {
576
- // Temporary credentials always have a session token
577
- return !!credentials.sessionToken;
578
- }
579
186
  }
580
- ```
581
-
582
- ## Advanced Usage
583
187
 
584
- ### Credential Rotation Service
188
+ @Controller('security')
189
+ export class SecurityController {
190
+ constructor(private readonly securityService: SecurityService) {}
585
191
 
586
- ```typescript
587
- @Injectable()
588
- export class CredentialRotationService {
589
- private rotationSchedules = new Map<string, NodeJS.Timeout>();
590
-
591
- constructor(
592
- private stsService: StsService,
593
- private sessionService: SessionManagementService
594
- ) {}
595
-
596
- async scheduleCredentialRotation(
597
- sessionName: string,
598
- rotationIntervalMinutes: number = 50,
599
- sessionDurationSeconds: number = 3600
600
- ) {
601
- // Clear existing rotation if any
602
- this.clearRotationSchedule(sessionName);
603
-
604
- const rotationInterval = rotationIntervalMinutes * 60 * 1000;
605
-
606
- const timer = setInterval(async () => {
607
- try {
608
- console.log(`Rotating credentials for session: ${sessionName}`);
609
- await this.sessionService.refreshSession(sessionName, sessionDurationSeconds);
610
- console.log(`Successfully rotated credentials for session: ${sessionName}`);
611
- } catch (error) {
612
- console.error(`Failed to rotate credentials for session ${sessionName}:`, error);
613
- }
614
- }, rotationInterval);
615
-
616
- this.rotationSchedules.set(sessionName, timer);
617
-
618
- console.log(`Scheduled credential rotation for ${sessionName} every ${rotationIntervalMinutes} minutes`);
192
+ @Get('account')
193
+ async getAccountInfo() {
194
+ const accountId = await this.securityService.stsService.getAccountId();
195
+ return { accountId };
619
196
  }
620
197
 
621
- clearRotationSchedule(sessionName: string) {
622
- const existingTimer = this.rotationSchedules.get(sessionName);
623
- if (existingTimer) {
624
- clearInterval(existingTimer);
625
- this.rotationSchedules.delete(sessionName);
626
- }
198
+ @Get('validate')
199
+ async validateCredentials() {
200
+ return await this.securityService.validateCurrentCredentials();
627
201
  }
628
202
 
629
- clearAllRotationSchedules() {
630
- for (const [sessionName, timer] of this.rotationSchedules.entries()) {
631
- clearInterval(timer);
632
- }
633
- this.rotationSchedules.clear();
634
- }
635
-
636
- onModuleDestroy() {
637
- this.clearAllRotationSchedules();
638
- }
639
- }
640
- ```
641
-
642
- ### Multi-Account Manager
643
-
644
- ```typescript
645
- @Injectable()
646
- export class MultiAccountManagerService {
647
- constructor(
648
- private stsService: StsService,
649
- private crossAccountService: CrossAccountService
650
- ) {}
651
-
652
- async assumeRoleInMultipleAccounts(
653
- accounts: Array<{ accountId: string; roleName: string; externalId?: string }>,
654
- sessionDuration: number = 3600
655
- ) {
656
- const results = await Promise.allSettled(
657
- accounts.map(account =>
658
- this.crossAccountService.accessCrossAccountResource(
659
- account.accountId,
660
- account.roleName,
661
- account.externalId,
662
- sessionDuration
663
- )
664
- )
203
+ @Post('assume-role')
204
+ async assumeRole(@Body() body: {
205
+ targetAccountId: string;
206
+ roleName: string;
207
+ }) {
208
+ return await this.securityService.getCrossAccountCredentials(
209
+ body.targetAccountId,
210
+ body.roleName
665
211
  );
666
-
667
- return accounts.map((account, index) => ({
668
- accountId: account.accountId,
669
- roleName: account.roleName,
670
- success: results[index].status === 'fulfilled',
671
- ...(results[index].status === 'fulfilled'
672
- ? { credentials: (results[index] as PromiseFulfilledResult<any>).value }
673
- : { error: (results[index] as PromiseRejectedResult).reason.message }
674
- )
675
- }));
676
- }
677
-
678
- async getAccountInventory() {
679
- const currentAccount = await this.stsService.getAccountId();
680
- const identity = await this.stsService.stsClient.send(new GetCallerIdentityCommand({}));
681
-
682
- return {
683
- currentAccount,
684
- currentIdentity: {
685
- arn: identity.Arn!,
686
- userId: identity.UserId!,
687
- type: this.getIdentityType(identity.Arn!)
688
- },
689
- timestamp: new Date().toISOString()
690
- };
691
- }
692
-
693
- private getIdentityType(arn: string): string {
694
- if (arn.includes(':user/')) return 'IAM User';
695
- if (arn.includes(':role/')) return 'IAM Role';
696
- if (arn.includes(':assumed-role/')) return 'Assumed Role';
697
- return 'Unknown';
698
212
  }
699
213
  }
700
214
  ```
701
215
 
702
- ## Types and Interfaces
703
-
704
- ```typescript
705
- interface SessionInfo {
706
- sessionName: string;
707
- credentials: {
708
- accessKeyId: string;
709
- secretAccessKey: string;
710
- sessionToken: string;
711
- };
712
- expiration: Date;
713
- createdAt: Date;
714
- mfaUsed: boolean;
715
- }
716
-
717
- interface AWSCredentials {
718
- accessKeyId: string;
719
- secretAccessKey: string;
720
- sessionToken?: string;
721
- expiration?: Date;
722
- }
723
-
724
- interface ValidationResult {
725
- valid: boolean;
726
- identity?: {
727
- account: string;
728
- arn: string;
729
- userId: string;
730
- };
731
- error?: string;
732
- errorCode?: string;
733
- expiresAt?: Date;
734
- }
735
-
736
- interface ExpirationCheck {
737
- isExpired: boolean;
738
- isNearExpiration: boolean;
739
- remainingMinutes: number;
740
- expiration: Date;
741
- }
742
-
743
- interface IdentityProvider {
744
- type: 'SAML' | 'OIDC';
745
- providerId?: string;
746
- principalArn?: string;
747
- }
748
- ```
216
+ ## Environment Variables
749
217
 
750
- ## Best Practices
218
+ ```bash
219
+ # Required
220
+ AWS_REGION=us-east-1
751
221
 
752
- ### 1. Session Duration Management
222
+ # Optional
223
+ AWS_PROFILE=my-profile
753
224
 
754
- ```typescript
755
- // Use appropriate session durations
756
- const shortSession = 900; // 15 minutes for high-privilege operations
757
- const normalSession = 3600; // 1 hour for regular operations
758
- const longSession = 43200; // 12 hours for batch jobs (max)
225
+ # For cross-account access
226
+ EXTERNAL_ID=unique-external-id # If required by role trust policy
759
227
  ```
760
228
 
761
- ### 2. Error Handling
229
+ ## Common Use Cases
762
230
 
231
+ ### 1. Account Verification
763
232
  ```typescript
764
- async safeAssumeRole(roleArn: string, sessionName: string): Promise<any | null> {
765
- try {
766
- return await this.assumeRole(roleArn, sessionName);
767
- } catch (error: any) {
768
- if (error.name === 'AccessDenied') {
769
- console.error('Insufficient permissions to assume role');
770
- } else if (error.name === 'InvalidParameterValue') {
771
- console.error('Invalid role ARN or session name');
772
- }
773
- return null;
774
- }
233
+ const accountId = await stsService.getAccountId();
234
+ if (accountId !== expectedAccountId) {
235
+ throw new Error('Running in wrong AWS account');
775
236
  }
776
237
  ```
777
238
 
778
- ### 3. Security Best Practices
779
-
239
+ ### 2. Dynamic Resource ARN Construction
780
240
  ```typescript
781
- // Always use external IDs for cross-account access
782
- // Rotate credentials regularly
783
- // Use MFA when possible
784
- // Monitor session usage
241
+ const accountId = await stsService.getAccountId();
242
+ const bucketArn = `arn:aws:s3:::my-bucket-${accountId}`;
785
243
  ```
786
244
 
787
- ## Testing
788
-
245
+ ### 3. Cross-Account Access Setup
789
246
  ```typescript
790
- import { Test, TestingModule } from '@nestjs/testing';
791
- import { ServerAwsStsModule, StsService } from '@onivoro/server-aws-sts';
792
-
793
- describe('StsService', () => {
794
- let service: StsService;
795
-
796
- beforeEach(async () => {
797
- const module: TestingModule = await Test.createTestingModule({
798
- imports: [ServerAwsStsModule.configure({
799
- AWS_REGION: 'us-east-1',
800
- AWS_PROFILE: 'test'
801
- })],
802
- }).compile();
803
-
804
- service = module.get<StsService>(StsService);
805
- });
806
-
807
- it('should be defined', () => {
808
- expect(service).toBeDefined();
809
- });
810
-
811
- it('should get account ID', async () => {
812
- const accountId = await service.getAccountId();
813
- expect(accountId).toBeDefined();
814
- expect(typeof accountId).toBe('string');
815
- });
247
+ // Use the exposed stsClient for assume role operations
248
+ const assumeRoleCommand = new AssumeRoleCommand({
249
+ RoleArn: `arn:aws:iam::${targetAccount}:role/${roleName}`,
250
+ RoleSessionName: 'my-session'
816
251
  });
252
+ const credentials = await stsService.stsClient.send(assumeRoleCommand);
817
253
  ```
818
254
 
819
- ## API Reference
255
+ ## Limitations
256
+
257
+ - Only provides `getAccountId()` method out of the box
258
+ - No built-in support for role assumption or session tokens
259
+ - No credential caching or management
260
+ - For advanced STS operations, use the exposed `stsClient` directly
820
261
 
821
- ### Exported Classes
822
- - `ServerAwsStsConfig`: Configuration class for STS settings
823
- - `ServerAwsStsModule`: NestJS module for STS integration
262
+ ## Best Practices
824
263
 
825
- ### Exported Services
826
- - `StsService`: Main STS service with identity verification and account management capabilities
264
+ 1. **Credential Validation**: Use `getAccountId()` to verify you're in the correct AWS account
265
+ 2. **Role Names**: Use descriptive role session names for audit trails
266
+ 3. **Token Duration**: Request only the minimum token duration needed
267
+ 4. **Error Handling**: Always handle STS errors appropriately
268
+ 5. **Security**: Never log or expose temporary credentials
827
269
 
828
270
  ## License
829
271
 
830
- This library is licensed under the MIT License. See the LICENSE file in this package for details.
272
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onivoro/server-aws-sts",
3
- "version": "24.30.12",
3
+ "version": "24.30.14",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -10,8 +10,8 @@
10
10
  "url": "https://github.com/onivoro/monorepo.git"
11
11
  },
12
12
  "dependencies": {
13
- "@onivoro/server-aws-credential-providers": "24.30.12",
14
- "@onivoro/server-common": "24.30.12",
13
+ "@onivoro/server-aws-credential-providers": "24.30.14",
14
+ "@onivoro/server-common": "24.30.14",
15
15
  "tslib": "^2.3.0"
16
16
  },
17
17
  "peerDependencies": {