Skip to content

Commit

Permalink
Implement Credential Chain Support for SSO Provider
Browse files Browse the repository at this point in the history
  • Loading branch information
skmcgrail committed Jan 27, 2021
1 parent 40d7ce4 commit d14d48d
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 39 deletions.
19 changes: 19 additions & 0 deletions aws/session/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/processcreds"
"github.com/aws/aws-sdk-go/aws/credentials/ssocreds"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/request"
Expand Down Expand Up @@ -100,6 +101,9 @@ func resolveCredsFromProfile(cfg *aws.Config,
sharedCfg.Creds,
)

case sharedCfg.hasSSOConfiguration():
creds = resolveSSOCredentials(cfg, sharedCfg, handlers)

case len(sharedCfg.CredentialProcess) != 0:
// Get credentials from CredentialProcess
creds = processcreds.NewCredentials(sharedCfg.CredentialProcess)
Expand Down Expand Up @@ -151,6 +155,21 @@ func resolveCredsFromProfile(cfg *aws.Config,
return creds, nil
}

func resolveSSOCredentials(cfg *aws.Config, sharedCfg sharedConfig, handlers request.Handlers) *credentials.Credentials {
cfgCopy := cfg.Copy()
cfgCopy.Region = &sharedCfg.SSORegion

return ssocreds.NewCredentials(
&Session{
Config: cfgCopy,
Handlers: handlers.Copy(),
},
sharedCfg.SSOAccountID,
sharedCfg.SSORoleName,
sharedCfg.SSOStartURL,
)
}

// valid credential source values
const (
credSourceEc2Metadata = "Ec2InstanceMetadata"
Expand Down
188 changes: 154 additions & 34 deletions aws/session/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ package session

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
Expand Down Expand Up @@ -68,6 +70,14 @@ func setupCredentialsEndpoints(t *testing.T) (endpoints.Resolver, func()) {
Format("2006-01-02T15:04:05Z"))))
}))

ssoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf(
getRoleCredentialsResponse,
time.Now().
Add(15*time.Minute).
UnixNano()/int64(time.Millisecond))))
}))

