Skip to content

Commit

Permalink
Refactor for clarity
Browse files Browse the repository at this point in the history
- Rename expiration variables to reflect their actual use
- Create Token struct so that we can generate the expiration time
  immediately after we presign the token, and return the Token
  struct from the generate token functions.
  • Loading branch information
nckturner committed Oct 18, 2018
1 parent 981ecbe commit b60fb07
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 36 deletions.
11 changes: 7 additions & 4 deletions cmd/aws-iam-authenticator/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ var tokenCmd = &cobra.Command{
os.Exit(1)
}

var tok string
var tok token.Token
var out string
var err error
gen, err := token.NewGenerator(forwardSessionName)
if err != nil {
Expand All @@ -60,10 +61,12 @@ var tokenCmd = &cobra.Command{
fmt.Fprintf(os.Stderr, "could not get token: %v\n", err)
os.Exit(1)
}
if !tokenOnly {
tok = gen.FormatJSON(tok)
if tokenOnly {
out = tok.Token
} else {
out = gen.FormatJSON(tok)
}
fmt.Println(tok)
fmt.Println(out)
},
}

Expand Down
62 changes: 34 additions & 28 deletions pkg/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,23 @@ const (
// signed, but we set this unused parameter to 60 for legacy reasons (we check for a value between 0 and 60 on the
// server side in 0.3.0 or earlier). IT IS IGNORED. If we can get STS to support x-amz-expires, then we should
// set this parameter to the actual expiration, and make it configurable.
tokenExpirationParam = 60
requestPresignParam = 60
// The actual token expiration (presigned STS urls are valid for 15 minutes after timestamp in x-amz-date).
tokenExpiration = 15 * time.Minute
// Refresh token after 14 minutes for some cushion.
refreshTokenAfter = 14 * time.Minute
v1Prefix = "k8s-aws-v1."
maxTokenLenBytes = 1024 * 4
clusterIDHeader = "x-k8s-aws-id"
presignedURLExpiration = 15 * time.Minute
v1Prefix = "k8s-aws-v1."
maxTokenLenBytes = 1024 * 4
clusterIDHeader = "x-k8s-aws-id"
// Format of the X-Amz-Date header used for expiration
// https://golang.org/pkg/time/#pkg-constants
dateHeaderFormat = "20060102T030405Z"
dateHeaderFormat = "20060102T150405Z"
)

// Token is generated and used by Kubernetes client-go to authenticate with a Kubernetes cluster.
type Token struct {
Token string
Expiration time.Time
}

