Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/guides/credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,55 @@ serverless deploy --aws-profile devProfile

To use web identity token authentication the `AWS_WEB_IDENTITY_TOKEN_FILE` and `AWS_ROLE_ARN` environment need to be set. It is automatically set if you specify a service account in AWS EKS.

##### Using AWS SSO (Single Sign-On)

**Note: SSO support requires enabling AWS SDK v3 mode by setting the `SLS_AWS_SDK_V3=1` environment variable.**

AWS SSO (now AWS IAM Identity Center) provides a centralized way to manage access to multiple AWS accounts. The Serverless Framework supports SSO authentication when using AWS SDK v3 mode.

To use AWS SSO:

1. **Configure your SSO profile** (if not already done):
```bash
aws configure sso
```

2. **Login to your SSO session**:
```bash
aws sso login --profile your-sso-profile
```

3. **Enable SDK v3 mode and deploy**:
```bash
export SLS_AWS_SDK_V3=1
serverless deploy --aws-profile your-sso-profile
```

The framework supports both legacy SSO configuration and the newer SSO session format in `~/.aws/config`:

```ini
# Legacy format
[profile my-sso-profile]
sso_start_url = https://example.awsapps.com/start
sso_region = us-east-1
sso_account_id = 123456789012
sso_role_name = DeveloperRole
region = us-west-2

# New session format
[profile my-session-profile]
sso_session = my-session
sso_account_id = 123456789012
sso_role_name = DeveloperRole
region = us-west-2

[sso-session my-session]
sso_start_url = https://example.awsapps.com/start
sso_region = us-east-1
```

When your SSO session expires, the framework will provide a helpful error message with instructions to re-authenticate.

#### Per Stage Profiles

As an advanced use-case, you can deploy different stages to different accounts by using different profiles per stage. In order to use different profiles per stage, you must leverage [variables](./variables.md) and the provider profile setting.
Expand Down
3 changes: 3 additions & 0 deletions lib/plugins/aws/deploy/lib/check-for-changes.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ module.exports = {
);
}
})();
// SDK v3 doesn't include Contents property when there are no objects
if (!result.Contents) return [];

const objects = result.Contents.filter(({ Key: key }) =>
isDeploymentDirToken(key.split('/')[3])
);
Expand Down
102 changes: 91 additions & 11 deletions lib/plugins/aws/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const AWSClientFactory = require('../../aws/client-factory');
const { createCommand } = require('../../aws/commands');
const { buildClientConfig, shouldUseS3Acceleration } = require('../../aws/config');
const { transformV3Error } = require('../../aws/error-utils');
const { fromNodeProviderChain } = require('@aws-sdk/credential-providers');

