173 lines
4.5 KiB
Go
173 lines
4.5 KiB
Go
// Package logincreds implements AWS credential provision for sessions created
|
|
// via an `aws login` command.
|
|
package logincreds
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/internal/sdk"
|
|
"github.com/aws/aws-sdk-go-v2/service/signin"
|
|
"github.com/aws/aws-sdk-go-v2/service/signin/types"
|
|
)
|
|
|
|
// ProviderName identifies the login provider.
|
|
const ProviderName = "LoginProvider"
|
|
|
|
// TokenAPIClient provides the interface for the login session's token
|
|
// retrieval operation.
|
|
type TokenAPIClient interface {
|
|
CreateOAuth2Token(context.Context, *signin.CreateOAuth2TokenInput, ...func(*signin.Options)) (*signin.CreateOAuth2TokenOutput, error)
|
|
}
|
|
|
|
// Provider supplies credentials for an `aws login` session.
|
|
type Provider struct {
|
|
options Options
|
|
}
|
|
|
|
var _ aws.CredentialsProvider = (*Provider)(nil)
|
|
|
|
// Options configures the Provider.
|
|
type Options struct {
|
|
Client TokenAPIClient
|
|
|
|
// APIOptions to pass to the underlying CreateOAuth2Token operation.
|
|
ClientOptions []func(*signin.Options)
|
|
|
|
// The path to the cached login token.
|
|
CachedTokenFilepath string
|
|
|
|
// The chain of providers that was used to create this provider.
|
|
//
|
|
// These values are for reporting purposes and are not meant to be set up
|
|
// directly.
|
|
CredentialSources []aws.CredentialSource
|
|
}
|
|
|
|
// New returns a new login session credentials provider.
|
|
func New(client TokenAPIClient, path string, opts ...func(*Options)) *Provider {
|
|
options := Options{
|
|
Client: client,
|
|
CachedTokenFilepath: path,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
return &Provider{options}
|
|
}
|
|
|
|
// Retrieve generates a new set of temporary credentials using an `aws login`
|
|
// session.
|
|
func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
|
token, err := p.loadToken()
|
|
if err != nil {
|
|
return aws.Credentials{}, fmt.Errorf("load login token: %w", err)
|
|
}
|
|
if err := token.Validate(); err != nil {
|
|
return aws.Credentials{}, fmt.Errorf("validate login token: %w", err)
|
|
}
|
|
|
|
// the token may have been refreshed elsewhere or the login session might
|
|
// have just been created
|
|
if sdk.NowTime().Before(token.AccessToken.ExpiresAt) {
|
|
return token.Credentials(), nil
|
|
}
|
|
|
|
opts := make([]func(*signin.Options), len(p.options.ClientOptions)+1)
|
|
opts[0] = addSignDPOP(token)
|
|
copy(opts[1:], p.options.ClientOptions)
|
|
|
|
out, err := p.options.Client.CreateOAuth2Token(ctx, &signin.CreateOAuth2TokenInput{
|
|
TokenInput: &types.CreateOAuth2TokenRequestBody{
|
|
ClientId: aws.String(token.ClientID),
|
|
GrantType: aws.String("refresh_token"),
|
|
RefreshToken: aws.String(token.RefreshToken),
|
|
},
|
|
}, opts...)
|
|
if err != nil {
|
|
var terr *types.AccessDeniedException
|
|
if errors.As(err, &terr) {
|
|
err = toAccessDeniedError(terr)
|
|
}
|
|
return aws.Credentials{}, fmt.Errorf("create oauth2 token: %w", err)
|
|
}
|
|
|
|
token.Update(out)
|
|
if err := p.saveToken(token); err != nil {
|
|
return aws.Credentials{}, fmt.Errorf("save token: %w", err)
|
|
}
|
|
|
|
return token.Credentials(), nil
|
|
}
|
|
|
|
// ProviderSources returns the credential chain that was used to construct this
|
|
// provider.
|
|
func (p *Provider) ProviderSources() []aws.CredentialSource {
|
|
if p.options.CredentialSources == nil {
|
|
return []aws.CredentialSource{aws.CredentialSourceLogin}
|
|
}
|
|
return p.options.CredentialSources
|
|
}
|
|
|
|
func (p *Provider) loadToken() (*loginToken, error) {
|
|
f, err := openFile(p.options.CachedTokenFilepath)
|
|
if err != nil && os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("token file not found, please reauthenticate")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
j, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var t *loginToken
|
|
if err := json.Unmarshal(j, &t); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func (p *Provider) saveToken(token *loginToken) error {
|
|
j, err := json.Marshal(token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := createFile(p.options.CachedTokenFilepath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
if _, err := f.Write(j); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func toAccessDeniedError(err *types.AccessDeniedException) error {
|
|
switch err.Error_ {
|
|
case types.OAuth2ErrorCodeTokenExpired:
|
|
return fmt.Errorf("login session has expired, please reauthenticate")
|
|
case types.OAuth2ErrorCodeUserCredentialsChanged:
|
|
return fmt.Errorf("login session password has changed, please reauthenticate")
|
|
case types.OAuth2ErrorCodeInsufficientPermissions:
|
|
return fmt.Errorf("insufficient permissions, you may be missing permissions for the CreateOAuth2Token action")
|
|
default:
|
|
return err
|
|
}
|
|
}
|