@onivoro/server-aws-credential-providers 24.0.0
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 +592 -0
- package/jest.config.ts +11 -0
- package/package.json +10 -0
- package/project.json +23 -0
- package/src/index.ts +4 -0
- package/src/lib/aws-credentials.class.ts +4 -0
- package/src/lib/resolve-aws-credential-providers-by-profile.function.ts +43 -0
- package/src/lib/server-aws-credential-providers-config.class.ts +3 -0
- package/src/lib/server-aws-credential-providers.module.ts +21 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# @onivoro/server-aws-credential-providers
|
|
2
|
+
|
|
3
|
+
A NestJS module for managing AWS credential providers with support for multiple AWS profiles, credential resolution, and secure credential management for server-side applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @onivoro/server-aws-credential-providers
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Multi-Profile Support**: Manage multiple AWS profiles (dev, staging, production)
|
|
14
|
+
- **Credential Resolution**: Automatic credential resolution from various sources
|
|
15
|
+
- **Profile-Based Configuration**: Profile-specific credential providers
|
|
16
|
+
- **Environment Integration**: Seamless integration with AWS environment configurations
|
|
17
|
+
- **Secure Credential Management**: Safe handling of AWS credentials
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Module Configuration
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { ServerAwsCredentialProvidersModule } from '@onivoro/server-aws-credential-providers';
|
|
25
|
+
|
|
26
|
+
@Module({
|
|
27
|
+
imports: [
|
|
28
|
+
ServerAwsCredentialProvidersModule.forRoot({
|
|
29
|
+
profiles: {
|
|
30
|
+
development: {
|
|
31
|
+
region: 'us-east-1',
|
|
32
|
+
accessKeyId: process.env.AWS_DEV_ACCESS_KEY_ID,
|
|
33
|
+
secretAccessKey: process.env.AWS_DEV_SECRET_ACCESS_KEY,
|
|
34
|
+
},
|
|
35
|
+
production: {
|
|
36
|
+
region: 'us-west-2',
|
|
37
|
+
accessKeyId: process.env.AWS_PROD_ACCESS_KEY_ID,
|
|
38
|
+
secretAccessKey: process.env.AWS_PROD_SECRET_ACCESS_KEY,
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
defaultProfile: 'development'
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
export class AppModule {}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Using AWS Credentials
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { AwsCredentials } from '@onivoro/server-aws-credential-providers';
|
|
52
|
+
|
|
53
|
+
@Injectable()
|
|
54
|
+
export class S3Service {
|
|
55
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
56
|
+
|
|
57
|
+
async getS3Client(profile: string = 'development') {
|
|
58
|
+
const credentials = await this.awsCredentials.getCredentials(profile);
|
|
59
|
+
|
|
60
|
+
return new S3Client({
|
|
61
|
+
region: credentials.region,
|
|
62
|
+
credentials: {
|
|
63
|
+
accessKeyId: credentials.accessKeyId,
|
|
64
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
65
|
+
sessionToken: credentials.sessionToken,
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
### ServerAwsCredentialProvidersConfig
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { ServerAwsCredentialProvidersConfig } from '@onivoro/server-aws-credential-providers';
|
|
78
|
+
|
|
79
|
+
export class AppAwsCredentialsConfig extends ServerAwsCredentialProvidersConfig {
|
|
80
|
+
profiles = {
|
|
81
|
+
development: {
|
|
82
|
+
region: process.env.AWS_DEV_REGION || 'us-east-1',
|
|
83
|
+
accessKeyId: process.env.AWS_DEV_ACCESS_KEY_ID,
|
|
84
|
+
secretAccessKey: process.env.AWS_DEV_SECRET_ACCESS_KEY,
|
|
85
|
+
roleArn: process.env.AWS_DEV_ROLE_ARN,
|
|
86
|
+
},
|
|
87
|
+
staging: {
|
|
88
|
+
region: process.env.AWS_STAGING_REGION || 'us-east-1',
|
|
89
|
+
accessKeyId: process.env.AWS_STAGING_ACCESS_KEY_ID,
|
|
90
|
+
secretAccessKey: process.env.AWS_STAGING_SECRET_ACCESS_KEY,
|
|
91
|
+
roleArn: process.env.AWS_STAGING_ROLE_ARN,
|
|
92
|
+
},
|
|
93
|
+
production: {
|
|
94
|
+
region: process.env.AWS_PROD_REGION || 'us-west-2',
|
|
95
|
+
accessKeyId: process.env.AWS_PROD_ACCESS_KEY_ID,
|
|
96
|
+
secretAccessKey: process.env.AWS_PROD_SECRET_ACCESS_KEY,
|
|
97
|
+
roleArn: process.env.AWS_PROD_ROLE_ARN,
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
defaultProfile = process.env.AWS_DEFAULT_PROFILE || 'development';
|
|
101
|
+
credentialProviderTimeout = parseInt(process.env.AWS_CREDENTIAL_TIMEOUT) || 30000;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Environment Variables
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Development Profile
|
|
109
|
+
AWS_DEV_REGION=us-east-1
|
|
110
|
+
AWS_DEV_ACCESS_KEY_ID=your-dev-access-key
|
|
111
|
+
AWS_DEV_SECRET_ACCESS_KEY=your-dev-secret-key
|
|
112
|
+
AWS_DEV_ROLE_ARN=arn:aws:iam::123456789012:role/DevRole
|
|
113
|
+
|
|
114
|
+
# Staging Profile
|
|
115
|
+
AWS_STAGING_REGION=us-east-1
|
|
116
|
+
AWS_STAGING_ACCESS_KEY_ID=your-staging-access-key
|
|
117
|
+
AWS_STAGING_SECRET_ACCESS_KEY=your-staging-secret-key
|
|
118
|
+
AWS_STAGING_ROLE_ARN=arn:aws:iam::123456789012:role/StagingRole
|
|
119
|
+
|
|
120
|
+
# Production Profile
|
|
121
|
+
AWS_PROD_REGION=us-west-2
|
|
122
|
+
AWS_PROD_ACCESS_KEY_ID=your-prod-access-key
|
|
123
|
+
AWS_PROD_SECRET_ACCESS_KEY=your-prod-secret-key
|
|
124
|
+
AWS_PROD_ROLE_ARN=arn:aws:iam::123456789012:role/ProdRole
|
|
125
|
+
|
|
126
|
+
# Default Settings
|
|
127
|
+
AWS_DEFAULT_PROFILE=development
|
|
128
|
+
AWS_CREDENTIAL_TIMEOUT=30000
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Core Classes
|
|
132
|
+
|
|
133
|
+
### AwsCredentials
|
|
134
|
+
|
|
135
|
+
Main service for managing AWS credentials:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { AwsCredentials } from '@onivoro/server-aws-credential-providers';
|
|
139
|
+
|
|
140
|
+
@Injectable()
|
|
141
|
+
export class CloudService {
|
|
142
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
143
|
+
|
|
144
|
+
async getDynamoDBClient(profile?: string) {
|
|
145
|
+
const credentials = await this.awsCredentials.getCredentials(profile);
|
|
146
|
+
|
|
147
|
+
return new DynamoDBClient({
|
|
148
|
+
region: credentials.region,
|
|
149
|
+
credentials: {
|
|
150
|
+
accessKeyId: credentials.accessKeyId,
|
|
151
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
152
|
+
sessionToken: credentials.sessionToken,
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async assumeRole(roleArn: string, sessionName: string, profile?: string) {
|
|
158
|
+
return this.awsCredentials.assumeRole({
|
|
159
|
+
roleArn,
|
|
160
|
+
sessionName,
|
|
161
|
+
profile
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getCredentialsForProfile(profileName: string) {
|
|
166
|
+
return this.awsCredentials.getCredentials(profileName);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async refreshCredentials(profile?: string) {
|
|
170
|
+
return this.awsCredentials.refreshCredentials(profile);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Utility Functions
|
|
176
|
+
|
|
177
|
+
### resolveAwsCredentialProvidersByProfile
|
|
178
|
+
|
|
179
|
+
Resolves credential providers for a specific profile:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { resolveAwsCredentialProvidersByProfile } from '@onivoro/server-aws-credential-providers';
|
|
183
|
+
|
|
184
|
+
// Resolve credentials for a specific profile
|
|
185
|
+
const credentialProvider = await resolveAwsCredentialProvidersByProfile('production');
|
|
186
|
+
|
|
187
|
+
// Use with AWS SDK clients
|
|
188
|
+
const s3Client = new S3Client({
|
|
189
|
+
region: 'us-west-2',
|
|
190
|
+
credentials: credentialProvider
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Advanced Usage
|
|
195
|
+
|
|
196
|
+
### Multi-Environment Service
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
@Injectable()
|
|
200
|
+
export class MultiEnvironmentService {
|
|
201
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
202
|
+
|
|
203
|
+
async deployToMultipleEnvironments(deploymentConfig: any) {
|
|
204
|
+
const environments = ['development', 'staging', 'production'];
|
|
205
|
+
const results = [];
|
|
206
|
+
|
|
207
|
+
for (const env of environments) {
|
|
208
|
+
try {
|
|
209
|
+
console.log(`Deploying to ${env}...`);
|
|
210
|
+
|
|
211
|
+
const credentials = await this.awsCredentials.getCredentials(env);
|
|
212
|
+
const client = this.createClientForEnvironment(env, credentials);
|
|
213
|
+
|
|
214
|
+
const result = await this.performDeployment(client, deploymentConfig);
|
|
215
|
+
results.push({ environment: env, success: true, result });
|
|
216
|
+
|
|
217
|
+
console.log(`✅ Successfully deployed to ${env}`);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(`❌ Failed to deploy to ${env}:`, error.message);
|
|
220
|
+
results.push({
|
|
221
|
+
environment: env,
|
|
222
|
+
success: false,
|
|
223
|
+
error: error.message
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private createClientForEnvironment(environment: string, credentials: any) {
|
|
232
|
+
// Create appropriate AWS service client based on environment
|
|
233
|
+
switch (environment) {
|
|
234
|
+
case 'development':
|
|
235
|
+
return new S3Client({ region: 'us-east-1', credentials });
|
|
236
|
+
case 'staging':
|
|
237
|
+
return new S3Client({ region: 'us-east-1', credentials });
|
|
238
|
+
case 'production':
|
|
239
|
+
return new S3Client({ region: 'us-west-2', credentials });
|
|
240
|
+
default:
|
|
241
|
+
throw new Error(`Unknown environment: ${environment}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private async performDeployment(client: any, config: any) {
|
|
246
|
+
// Deployment logic here
|
|
247
|
+
return { deploymentId: `deploy-${Date.now()}` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Credential Caching Service
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
@Injectable()
|
|
256
|
+
export class CredentialCacheService {
|
|
257
|
+
private credentialCache = new Map<string, { credentials: any; expiry: Date }>();
|
|
258
|
+
private readonly CACHE_DURATION = 50 * 60 * 1000; // 50 minutes
|
|
259
|
+
|
|
260
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
261
|
+
|
|
262
|
+
async getCachedCredentials(profile: string = 'default') {
|
|
263
|
+
const cacheKey = `credentials-${profile}`;
|
|
264
|
+
const cached = this.credentialCache.get(cacheKey);
|
|
265
|
+
|
|
266
|
+
if (cached && cached.expiry > new Date()) {
|
|
267
|
+
console.log(`Using cached credentials for profile: ${profile}`);
|
|
268
|
+
return cached.credentials;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(`Fetching fresh credentials for profile: ${profile}`);
|
|
272
|
+
const credentials = await this.awsCredentials.getCredentials(profile);
|
|
273
|
+
|
|
274
|
+
// Cache the credentials
|
|
275
|
+
this.credentialCache.set(cacheKey, {
|
|
276
|
+
credentials,
|
|
277
|
+
expiry: new Date(Date.now() + this.CACHE_DURATION)
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return credentials;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
clearCache(profile?: string) {
|
|
284
|
+
if (profile) {
|
|
285
|
+
this.credentialCache.delete(`credentials-${profile}`);
|
|
286
|
+
} else {
|
|
287
|
+
this.credentialCache.clear();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
getCacheStatus() {
|
|
292
|
+
const status = Array.from(this.credentialCache.entries()).map(([key, value]) => ({
|
|
293
|
+
profile: key.replace('credentials-', ''),
|
|
294
|
+
hasCredentials: !!value.credentials,
|
|
295
|
+
expiresAt: value.expiry,
|
|
296
|
+
isExpired: value.expiry <= new Date()
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
return status;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Cross-Account Access Service
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
@Injectable()
|
|
308
|
+
export class CrossAccountService {
|
|
309
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
310
|
+
|
|
311
|
+
async accessCrossAccountResource(
|
|
312
|
+
targetAccountId: string,
|
|
313
|
+
roleName: string,
|
|
314
|
+
sourceProfile: string = 'production'
|
|
315
|
+
) {
|
|
316
|
+
const sourceCredentials = await this.awsCredentials.getCredentials(sourceProfile);
|
|
317
|
+
|
|
318
|
+
// Create STS client with source credentials
|
|
319
|
+
const stsClient = new STSClient({
|
|
320
|
+
region: sourceCredentials.region,
|
|
321
|
+
credentials: sourceCredentials
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Assume role in target account
|
|
325
|
+
const roleArn = `arn:aws:iam::${targetAccountId}:role/${roleName}`;
|
|
326
|
+
const assumeRoleCommand = new AssumeRoleCommand({
|
|
327
|
+
RoleArn: roleArn,
|
|
328
|
+
RoleSessionName: `cross-account-${Date.now()}`,
|
|
329
|
+
DurationSeconds: 3600 // 1 hour
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const assumeRoleResponse = await stsClient.send(assumeRoleCommand);
|
|
333
|
+
|
|
334
|
+
if (!assumeRoleResponse.Credentials) {
|
|
335
|
+
throw new Error('Failed to assume cross-account role');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
accessKeyId: assumeRoleResponse.Credentials.AccessKeyId!,
|
|
340
|
+
secretAccessKey: assumeRoleResponse.Credentials.SecretAccessKey!,
|
|
341
|
+
sessionToken: assumeRoleResponse.Credentials.SessionToken!,
|
|
342
|
+
expiration: assumeRoleResponse.Credentials.Expiration
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async createCrossAccountClient<T>(
|
|
347
|
+
clientClass: new (config: any) => T,
|
|
348
|
+
targetAccountId: string,
|
|
349
|
+
roleName: string,
|
|
350
|
+
region: string,
|
|
351
|
+
sourceProfile?: string
|
|
352
|
+
): Promise<T> {
|
|
353
|
+
const crossAccountCredentials = await this.accessCrossAccountResource(
|
|
354
|
+
targetAccountId,
|
|
355
|
+
roleName,
|
|
356
|
+
sourceProfile
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return new clientClass({
|
|
360
|
+
region,
|
|
361
|
+
credentials: {
|
|
362
|
+
accessKeyId: crossAccountCredentials.accessKeyId,
|
|
363
|
+
secretAccessKey: crossAccountCredentials.secretAccessKey,
|
|
364
|
+
sessionToken: crossAccountCredentials.sessionToken
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Profile Validation Service
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
@Injectable()
|
|
375
|
+
export class ProfileValidationService {
|
|
376
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
377
|
+
|
|
378
|
+
async validateAllProfiles() {
|
|
379
|
+
const config = this.awsCredentials.getConfiguration();
|
|
380
|
+
const results = [];
|
|
381
|
+
|
|
382
|
+
for (const [profileName, profileConfig] of Object.entries(config.profiles)) {
|
|
383
|
+
try {
|
|
384
|
+
console.log(`Validating profile: ${profileName}`);
|
|
385
|
+
|
|
386
|
+
const credentials = await this.awsCredentials.getCredentials(profileName);
|
|
387
|
+
|
|
388
|
+
// Test credentials by calling STS GetCallerIdentity
|
|
389
|
+
const stsClient = new STSClient({
|
|
390
|
+
region: profileConfig.region,
|
|
391
|
+
credentials
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const identity = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
395
|
+
|
|
396
|
+
results.push({
|
|
397
|
+
profile: profileName,
|
|
398
|
+
valid: true,
|
|
399
|
+
account: identity.Account,
|
|
400
|
+
arn: identity.Arn,
|
|
401
|
+
userId: identity.UserId
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
console.log(`✅ Profile ${profileName} is valid (Account: ${identity.Account})`);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(`❌ Profile ${profileName} validation failed:`, error.message);
|
|
407
|
+
results.push({
|
|
408
|
+
profile: profileName,
|
|
409
|
+
valid: false,
|
|
410
|
+
error: error.message
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return results;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async validateProfile(profileName: string) {
|
|
419
|
+
try {
|
|
420
|
+
const credentials = await this.awsCredentials.getCredentials(profileName);
|
|
421
|
+
const config = this.awsCredentials.getConfiguration();
|
|
422
|
+
const profileConfig = config.profiles[profileName];
|
|
423
|
+
|
|
424
|
+
if (!profileConfig) {
|
|
425
|
+
throw new Error(`Profile ${profileName} not found in configuration`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const stsClient = new STSClient({
|
|
429
|
+
region: profileConfig.region,
|
|
430
|
+
credentials
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const identity = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
profile: profileName,
|
|
437
|
+
valid: true,
|
|
438
|
+
account: identity.Account,
|
|
439
|
+
arn: identity.Arn,
|
|
440
|
+
userId: identity.UserId,
|
|
441
|
+
region: profileConfig.region
|
|
442
|
+
};
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return {
|
|
445
|
+
profile: profileName,
|
|
446
|
+
valid: false,
|
|
447
|
+
error: error.message
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Integration Examples
|
|
455
|
+
|
|
456
|
+
### Using with Other AWS Services
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
@Injectable()
|
|
460
|
+
export class IntegratedAwsService {
|
|
461
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
462
|
+
|
|
463
|
+
async createS3Service(profile?: string) {
|
|
464
|
+
const credentials = await this.awsCredentials.getCredentials(profile);
|
|
465
|
+
return new S3Service(new S3Client({
|
|
466
|
+
region: credentials.region,
|
|
467
|
+
credentials
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async createDynamoDBService(profile?: string) {
|
|
472
|
+
const credentials = await this.awsCredentials.getCredentials(profile);
|
|
473
|
+
return new DynamoDBService(new DynamoDBClient({
|
|
474
|
+
region: credentials.region,
|
|
475
|
+
credentials
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async createLambdaService(profile?: string) {
|
|
480
|
+
const credentials = await this.awsCredentials.getCredentials(profile);
|
|
481
|
+
return new LambdaService(new LambdaClient({
|
|
482
|
+
region: credentials.region,
|
|
483
|
+
credentials
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Best Practices
|
|
490
|
+
|
|
491
|
+
### 1. Environment-Specific Profiles
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const getProfileForEnvironment = (env: string) => {
|
|
495
|
+
switch (env) {
|
|
496
|
+
case 'development':
|
|
497
|
+
case 'dev':
|
|
498
|
+
return 'development';
|
|
499
|
+
case 'staging':
|
|
500
|
+
case 'test':
|
|
501
|
+
return 'staging';
|
|
502
|
+
case 'production':
|
|
503
|
+
case 'prod':
|
|
504
|
+
return 'production';
|
|
505
|
+
default:
|
|
506
|
+
return 'development';
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### 2. Credential Rotation Handling
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
@Injectable()
|
|
515
|
+
export class CredentialRotationService {
|
|
516
|
+
constructor(private awsCredentials: AwsCredentials) {}
|
|
517
|
+
|
|
518
|
+
async handleCredentialRotation(profile: string) {
|
|
519
|
+
try {
|
|
520
|
+
// Clear any cached credentials
|
|
521
|
+
await this.awsCredentials.refreshCredentials(profile);
|
|
522
|
+
|
|
523
|
+
// Verify new credentials work
|
|
524
|
+
const validation = await this.validateCredentials(profile);
|
|
525
|
+
|
|
526
|
+
if (!validation.valid) {
|
|
527
|
+
throw new Error(`New credentials for ${profile} are invalid`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
console.log(`✅ Credentials rotated successfully for ${profile}`);
|
|
531
|
+
return { success: true, profile, newIdentity: validation };
|
|
532
|
+
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error(`❌ Credential rotation failed for ${profile}:`, error.message);
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private async validateCredentials(profile: string) {
|
|
540
|
+
const credentials = await this.awsCredentials.getCredentials(profile);
|
|
541
|
+
// Validation logic here
|
|
542
|
+
return { valid: true };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## Testing
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
551
|
+
import { ServerAwsCredentialProvidersModule, AwsCredentials } from '@onivoro/server-aws-credential-providers';
|
|
552
|
+
|
|
553
|
+
describe('AwsCredentials', () => {
|
|
554
|
+
let service: AwsCredentials;
|
|
555
|
+
|
|
556
|
+
beforeEach(async () => {
|
|
557
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
558
|
+
imports: [ServerAwsCredentialProvidersModule.forRoot({
|
|
559
|
+
profiles: {
|
|
560
|
+
test: {
|
|
561
|
+
region: 'us-east-1',
|
|
562
|
+
accessKeyId: 'test-key',
|
|
563
|
+
secretAccessKey: 'test-secret'
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
})],
|
|
567
|
+
}).compile();
|
|
568
|
+
|
|
569
|
+
service = module.get<AwsCredentials>(AwsCredentials);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('should resolve credentials for profile', async () => {
|
|
573
|
+
const credentials = await service.getCredentials('test');
|
|
574
|
+
expect(credentials).toBeDefined();
|
|
575
|
+
expect(credentials.region).toBe('us-east-1');
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## API Reference
|
|
581
|
+
|
|
582
|
+
### Exported Classes
|
|
583
|
+
- `AwsCredentials`: Main credential management service
|
|
584
|
+
- `ServerAwsCredentialProvidersConfig`: Configuration class
|
|
585
|
+
- `ServerAwsCredentialProvidersModule`: NestJS module
|
|
586
|
+
|
|
587
|
+
### Exported Functions
|
|
588
|
+
- `resolveAwsCredentialProvidersByProfile`: Profile-specific credential resolution
|
|
589
|
+
|
|
590
|
+
## License
|
|
591
|
+
|
|
592
|
+
This package is part of the Onivoro monorepo and follows the same licensing terms.
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
displayName: 'lib-server-aws-credential-providers',
|
|
4
|
+
preset: '../../../jest.preset.js',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
|
8
|
+
},
|
|
9
|
+
moduleFileExtensions: ['ts', 'js', 'html'],
|
|
10
|
+
coverageDirectory: '../../../coverage/libs/server/aws-credential-providers',
|
|
11
|
+
};
|
package/package.json
ADDED
package/project.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lib-server-aws-credential-providers",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/server/aws-credential-providers/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"build": {
|
|
8
|
+
"executor": "@nx/js:tsc",
|
|
9
|
+
"outputs": ["{options.outputPath}"],
|
|
10
|
+
"options": {
|
|
11
|
+
"outputPath": "dist/libs/server/aws-credential-providers",
|
|
12
|
+
"main": "libs/server/aws-credential-providers/src/index.ts",
|
|
13
|
+
"tsConfig": "libs/server/aws-credential-providers/tsconfig.lib.json",
|
|
14
|
+
"assets": [
|
|
15
|
+
"libs/server/aws-credential-providers/README.md",
|
|
16
|
+
"libs/server/aws-credential-providers/package.json"
|
|
17
|
+
],
|
|
18
|
+
"declaration": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"tags": []
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fromIni } from "@aws-sdk/credential-providers";
|
|
2
|
+
import { AwsCredentials } from "./aws-credentials.class";
|
|
3
|
+
|
|
4
|
+
const AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID';
|
|
5
|
+
const AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';
|
|
6
|
+
|
|
7
|
+
export async function resolveAwsCredentialProvidersByProfile(profile?: string | undefined): Promise<AwsCredentials | undefined> {
|
|
8
|
+
if (profile) {
|
|
9
|
+
try {
|
|
10
|
+
console.warn(`attempting to use AWS profile "${profile}" for AWS authentication`);
|
|
11
|
+
|
|
12
|
+
const credentialResolver = fromIni({ profile });
|
|
13
|
+
|
|
14
|
+
return await credentialResolver();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.warn(`failed to load AWS profile "${profile}"... ensure that ${[AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY].map(_ => `"${_.toLowerCase()}"`).join(' and ')} are lowercase in your ~/.aws/credentials file`);
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
(process.env[AWS_ACCESS_KEY_ID] && process.env[AWS_SECRET_ACCESS_KEY])
|
|
20
|
+
) {
|
|
21
|
+
console.log(`using UPPERCASE AWS_* environment variables for AWS authentication`);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
accessKeyId: process.env[AWS_ACCESS_KEY_ID],
|
|
25
|
+
secretAccessKey: process.env[AWS_SECRET_ACCESS_KEY],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
(process.env[AWS_ACCESS_KEY_ID.toLowerCase()] && process.env[AWS_SECRET_ACCESS_KEY.toLowerCase()])
|
|
31
|
+
) {
|
|
32
|
+
console.log(`using lowercase aws_* environment variables for AWS authentication`);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
accessKeyId: process.env[AWS_ACCESS_KEY_ID.toLowerCase()]!,
|
|
36
|
+
secretAccessKey: process.env[AWS_SECRET_ACCESS_KEY.toLowerCase()]!,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ServerAwsCredentialProvidersConfig } from './server-aws-credential-providers-config.class';
|
|
3
|
+
import { resolveAwsCredentialProvidersByProfile } from './resolve-aws-credential-providers-by-profile.function';
|
|
4
|
+
import { AwsCredentials } from './aws-credentials.class';
|
|
5
|
+
|
|
6
|
+
@Module({})
|
|
7
|
+
export class ServerAwsCredentialProvidersModule {
|
|
8
|
+
static configure(config: ServerAwsCredentialProvidersConfig) {
|
|
9
|
+
return {
|
|
10
|
+
module: ServerAwsCredentialProvidersModule,
|
|
11
|
+
providers: [
|
|
12
|
+
{ provide: ServerAwsCredentialProvidersConfig, useValue: config },
|
|
13
|
+
{
|
|
14
|
+
provide: AwsCredentials,
|
|
15
|
+
useFactory: async () => await resolveAwsCredentialProvidersByProfile(config.AWS_PROFILE),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
exports: [AwsCredentials, ServerAwsCredentialProvidersConfig],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.server.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../../dist/out-tsc"
|
|
5
|
+
},
|
|
6
|
+
"files": [],
|
|
7
|
+
"include": [],
|
|
8
|
+
"references": [
|
|
9
|
+
{
|
|
10
|
+
"path": "./tsconfig.lib.json"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "./tsconfig.spec.json"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": [
|
|
5
|
+
"jest",
|
|
6
|
+
"node"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"jest.config.ts",
|
|
11
|
+
"**/*.test.ts",
|
|
12
|
+
"**/*.spec.ts",
|
|
13
|
+
"**/*.test.tsx",
|
|
14
|
+
"**/*.spec.tsx",
|
|
15
|
+
"**/*.test.js",
|
|
16
|
+
"**/*.spec.js",
|
|
17
|
+
"**/*.test.jsx",
|
|
18
|
+
"**/*.spec.jsx",
|
|
19
|
+
"**/*.d.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|