const isLambdaArn = RegExp.prototype.test.bind(/^arn:[^:]+:lambda:/);
const isEcrUri = RegExp.prototype.test.bind(
Expand Down Expand Up @@ -1773,6 +1774,27 @@ class AwsProvider {

return await this.clientFactory.send(service, command, clientConfig);
} catch (error) {
// Enhanced error handling for SSO-specific issues
if (error.message && error.message.includes('SSO')) {
const profile = this._getActiveProfile();
if (error.message.includes('expired') || error.message.includes('Token has expired')) {
throw new ServerlessError(
`AWS SSO session has expired for profile "${profile || 'default'}". Please run:\n\n` +
` aws sso login${profile ? ` --profile ${profile}` : ''}\n\n` +
'to refresh your SSO credentials.',
'AWS_SSO_SESSION_EXPIRED'
);
}
if (error.message.includes('No SSO session') || error.message.includes('not found')) {
throw new ServerlessError(
`No AWS SSO session found for profile "${profile || 'default'}". Please run:\n\n` +
` aws sso login${profile ? ` --profile ${profile}` : ''}\n\n` +
'to authenticate with AWS SSO.',
'AWS_SSO_SESSION_NOT_FOUND'
);
}
}

// Transform v3 error to be compatible with existing error handling
throw transformV3Error(error);
}
Expand All @@ -1793,25 +1815,58 @@ class AwsProvider {
return this.clientFactory.getClient(service, clientConfig);
}

/**
* Get the active AWS profile name based on precedence
* @private
*/
_getActiveProfile() {
// Check CLI option first (highest priority)
if (this.options['aws-profile']) {
return this.options['aws-profile'];
}

// Check stage-specific environment variable
const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;
if (stageUpper && process.env[`AWS_${stageUpper}_PROFILE`]) {
return process.env[`AWS_${stageUpper}_PROFILE`];
}

// Check general AWS_PROFILE
if (process.env.AWS_PROFILE) {
return process.env.AWS_PROFILE;
}

// Check serverless.yml provider.profile
if (this.serverless.service.provider.profile) {
return this.serverless.service.provider.profile;
}

// Check AWS_DEFAULT_PROFILE or fall back to 'default'
return process.env.AWS_DEFAULT_PROFILE || undefined;
}

/**
* Build base configuration for AWS SDK v3 clients
* @private
*/
_getV3BaseConfig() {
// Convert v2 credentials format to v3 format
const { credentials: v2Creds } = this.getCredentials();
const credentials =
v2Creds && v2Creds.accessKeyId
? {
accessKeyId: v2Creds.accessKeyId,
secretAccessKey: v2Creds.secretAccessKey,
sessionToken: v2Creds.sessionToken,
}
: undefined;
// For SDK v3, we'll use the credential provider chain which handles SSO
const profile = this._getActiveProfile();

// Use fromNodeProviderChain which automatically handles:
// - SSO profiles
// - Environment variables
// - IAM roles
// - Container credentials
// - Instance metadata
const credentialProvider = fromNodeProviderChain({
profile,
clientConfig: { region: this.getRegion() },
});

return buildClientConfig({
region: this.getRegion(),
credentials,
credentials: credentialProvider,
});
}

Expand Down Expand Up @@ -1854,6 +1909,31 @@ class AwsProvider {
const result = {};
const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;

// For v3 SDK with SSO support, we use credential providers instead
// This maintains backward compatibility while enabling SSO
if (this._v3Enabled) {
// For v3, we return mock credentials to satisfy existing code expectations
// The actual credentials will be resolved by fromNodeProviderChain when needed
result.credentials = {
accessKeyId: 'SSO_ACCESS_KEY_ID', // Placeholder for SSO credentials
secretAccessKey: 'SSO_SECRET_ACCESS_KEY',
sessionToken: 'SSO_SESSION_TOKEN',
};

const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
if (
deploymentBucketObject &&
deploymentBucketObject.serverSideEncryption &&
deploymentBucketObject.serverSideEncryption === 'aws:kms'
) {
result.signatureVersion = 'v4';
}

this.cachedCredentials = result;
return result;
}

// Original v2 credential resolution logic
// add specified credentials, overriding with more specific declarations
const awsDefaultProfile = process.env.AWS_DEFAULT_PROFILE || 'default';
try {
Expand Down
2 changes: 1 addition & 1 deletion lib/plugins/aws/remove/lib/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports = {
throw err;
}

if (result) {
if (result && result.Contents) {
result.Contents.forEach((object) => {
this.objectsInBucket.push({
Key: object.Key,
Expand Down
3 changes: 2 additions & 1 deletion lib/plugins/aws/utils/find-and-group-deployments.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
const _ = require('lodash');

module.exports = (s3Response, prefix, service, stage) => {
if (s3Response.Contents.length) {
// SDK v3 doesn't include Contents property when there are no objects
if (s3Response.Contents && s3Response.Contents.length) {
const regex = new RegExp(`${prefix}/${service}/${stage}/(.+-.+-.+-.+)/(.+)`);
const s3Objects = s3Response.Contents.filter((s3Object) => s3Object.Key.match(regex));
const names = s3Objects.map((s3Object) => {
Expand Down
Loading