resolver := endpoints.ResolverFunc(
func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
switch service {
Expand All @@ -79,6 +89,10 @@ func setupCredentialsEndpoints(t *testing.T) (endpoints.Resolver, func()) {
return endpoints.ResolvedEndpoint{
URL: stsServer.URL,
}, nil
case "portal.sso":
return endpoints.ResolvedEndpoint{
URL: ssoServer.URL,
}, nil
default:
return endpoints.ResolvedEndpoint{},
fmt.Errorf("unknown service endpoint, %s", service)
Expand All @@ -89,6 +103,7 @@ func setupCredentialsEndpoints(t *testing.T) (endpoints.Resolver, func()) {
shareddefaults.ECSContainerCredentialsURI = origECSEndpoint
ecsMetadataServer.Close()
ec2MetadataServer.Close()
ssoServer.Close()
stsServer.Close()
}
}
Expand All @@ -105,30 +120,34 @@ func TestSharedConfigCredentialSource(t *testing.T) {
expectedError error
expectedAccessKey string
expectedSecretKey string
expectedSessionToken string
expectedChain []string
init func()
init func() (func(), error)
dependentOnOS bool
}{
{
name: "credential source and source profile",
profile: "invalid_source_and_credential_source",
expectedError: ErrSharedConfigSourceCollision,
init: func() {
init: func() (func(), error) {
os.Setenv("AWS_ACCESS_KEY", "access_key")
os.Setenv("AWS_SECRET_KEY", "secret_key")
return func() {}, nil
},
},
{
name: "env var credential source",
sessOptProfile: "env_var_credential_source",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
name: "env var credential source",
sessOptProfile: "env_var_credential_source",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
expectedChain: []string{
"assume_role_w_creds_role_arn_env",
},
init: func() {
init: func() (func(), error) {
os.Setenv("AWS_ACCESS_KEY", "access_key")
os.Setenv("AWS_SECRET_KEY", "secret_key")
return func() {}, nil
},
},
{
Expand All @@ -137,36 +156,42 @@ func TestSharedConfigCredentialSource(t *testing.T) {
expectedChain: []string{
"assume_role_w_creds_role_arn_ec2",
},
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
},
{
name: "ec2metadata custom EC2 IMDS endpoint, env var",
profile: "not-exists-profile",
expectedAccessKey: "ec2_custom_key",
expectedSecretKey: "ec2_custom_secret",
init: func() {
name: "ec2metadata custom EC2 IMDS endpoint, env var",
profile: "not-exists-profile",
expectedAccessKey: "ec2_custom_key",
expectedSecretKey: "ec2_custom_secret",
expectedSessionToken: "token",
init: func() (func(), error) {
altServer := newEc2MetadataServer("ec2_custom_key", "ec2_custom_secret", true)
os.Setenv("AWS_EC2_METADATA_SERVICE_ENDPOINT", altServer.URL)
return func() {}, nil
},
},
{
name: "ecs container credential source",
profile: "ecscontainer",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
name: "ecs container credential source",
profile: "ecscontainer",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
expectedChain: []string{
"assume_role_w_creds_role_arn_ecs",
},
init: func() {
init: func() (func(), error) {
os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/ECS")
return func() {}, nil
},
},
{
name: "chained assume role with env creds",
profile: "chained_assume_role",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
name: "chained assume role with env creds",
profile: "chained_assume_role",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
expectedChain: []string{
"assume_role_w_creds_role_arn_chain",
"assume_role_w_creds_role_arn_ec2",
Expand All @@ -180,25 +205,60 @@ func TestSharedConfigCredentialSource(t *testing.T) {
expectedSecretKey: "cred_proc_secret",
},
{
name: "credential process with ARN set",
profile: "cred_proc_arn_set",
dependentOnOS: true,
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
name: "credential process with ARN set",
profile: "cred_proc_arn_set",
dependentOnOS: true,
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
expectedChain: []string{
"assume_role_w_creds_proc_role_arn",
},
},
{
name: "chained assume role with credential process",
profile: "chained_cred_proc",
dependentOnOS: true,
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
name: "chained assume role with credential process",
profile: "chained_cred_proc",
dependentOnOS: true,
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
expectedChain: []string{
"assume_role_w_creds_proc_source_prof",
},
},
{
name: "sso credentials",
profile: "sso_creds",
expectedAccessKey: "SSO_AKID",
expectedSecretKey: "SSO_SECRET_KEY",
expectedSessionToken: "SSO_SESSION_TOKEN",
init: func() (func(), error) {
return ssoTestSetup()
},
},
{
name: "chained assume role with sso credentials",
profile: "source_sso_creds",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
expectedChain: []string{
"source_sso_creds_arn",
},
init: func() (func(), error) {
return ssoTestSetup()
},
},
{
name: "chained assume role with sso and static credentials",
profile: "assume_sso_and_static",
expectedAccessKey: "AKID",
expectedSecretKey: "SECRET",
expectedSessionToken: "SESSION_TOKEN",
expectedChain: []string{
"assume_sso_and_static_arn",
},
},
}

for i, c := range cases {
Expand All @@ -222,7 +282,11 @@ func TestSharedConfigCredentialSource(t *testing.T) {
defer cleanupFn()

if c.init != nil {
c.init()
cleanup, err := c.init()
if err != nil {
t.Fatalf("expect no error, got %v", err)
}
defer cleanup()
}

var credChain []string
Expand Down Expand Up @@ -268,6 +332,10 @@ func TestSharedConfigCredentialSource(t *testing.T) {
if e, a := c.expectedSecretKey, creds.SecretAccessKey; e != a {
t.Errorf("expected %v, but received %v", e, a)
}

if e, a := c.expectedSessionToken, creds.SessionToken; e != a {
t.Errorf("expected %v, but received %v", e, a)
}
})
}
}
Expand Down Expand Up @@ -312,6 +380,20 @@ const assumeRoleRespMsg = `
</AssumeRoleResponse>
`

const getRoleCredentialsResponse = `{
"roleCredentials": {
"accessKeyId": "SSO_AKID",
"secretAccessKey": "SSO_SECRET_KEY",
"sessionToken": "SSO_SESSION_TOKEN",
"expiration": %d
}
}`

const ssoTokenCacheFile = `{
"accessToken": "ssoAccessToken",
"expiresAt": "%s"
}`

func TestSessionAssumeRole(t *testing.T) {
restoreEnvFn := initSessionTestEnv()
defer restoreEnvFn()
Expand Down Expand Up @@ -647,3 +729,41 @@ func TestSessionAssumeRole_WithMFA_ExtendedDuration(t *testing.T) {
t.Errorf("expect %v, to be in %v", e, a)
}
}

func ssoTestSetup() (func(), error) {
dir, err := ioutil.TempDir("", "sso-test")
if err != nil {
return nil, err
}

cacheDir := filepath.Join(dir, ".aws", "sso", "cache")
err = os.MkdirAll(cacheDir, 0750)
if err != nil {
os.RemoveAll(dir)
return nil, err
}

tokenFile, err := os.Create(filepath.Join(cacheDir, "eb5e43e71ce87dd92ec58903d76debd8ee42aefd.json"))
if err != nil {
os.RemoveAll(dir)
return nil, err
}
defer tokenFile.Close()

_, err = tokenFile.WriteString(fmt.Sprintf(ssoTokenCacheFile, time.Now().
Add(15*time.Minute).
Format(time.RFC3339)))
if err != nil {
os.RemoveAll(dir)
return nil, err
}

if runtime.GOOS == "windows" {
os.Setenv("USERPROFILE", dir)
} else {
os.Setenv("HOME", dir)
}

return func() {
}, nil
}
2 changes: 1 addition & 1 deletion aws/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const (

// ErrSharedConfigSourceCollision will be returned if a section contains both
// source_profile and credential_source
var ErrSharedConfigSourceCollision = awserr.New(ErrCodeSharedConfig, "only source profile or credential source can be specified, not both", nil)
var ErrSharedConfigSourceCollision = awserr.New(ErrCodeSharedConfig, "only one credential type may be specified per profile: source profile, credential source, credential process, web identity token, or sso", nil)

// ErrSharedConfigECSContainerEnvVarEmpty will be returned if the environment
// variables are empty and Environment was set as the credential source
Expand Down
Loading

0 comments on commit d14d48d

Please sign in to comment.