// FormatError is returned when there is a problem with token that is
// an encoded sts request. This can include the url, data, action or anything
// else that prevents the sts call from being made.
Expand Down Expand Up @@ -137,15 +141,15 @@ type getCallerIdentityWrapper struct {
// Generator provides new tokens for the heptio authenticator.
type Generator interface {
// Get a token using credentials in the default credentials chain.
Get(string) (string, error)
Get(string) (Token, error)
// GetWithRole creates a token by assuming the provided role, using the credentials in the default chain.
GetWithRole(clusterID, roleARN string) (string, error)
GetWithRole(clusterID, roleARN string) (Token, error)
// GetWithRoleForSession creates a token by assuming the provided role, using the provided session.
GetWithRoleForSession(clusterID string, roleARN string, sess *session.Session) (string, error)
GetWithRoleForSession(clusterID string, roleARN string, sess *session.Session) (Token, error)
// GetWithSTS returns a token valid for clusterID using the given STS client.
GetWithSTS(clusterID string, stsAPI *sts.STS) (string, error)
GetWithSTS(clusterID string, stsAPI *sts.STS) (Token, error)
// FormatJSON returns the client auth formatted json for the ExecCredential auth
FormatJSON(string) string
FormatJSON(Token) string
}

type generator struct {
Expand All @@ -161,7 +165,7 @@ func NewGenerator(forwardSessionName bool) (Generator, error) {

// Get uses the directly available AWS credentials to return a token valid for
// clusterID. It follows the default AWS credential handling behavior.
func (g generator) Get(clusterID string) (string, error) {
func (g generator) Get(clusterID string) (Token, error) {
return g.GetWithRole(clusterID, "")
}

Expand All @@ -174,23 +178,23 @@ func StdinStderrTokenProvider() (string, error) {

// GetWithRole assumes the given AWS IAM role and returns a token valid for
// clusterID. If roleARN is empty, behaves like Get (does not assume a role).
func (g generator) GetWithRole(clusterID string, roleARN string) (string, error) {
func (g generator) GetWithRole(clusterID string, roleARN string) (Token, error) {
// create a session with the "base" credentials available
// (from environment variable, profile files, EC2 metadata, etc)
sess, err := session.NewSessionWithOptions(session.Options{
AssumeRoleTokenProvider: StdinStderrTokenProvider,
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return "", fmt.Errorf("could not create session: %v", err)
return Token{}, fmt.Errorf("could not create session: %v", err)
}

return g.GetWithRoleForSession(clusterID, roleARN, sess)
}

// GetWithRole assumes the given AWS IAM role for the given session and behaves
// like GetWithRole.
func (g generator) GetWithRoleForSession(clusterID string, roleARN string, sess *session.Session) (string, error) {
func (g generator) GetWithRoleForSession(clusterID string, roleARN string, sess *session.Session) (Token, error) {
// use an STS client based on the direct credentials
stsAPI := sts.New(sess)

Expand All @@ -204,7 +208,7 @@ func (g generator) GetWithRoleForSession(clusterID string, roleARN string, sess
// capabilities
resp, err := stsAPI.GetCallerIdentity(&sts.GetCallerIdentityInput{})
if err != nil {
return "", err
return Token{}, err
}

userIDParts := strings.Split(*resp.UserId, ":")
Expand All @@ -226,7 +230,7 @@ func (g generator) GetWithRoleForSession(clusterID string, roleARN string, sess
}

// GetWithSTS returns a token valid for clusterID using the given STS client.
func (g generator) GetWithSTS(clusterID string, stsAPI *sts.STS) (string, error) {
func (g generator) GetWithSTS(clusterID string, stsAPI *sts.STS) (Token, error) {
// generate an sts:GetCallerIdentity request and add our custom cluster ID header
request, _ := stsAPI.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{})
request.HTTPRequest.Header.Add(clusterIDHeader, clusterID)
Expand All @@ -237,26 +241,28 @@ func (g generator) GetWithSTS(clusterID string, stsAPI *sts.STS) (string, error)
// parameter is a required argument to Presign(), and authenticators 0.3.0 and older are expecting a value between
// 0 and 60 on the server side).
// https://github.com/aws/aws-sdk-go/issues/2167
presignedURLString, err := request.Presign(tokenExpirationParam)
presignedURLString, err := request.Presign(requestPresignParam)
if err != nil {
return "", err
return Token{}, err
}

// Set token expiration to 1 minute before the presigned URL expires for some cushion
tokenExpiration := time.Now().Local().Add(presignedURLExpiration - 1*time.Minute)
// TODO: this may need to be a constant-time base64 encoding
return v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), nil
return Token{v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), tokenExpiration}, nil
}

// FormatJSON formats the json to support ExecCredential authentication
func (g generator) FormatJSON(token string) string {
expirationTimestamp := metav1.NewTime(time.Now().Local().Add(refreshTokenAfter))
func (g generator) FormatJSON(token Token) string {
expirationTimestamp := metav1.NewTime(token.Expiration)
execInput := &clientauthv1alpha1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1alpha1",
Kind: "ExecCredential",
},
Status: &clientauthv1alpha1.ExecCredentialStatus{
ExpirationTimestamp: &expirationTimestamp,
Token: token,
Token: token.Token,
},
}
enc, _ := json.Marshal(execInput)
Expand Down Expand Up @@ -350,13 +356,13 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) {

dateParam, err := time.Parse(dateHeaderFormat, date)
if err != nil {
return nil, FormatError{fmt.Sprintf("X-Amz-Date parameter in incorrect format %s (should be %s)", dateParam, dateHeaderFormat)}
return nil, FormatError{fmt.Sprintf("error parsing X-Amz-Date parameter %s into format %s: %s", date, dateHeaderFormat, err.Error())}
}

now := time.Now()
expiration := dateParam.Add(tokenExpiration)
expiration := dateParam.Add(presignedURLExpiration)
if now.After(expiration) {
return nil, FormatError{fmt.Sprintf("X-Amz-Date parameter is expired (%.f minute expiration) %s", tokenExpiration.Minutes(), dateParam)}
return nil, FormatError{fmt.Sprintf("X-Amz-Date parameter is expired (%.f minute expiration) %s", presignedURLExpiration.Minutes(), dateParam)}
}

req, err := http.NewRequest("GET", parsedURL.String(), nil)
Expand Down
15 changes: 11 additions & 4 deletions pkg/token/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
)

func validationErrorTest(t *testing.T, token string, expectedErr string) {
Expand All @@ -32,9 +34,12 @@ func assertSTSError(t *testing.T, err error) {
}
}

const validURL = "https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-expires=60"

var validToken = toToken(validURL)
var (
now = time.Now()
timeStr = now.Format("20060102T150405Z")
validToken = toToken(validURL)
validURL = fmt.Sprintf("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-expires=60&x-amz-date=%s", timeStr)
)

func toToken(url string) string {
return v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(url))
Expand Down Expand Up @@ -101,7 +106,9 @@ func TestVerifyTokenPreSTSValidations(t *testing.T) {
validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=get&action=post"), "query parameter with multiple values not supported")
validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=NotGetCallerIdenity"), "unexpected action parameter in pre-signed URL")
validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=abc%3bx-k8s-aws-i%3bdef"), "client did not sign the x-k8s-aws-id header in the pre-signed URL")
validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-expires=70"), "invalid X-Amz-Expires parameter in pre-signed URL")
validationErrorTest(t, toToken(fmt.Sprintf("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=9999999", timeStr)), "invalid X-Amz-Expires parameter in pre-signed URL")
validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=xxxxxxx&x-amz-expires=60"), "error parsing X-Amz-Date parameter")
validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=19900422T010203Z&x-amz-expires=60"), "X-Amz-Date parameter is expired")
}

func TestVerifyHTTPError(t *testing.T) {
Expand Down

0 comments on commit b60fb07

Please sign in to comment.