1// Package stscreds are credential Providers to retrieve STS AWS credentials.
2//
3// STS provides multiple ways to retrieve credentials which can be used when making
4// future AWS service API operation calls.
5//
6// The SDK will ensure that per instance of credentials.Credentials all requests
7// to refresh the credentials will be synchronized. But, the SDK is unable to
8// ensure synchronous usage of the AssumeRoleProvider if the value is shared
9// between multiple Credentials or service clients.
10//
11// Assume Role
12//
13// To assume an IAM role using STS with the SDK you can create a new Credentials
14// with the SDKs's stscreds package.
15//
16// 	// Initial credentials loaded from SDK's default credential chain. Such as
17// 	// the environment, shared credentials (~/.aws/credentials), or EC2 Instance
18// 	// Role. These credentials will be used to to make the STS Assume Role API.
19// 	cfg, err := config.LoadDefaultConfig(context.TODO())
20// 	if err != nil {
21// 		panic(err)
22// 	}
23//
24// 	// Create the credentials from AssumeRoleProvider to assume the role
25// 	// referenced by the "myRoleARN" ARN.
26// 	stsSvc := sts.NewFromConfig(cfg)
27// 	creds := stscreds.NewAssumeRoleProvider(stsSvc, "myRoleArn")
28//
29// 	cfg.Credentials = &aws.CredentialsCache{Provider: creds}
30//
31// 	// Create service client value configured for credentials
32// 	// from assumed role.
33// 	svc := s3.NewFromConfig(cfg)
34//
35// Assume Role with static MFA Token
36//
37// To assume an IAM role with a MFA token you can either specify a MFA token code
38// directly or provide a function to prompt the user each time the credentials
39// need to refresh the role's credentials. Specifying the TokenCode should be used
40// for short lived operations that will not need to be refreshed, and when you do
41// not want to have direct control over the user provides their MFA token.
42//
43// With TokenCode the AssumeRoleProvider will be not be able to refresh the role's
44// credentials.
45//
46// 	cfg, err := config.LoadDefaultConfig(context.TODO())
47// 	if err != nil {
48// 		panic(err)
49// 	}
50//
51// 	// Create the credentials from AssumeRoleProvider to assume the role
52// 	// referenced by the "myRoleARN" ARN using the MFA token code provided.
53// 	creds := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), "myRoleArn", func(o *stscreds.AssumeRoleOptions) {
54// 		o.SerialNumber = aws.String("myTokenSerialNumber")
55// 		o.TokenCode = aws.String("00000000")
56// 	})
57//
58// 	cfg.Credentials = &aws.CredentialsCache{Provider: creds}
59//
60// 	// Create service client value configured for credentials
61// 	// from assumed role.
62// 	svc := s3.NewFromConfig(cfg)
63//
64// Assume Role with MFA Token Provider
65//
66// To assume an IAM role with MFA for longer running tasks where the credentials
67// may need to be refreshed setting the TokenProvider field of AssumeRoleProvider
68// will allow the credential provider to prompt for new MFA token code when the
69// role's credentials need to be refreshed.
70//
71// The StdinTokenProvider function is available to prompt on stdin to retrieve
72// the MFA token code from the user. You can also implement custom prompts by
73// satisfying the TokenProvider function signature.
74//
75// Using StdinTokenProvider with multiple AssumeRoleProviders, or Credentials will
76// have undesirable results as the StdinTokenProvider will not be synchronized. A
77// single Credentials with an AssumeRoleProvider can be shared safely.
78//
79// 	cfg, err := config.LoadDefaultConfig(context.TODO())
80// 	if err != nil {
81// 		panic(err)
82// 	}
83//
84// 	// Create the credentials from AssumeRoleProvider to assume the role
85// 	// referenced by the "myRoleARN" ARN using the MFA token code provided.
86// 	creds := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), "myRoleArn", func(o *stscreds.AssumeRoleOptions) {
87// 		o.SerialNumber = aws.String("myTokenSerialNumber")
88// 		o.TokenProvider = stscreds.StdinTokenProvider
89// 	})
90//
91// 	cfg.Credentials = &aws.CredentialsCache{Provider: creds}
92//
93// 	// Create service client value configured for credentials
94// 	// from assumed role.
95// 	svc := s3.NewFromConfig(cfg)
96package stscreds
97
98import (
99	"context"
100	"fmt"
101	"time"
102
103	"github.com/aws/aws-sdk-go-v2/aws"
104	"github.com/aws/aws-sdk-go-v2/service/sts"
105	"github.com/aws/aws-sdk-go-v2/service/sts/types"
106)
107
108// StdinTokenProvider will prompt on stdout and read from stdin for a string value.
109// An error is returned if reading from stdin fails.
110//
111// Use this function go read MFA tokens from stdin. The function makes no attempt
112// to make atomic prompts from stdin across multiple gorouties.
113//
114// Using StdinTokenProvider with multiple AssumeRoleProviders, or Credentials will
115// have undesirable results as the StdinTokenProvider will not be synchronized. A
116// single Credentials with an AssumeRoleProvider can be shared safely
117//
118// Will wait forever until something is provided on the stdin.
119func StdinTokenProvider() (string, error) {
120	var v string
121	fmt.Printf("Assume Role MFA token code: ")
122	_, err := fmt.Scanln(&v)
123
124	return v, err
125}
126
127// ProviderName provides a name of AssumeRole provider
128const ProviderName = "AssumeRoleProvider"
129
130// AssumeRoleAPIClient is a client capable of the STS AssumeRole operation.
131type AssumeRoleAPIClient interface {
132	AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
133}
134
135// DefaultDuration is the default amount of time in minutes that the credentials
136// will be valid for.
137var DefaultDuration = time.Duration(15) * time.Minute
138
139// AssumeRoleProvider retrieves temporary credentials from the STS service, and
140// keeps track of their expiration time.
141//
142// This credential provider will be used by the SDKs default credential change
143// when shared configuration is enabled, and the shared config or shared credentials
144// file configure assume role. See Session docs for how to do this.
145//
146// AssumeRoleProvider does not provide any synchronization and it is not safe
147// to share this value across multiple Credentials, Sessions, or service clients
148// without also sharing the same Credentials instance.
149type AssumeRoleProvider struct {
150	options AssumeRoleOptions
151}
152
153// AssumeRoleOptions is the configurable options for AssumeRoleProvider
154type AssumeRoleOptions struct {
155	// Client implementation of the AssumeRole operation. Required
156	Client AssumeRoleAPIClient
157
158	// IAM Role ARN to be assumed. Required
159	RoleARN string
160
161	// Session name, if you wish to uniquely identify this session.
162	RoleSessionName string
163
164	// Expiry duration of the STS credentials. Defaults to 15 minutes if not set.
165	Duration time.Duration
166
167	// Optional ExternalID to pass along, defaults to nil if not set.
168	ExternalID *string
169
170	// The policy plain text must be 2048 bytes or shorter. However, an internal
171	// conversion compresses it into a packed binary format with a separate limit.
172	// The PackedPolicySize response element indicates by percentage how close to
173	// the upper size limit the policy is, with 100% equaling the maximum allowed
174	// size.
175	Policy *string
176
177	// The ARNs of IAM managed policies you want to use as managed session policies.
178	// The policies must exist in the same account as the role.
179	//
180	// This parameter is optional. You can provide up to 10 managed policy ARNs.
181	// However, the plain text that you use for both inline and managed session
182	// policies can't exceed 2,048 characters.
183	//
184	// An AWS conversion compresses the passed session policies and session tags
185	// into a packed binary format that has a separate limit. Your request can fail
186	// for this limit even if your plain text meets the other requirements. The
187	// PackedPolicySize response element indicates by percentage how close the policies
188	// and tags for your request are to the upper size limit.
189	//
190	// Passing policies to this operation returns new temporary credentials. The
191	// resulting session's permissions are the intersection of the role's identity-based
192	// policy and the session policies. You can use the role's temporary credentials
193	// in subsequent AWS API calls to access resources in the account that owns
194	// the role. You cannot use session policies to grant more permissions than
195	// those allowed by the identity-based policy of the role that is being assumed.
196	// For more information, see Session Policies (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#policies_session)
197	// in the IAM User Guide.
198	PolicyARNs []types.PolicyDescriptorType
199
200	// The identification number of the MFA device that is associated with the user
201	// who is making the AssumeRole call. Specify this value if the trust policy
202	// of the role being assumed includes a condition that requires MFA authentication.
203	// The value is either the serial number for a hardware device (such as GAHT12345678)
204	// or an Amazon Resource Name (ARN) for a virtual device (such as arn:aws:iam::123456789012:mfa/user).
205	SerialNumber *string
206
207	// Async method of providing MFA token code for assuming an IAM role with MFA.
208	// The value returned by the function will be used as the TokenCode in the Retrieve
209	// call. See StdinTokenProvider for a provider that prompts and reads from stdin.
210	//
211	// This token provider will be called when ever the assumed role's
212	// credentials need to be refreshed when SerialNumber is also set and
213	// TokenCode is not set.
214	//
215	// If both TokenCode and TokenProvider is set, TokenProvider will be used and
216	// TokenCode is ignored.
217	TokenProvider func() (string, error)
218}
219
220// NewAssumeRoleProvider constructs and returns a credentials provider that
221// will retrieve credentials by assuming a IAM role using STS.
222func NewAssumeRoleProvider(client AssumeRoleAPIClient, roleARN string, optFns ...func(*AssumeRoleOptions)) *AssumeRoleProvider {
223	o := AssumeRoleOptions{
224		Client:  client,
225		RoleARN: roleARN,
226	}
227
228	for _, fn := range optFns {
229		fn(&o)
230	}
231
232	return &AssumeRoleProvider{
233		options: o,
234	}
235}
236
237// Retrieve generates a new set of temporary credentials using STS.
238func (p *AssumeRoleProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
239	// Apply defaults where parameters are not set.
240	if len(p.options.RoleSessionName) == 0 {
241		// Try to work out a role name that will hopefully end up unique.
242		p.options.RoleSessionName = fmt.Sprintf("aws-go-sdk-%d", time.Now().UTC().UnixNano())
243	}
244	if p.options.Duration == 0 {
245		// Expire as often as AWS permits.
246		p.options.Duration = DefaultDuration
247	}
248	input := &sts.AssumeRoleInput{
249		DurationSeconds: aws.Int32(int32(p.options.Duration / time.Second)),
250		PolicyArns:      p.options.PolicyARNs,
251		RoleArn:         aws.String(p.options.RoleARN),
252		RoleSessionName: aws.String(p.options.RoleSessionName),
253		ExternalId:      p.options.ExternalID,
254	}
255	if p.options.Policy != nil {
256		input.Policy = p.options.Policy
257	}
258	if p.options.SerialNumber != nil {
259		if p.options.TokenProvider != nil {
260			input.SerialNumber = p.options.SerialNumber
261			code, err := p.options.TokenProvider()
262			if err != nil {
263				return aws.Credentials{}, err
264			}
265			input.TokenCode = aws.String(code)
266		} else {
267			return aws.Credentials{}, fmt.Errorf("assume role with MFA enabled, but neither TokenCode nor TokenProvider are set")
268		}
269	}
270
271	resp, err := p.options.Client.AssumeRole(ctx, input)
272	if err != nil {
273		return aws.Credentials{Source: ProviderName}, err
274	}
275
276	return aws.Credentials{
277		AccessKeyID:     *resp.Credentials.AccessKeyId,
278		SecretAccessKey: *resp.Credentials.SecretAccessKey,
279		SessionToken:    *resp.Credentials.SessionToken,
280		Source:          ProviderName,
281
282		CanExpire: true,
283		Expires:   *resp.Credentials.Expiration,
284	}, nil
285}
286