1// Package endpointcreds provides support for retrieving credentials from an 2// arbitrary HTTP endpoint. 3// 4// The credentials endpoint Provider can receive both static and refreshable 5// credentials that will expire. Credentials are static when an "Expiration" 6// value is not provided in the endpoint's response. 7// 8// Static credentials will never expire once they have been retrieved. The format 9// of the static credentials response: 10// { 11// "AccessKeyId" : "MUA...", 12// "SecretAccessKey" : "/7PC5om....", 13// } 14// 15// Refreshable credentials will expire within the "ExpiryWindow" of the Expiration 16// value in the response. The format of the refreshable credentials response: 17// { 18// "AccessKeyId" : "MUA...", 19// "SecretAccessKey" : "/7PC5om....", 20// "Token" : "AQoDY....=", 21// "Expiration" : "2016-02-25T06:03:31Z" 22// } 23// 24// Errors should be returned in the following format and only returned with 400 25// or 500 HTTP status codes. 26// { 27// "code": "ErrorCode", 28// "message": "Helpful error message." 29// } 30package endpointcreds 31 32import ( 33 "encoding/json" 34 "time" 35 36 "github.com/aws/aws-sdk-go/aws" 37 "github.com/aws/aws-sdk-go/aws/awserr" 38 "github.com/aws/aws-sdk-go/aws/client" 39 "github.com/aws/aws-sdk-go/aws/client/metadata" 40 "github.com/aws/aws-sdk-go/aws/credentials" 41 "github.com/aws/aws-sdk-go/aws/request" 42 "github.com/aws/aws-sdk-go/private/protocol/json/jsonutil" 43) 44 45// ProviderName is the name of the credentials provider. 46const ProviderName = `CredentialsEndpointProvider` 47 48// Provider satisfies the credentials.Provider interface, and is a client to 49// retrieve credentials from an arbitrary endpoint. 50type Provider struct { 51 staticCreds bool 52 credentials.Expiry 53 54 // Requires a AWS Client to make HTTP requests to the endpoint with. 55 // the Endpoint the request will be made to is provided by the aws.Config's 56 // Endpoint value. 57 Client *client.Client 58 59 // ExpiryWindow will allow the credentials to trigger refreshing prior to 60 // the credentials actually expiring. This is beneficial so race conditions 61 // with expiring credentials do not cause request to fail unexpectedly 62 // due to ExpiredTokenException exceptions. 63 // 64 // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true 65 // 10 seconds before the credentials are actually expired. 66 // 67 // If ExpiryWindow is 0 or less it will be ignored. 68 ExpiryWindow time.Duration 69 70 // Optional authorization token value if set will be used as the value of 71 // the Authorization header of the endpoint credential request. 72 AuthorizationToken string 73} 74 75// NewProviderClient returns a credentials Provider for retrieving AWS credentials 76// from arbitrary endpoint. 77func NewProviderClient(cfg aws.Config, handlers request.Handlers, endpoint string, options ...func(*Provider)) credentials.Provider { 78 p := &Provider{ 79 Client: client.New( 80 cfg, 81 metadata.ClientInfo{ 82 ServiceName: "CredentialsEndpoint", 83 Endpoint: endpoint, 84 }, 85 handlers, 86 ), 87 } 88 89 p.Client.Handlers.Unmarshal.PushBack(unmarshalHandler) 90 p.Client.Handlers.UnmarshalError.PushBack(unmarshalError) 91 p.Client.Handlers.Validate.Clear() 92 p.Client.Handlers.Validate.PushBack(validateEndpointHandler) 93 94 for _, option := range options { 95 option(p) 96 } 97 98 return p 99} 100 101// NewCredentialsClient returns a Credentials wrapper for retrieving credentials 102// from an arbitrary endpoint concurrently. The client will request the 103func NewCredentialsClient(cfg aws.Config, handlers request.Handlers, endpoint string, options ...func(*Provider)) *credentials.Credentials { 104 return credentials.NewCredentials(NewProviderClient(cfg, handlers, endpoint, options...)) 105} 106 107// IsExpired returns true if the credentials retrieved are expired, or not yet 108// retrieved. 109func (p *Provider) IsExpired() bool { 110 if p.staticCreds { 111 return false 112 } 113 return p.Expiry.IsExpired() 114} 115 116// Retrieve will attempt to request the credentials from the endpoint the Provider 117// was configured for. And error will be returned if the retrieval fails. 118func (p *Provider) Retrieve() (credentials.Value, error) { 119 resp, err := p.getCredentials() 120 if err != nil { 121 return credentials.Value{ProviderName: ProviderName}, 122 awserr.New("CredentialsEndpointError", "failed to load credentials", err) 123 } 124 125 if resp.Expiration != nil { 126 p.SetExpiration(*resp.Expiration, p.ExpiryWindow) 127 } else { 128 p.staticCreds = true 129 } 130 131 return credentials.Value{ 132 AccessKeyID: resp.AccessKeyID, 133 SecretAccessKey: resp.SecretAccessKey, 134 SessionToken: resp.Token, 135 ProviderName: ProviderName, 136 }, nil 137} 138 139type getCredentialsOutput struct { 140 Expiration *time.Time 141 AccessKeyID string 142 SecretAccessKey string 143 Token string 144} 145 146type errorOutput struct { 147 Code string `json:"code"` 148 Message string `json:"message"` 149} 150 151func (p *Provider) getCredentials() (*getCredentialsOutput, error) { 152 op := &request.Operation{ 153 Name: "GetCredentials", 154 HTTPMethod: "GET", 155 } 156 157 out := &getCredentialsOutput{} 158 req := p.Client.NewRequest(op, nil, out) 159 req.HTTPRequest.Header.Set("Accept", "application/json") 160 if authToken := p.AuthorizationToken; len(authToken) != 0 { 161 req.HTTPRequest.Header.Set("Authorization", authToken) 162 } 163 164 return out, req.Send() 165} 166 167func validateEndpointHandler(r *request.Request) { 168 if len(r.ClientInfo.Endpoint) == 0 { 169 r.Error = aws.ErrMissingEndpoint 170 } 171} 172 173func unmarshalHandler(r *request.Request) { 174 defer r.HTTPResponse.Body.Close() 175 176 out := r.Data.(*getCredentialsOutput) 177 if err := json.NewDecoder(r.HTTPResponse.Body).Decode(&out); err != nil { 178 r.Error = awserr.New(request.ErrCodeSerialization, 179 "failed to decode endpoint credentials", 180 err, 181 ) 182 } 183} 184 185func unmarshalError(r *request.Request) { 186 defer r.HTTPResponse.Body.Close() 187 188 var errOut errorOutput 189 err := jsonutil.UnmarshalJSONError(&errOut, r.HTTPResponse.Body) 190 if err != nil { 191 r.Error = awserr.NewRequestFailure( 192 awserr.New(request.ErrCodeSerialization, 193 "failed to decode error message", err), 194 r.HTTPResponse.StatusCode, 195 r.RequestID, 196 ) 197 return 198 } 199 200 // Response body format is not consistent between metadata endpoints. 201 // Grab the error message as a string and include that as the source error 202 r.Error = awserr.New(errOut.Code, errOut.Message, nil) 203} 204