@onivoro/server-aws-sts 24.30.12 → 24.30.13
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 +142 -700
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @onivoro/server-aws-sts
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
11
|
+
## Overview
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
The module uses environment-based configuration:
|
|
68
32
|
|
|
69
33
|
```typescript
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
105
|
-
constructor(private stsService: StsService) {}
|
|
51
|
+
export class AccountService {
|
|
52
|
+
constructor(private readonly stsService: StsService) {}
|
|
106
53
|
|
|
107
|
-
async
|
|
54
|
+
async getCurrentAccountId() {
|
|
108
55
|
const accountId = await this.stsService.getAccountId();
|
|
109
|
-
|
|
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
|
-
##
|
|
62
|
+
## Available Method
|
|
130
63
|
|
|
131
|
-
|
|
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 {
|
|
73
|
+
import {
|
|
74
|
+
AssumeRoleCommand,
|
|
75
|
+
GetSessionTokenCommand,
|
|
76
|
+
GetAccessKeyInfoCommand
|
|
77
|
+
} from '@aws-sdk/client-sts';
|
|
136
78
|
|
|
137
79
|
@Injectable()
|
|
138
|
-
export class
|
|
139
|
-
constructor(private stsService: StsService) {}
|
|
80
|
+
export class AdvancedStsService {
|
|
81
|
+
constructor(private readonly stsService: StsService) {}
|
|
140
82
|
|
|
141
|
-
|
|
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:
|
|
88
|
+
DurationSeconds: 3600 // 1 hour
|
|
146
89
|
});
|
|
147
|
-
|
|
148
|
-
const response = await this.stsService.stsClient.send(command);
|
|
149
90
|
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
100
|
+
|
|
101
|
+
const response = await this.stsService.stsClient.send(command);
|
|
102
|
+
return response.Credentials;
|
|
184
103
|
}
|
|
185
104
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
128
|
+
## Complete Example
|
|
231
129
|
|
|
232
130
|
```typescript
|
|
233
|
-
import {
|
|
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
|
|
237
|
-
constructor(private stsService: StsService) {}
|
|
238
|
-
|
|
239
|
-
async
|
|
240
|
-
|
|
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:
|
|
252
|
-
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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('
|
|
306
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
538
|
-
account: identity.Account!,
|
|
539
|
-
arn: identity.Arn!,
|
|
540
|
-
userId: identity.UserId!
|
|
541
|
-
},
|
|
542
|
-
expiresAt: credentials.expiration
|
|
177
|
+
accountId
|
|
543
178
|
};
|
|
544
|
-
} catch (error
|
|
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
|
-
|
|
188
|
+
@Controller('security')
|
|
189
|
+
export class SecurityController {
|
|
190
|
+
constructor(private readonly securityService: SecurityService) {}
|
|
585
191
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
218
|
+
```bash
|
|
219
|
+
# Required
|
|
220
|
+
AWS_REGION=us-east-1
|
|
751
221
|
|
|
752
|
-
|
|
222
|
+
# Optional
|
|
223
|
+
AWS_PROFILE=my-profile
|
|
753
224
|
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
229
|
+
## Common Use Cases
|
|
762
230
|
|
|
231
|
+
### 1. Account Verification
|
|
763
232
|
```typescript
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
###
|
|
779
|
-
|
|
239
|
+
### 2. Dynamic Resource ARN Construction
|
|
780
240
|
```typescript
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
788
|
-
|
|
245
|
+
### 3. Cross-Account Access Setup
|
|
789
246
|
```typescript
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
822
|
-
- `ServerAwsStsConfig`: Configuration class for STS settings
|
|
823
|
-
- `ServerAwsStsModule`: NestJS module for STS integration
|
|
262
|
+
## Best Practices
|
|
824
263
|
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
272
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onivoro/server-aws-sts",
|
|
3
|
-
"version": "24.30.
|
|
3
|
+
"version": "24.30.13",
|
|
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.
|
|
14
|
-
"@onivoro/server-common": "24.30.
|
|
13
|
+
"@onivoro/server-aws-credential-providers": "24.30.13",
|
|
14
|
+
"@onivoro/server-common": "24.30.13",
|
|
15
15
|
"tslib": "^2.3.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|