provider.go (5038B)
1 // Package logincreds implements AWS credential provision for sessions created 2 // via an `aws login` command. 3 package logincreds 4 5 import ( 6 "context" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 13 "github.com/aws/aws-sdk-go-v2/aws" 14 "github.com/aws/aws-sdk-go-v2/internal/sdk" 15 "github.com/aws/aws-sdk-go-v2/service/signin" 16 "github.com/aws/aws-sdk-go-v2/service/signin/types" 17 ) 18 19 // ProviderName identifies the login provider. 20 const ProviderName = "LoginProvider" 21 22 // TokenAPIClient provides the interface for the login session's token 23 // retrieval operation. 24 type TokenAPIClient interface { 25 CreateOAuth2Token(context.Context, *signin.CreateOAuth2TokenInput, ...func(*signin.Options)) (*signin.CreateOAuth2TokenOutput, error) 26 } 27 28 // Provider supplies credentials for an `aws login` session. 29 type Provider struct { 30 options Options 31 } 32 33 var _ aws.CredentialsProvider = (*Provider)(nil) 34 35 // Options configures the Provider. 36 type Options struct { 37 Client TokenAPIClient 38 39 // APIOptions to pass to the underlying CreateOAuth2Token operation. 40 ClientOptions []func(*signin.Options) 41 42 // The path to the cached login token. 43 CachedTokenFilepath string 44 45 // Whether to restrict file permissions on newly-written cache files. 46 // When true, files are created with 0600 on Unix. 47 RestrictPermissions bool 48 49 // The chain of providers that was used to create this provider. 50 // 51 // These values are for reporting purposes and are not meant to be set up 52 // directly. 53 CredentialSources []aws.CredentialSource 54 } 55 56 // New returns a new login session credentials provider. 57 func New(client TokenAPIClient, path string, opts ...func(*Options)) *Provider { 58 options := Options{ 59 Client: client, 60 CachedTokenFilepath: path, 61 } 62 63 for _, opt := range opts { 64 opt(&options) 65 } 66 67 return &Provider{options} 68 } 69 70 // Retrieve generates a new set of temporary credentials using an `aws login` 71 // session. 72 func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) { 73 token, err := p.loadToken() 74 if err != nil { 75 return aws.Credentials{}, fmt.Errorf("load login token: %w", err) 76 } 77 if err := token.Validate(); err != nil { 78 return aws.Credentials{}, fmt.Errorf("validate login token: %w", err) 79 } 80 81 // the token may have been refreshed elsewhere or the login session might 82 // have just been created 83 if sdk.NowTime().Before(token.AccessToken.ExpiresAt) { 84 return token.Credentials(), nil 85 } 86 87 opts := make([]func(*signin.Options), len(p.options.ClientOptions)+1) 88 opts[0] = addSignDPOP(token) 89 copy(opts[1:], p.options.ClientOptions) 90 91 out, err := p.options.Client.CreateOAuth2Token(ctx, &signin.CreateOAuth2TokenInput{ 92 TokenInput: &types.CreateOAuth2TokenRequestBody{ 93 ClientId: aws.String(token.ClientID), 94 GrantType: aws.String("refresh_token"), 95 RefreshToken: aws.String(token.RefreshToken), 96 }, 97 }, opts...) 98 if err != nil { 99 var terr *types.AccessDeniedException 100 if errors.As(err, &terr) { 101 err = toAccessDeniedError(terr) 102 } 103 return aws.Credentials{}, fmt.Errorf("create oauth2 token: %w", err) 104 } 105 106 token.Update(out) 107 if err := p.saveToken(token); err != nil { 108 return aws.Credentials{}, fmt.Errorf("save token: %w", err) 109 } 110 111 return token.Credentials(), nil 112 } 113 114 // ProviderSources returns the credential chain that was used to construct this 115 // provider. 116 func (p *Provider) ProviderSources() []aws.CredentialSource { 117 if p.options.CredentialSources == nil { 118 return []aws.CredentialSource{aws.CredentialSourceLogin} 119 } 120 return p.options.CredentialSources 121 } 122 123 func (p *Provider) loadToken() (*loginToken, error) { 124 f, err := openFile(p.options.CachedTokenFilepath) 125 if err != nil && os.IsNotExist(err) { 126 return nil, fmt.Errorf("token file not found, please reauthenticate") 127 } 128 if err != nil { 129 return nil, err 130 } 131 defer f.Close() 132 133 j, err := io.ReadAll(f) 134 if err != nil { 135 return nil, err 136 } 137 138 var t *loginToken 139 if err := json.Unmarshal(j, &t); err != nil { 140 return nil, err 141 } 142 143 return t, nil 144 } 145 146 func (p *Provider) saveToken(token *loginToken) error { 147 j, err := json.Marshal(token) 148 if err != nil { 149 return err 150 } 151 152 mode := os.FileMode(0666) // matches that used by os.Create 153 if p.options.RestrictPermissions { 154 mode = 0600 155 } 156 157 // createFile DOES NOT re-create the file with new permissions if it 158 // already exists, so in that scenario any existing permissions are 159 // preserved 160 f, err := createFile(p.options.CachedTokenFilepath, mode) 161 if err != nil { 162 return err 163 } 164 defer f.Close() 165 166 if _, err := f.Write(j); err != nil { 167 return err 168 } 169 170 return nil 171 } 172 173 func toAccessDeniedError(err *types.AccessDeniedException) error { 174 switch err.Error_ { 175 case types.OAuth2ErrorCodeTokenExpired: 176 return fmt.Errorf("login session has expired, please reauthenticate") 177 case types.OAuth2ErrorCodeUserCredentialsChanged: 178 return fmt.Errorf("login session password has changed, please reauthenticate") 179 case types.OAuth2ErrorCodeInsufficientPermissions: 180 return fmt.Errorf("insufficient permissions, you may be missing permissions for the CreateOAuth2Token action") 181 default: 182 return err 183 } 184 }