Repository: Versent/saml2aws
Pull request #157 (2018-04-01 07:25:19)

feat(Login) Support sessions up to 12 hours :tada:
see https://aws.amazon.com/blogs/security/enable-federated-api-access-to-your-aws-resources-for-up-to-12-hours-using-iam-roles/

* Refactor aws config to use structures
* Added saving of the expiry time for sessions and principal ARN
* Enable content dumping using env DUMP_CONTENT=true while debugging


Patch:
diff --git a/Gopkg.lock b/Gopkg.lock
index 5894615..1a3ba40 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -10,8 +10,8 @@
 [[projects]]
   name = "github.com/PuerkitoBio/goquery"
   packages = ["."]
-  revision = "09540e565986583836b5fc792fe951ec5fd201dd"
-  version = "v1.3.0"
+  revision = "a86ea073017a6beddef78c8659e7224e8ca634b0"
+  version = "v1.4.0"
 
 [[projects]]
   name = "github.com/alecthomas/kingpin"
@@ -223,6 +223,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "8e5ad434dff4da641ffdfd4d83073bdac313a6290f29590e4ce41e0061778c89"
+  inputs-digest = "5afb5282cc1ca7766bbcc444da86352197566db3115a712277cde0db5102180b"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
index c0faa5c..24b6744 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -31,7 +31,7 @@
 
 [[constraint]]
   name = "github.com/PuerkitoBio/goquery"
-  version = "1.3.0"
+  version = "1.4.0"
 
 [[constraint]]
   name = "github.com/alecthomas/kingpin"
diff --git a/README.md b/README.md
index ef8868e..f5c3b04 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ The process goes something like this:
 * Log in to Identity Provider using form based authentication
 * Build a SAML assertion containing AWS roles
 * Exchange the role and SAML assertion with [AWS STS service](https://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html) to get a temporary set of credentials
-* Save these creds to an aws profile named "saml"
+* Save these credentials to an aws profile named "saml"
 
 # Requirements
 
@@ -27,7 +27,7 @@ The process goes something like this:
 
 Aside from Okta, most of the providers in this project are using screen scraping to log users into SAML, this isn't ideal and hopefully vendors make this easier in the future. In addition to this there are some things you need to know:
 
-1. AWS only permits session tokens being issued with a duration of up to 3600 seconds (1 hour), this is constrained by the [STS AssumeRoleWithSAML API](http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html) call and `DurationSeconds` field.
+1. AWS defaults to session tokens being issued with a duration of up to 3600 seconds (1 hour), this can now be configured as per [Enable Federated API Access to your AWS Resources for up to 12 hours Using IAM Roles](https://aws.amazon.com/blogs/security/enable-federated-api-access-to-your-aws-resources-for-up-to-12-hours-using-iam-roles/) and `--session-duration` flag.
 2. Every SAML provider is different, the login process, MFA support is pluggable and therefore some work may be needed to integrate with your identity server
 
 # Install
@@ -76,20 +76,25 @@ usage: saml2aws [<flags>] <command> [<args> ...]
 A command line tool to help with SAML access to the AWS token service.
 
 Flags:
-      --help                   Show context-sensitive help (also try --help-long and --help-man).
+      --help                   Show context-sensitive help (also try --help-long
+                               and --help-man).
       --version                Show application version.
       --verbose                Enable verbose logging
-  -i, --provider=PROVIDER      This flag it is obsolete see https://github.com/Versent/saml2aws#adding-idp-accounts.
+  -i, --provider=PROVIDER      This flag it is obsolete see
+                               https://github.com/Versent/saml2aws#adding-idp-accounts.
   -a, --idp-account="default"  The name of the configured IDP account
-      --idp-provider=IDP-PROVIDER
+      --idp-provider=IDP-PROVIDER  
                                The configured IDP provider
-      --mfa="Auto"             The name of the mfa
+      --mfa=MFA                The name of the mfa
   -s, --skip-verify            Skip verification of server certificate.
       --url=URL                The URL of the SAML IDP server used to login.
       --username=USERNAME      The username used to login.
+      --password=PASSWORD      The password used to login.
       --role=ROLE              The ARN of the role to assume.
       --aws-urn=AWS-URN        The URN used by SAML when you login.
       --skip-prompt            Skip prompting for parameters during login.
+      --session-duration=SESSION-DURATION  
+                               The duration of your AWS Session.
 
 Commands:
   help [<command>...]
@@ -103,14 +108,15 @@ Commands:
   login [<flags>]
     Login to a SAML 2.0 IDP and convert the SAML assertion to an STS token.
 
-        --password=PASSWORD  The password used to login.
-    -p, --profile="saml"     The AWS profile to save the temporary credentials
+    -p, --profile="saml"  The AWS profile to save the temporary credentials
 
   exec [<flags>] [<command>...]
     Exec the supplied command with env vars from STS token.
 
-        --password=PASSWORD  The password used to login.
-    -p, --profile="saml"     The AWS profile to save the temporary credentials
+    -p, --profile="saml"  The AWS profile to save the temporary credentials
+
+  list-roles
+    List available role ARNs.
 
 ```
 
diff --git a/cmd/saml2aws/commands/exec.go b/cmd/saml2aws/commands/exec.go
index 119cac5..fcd850d 100644
--- a/cmd/saml2aws/commands/exec.go
+++ b/cmd/saml2aws/commands/exec.go
@@ -3,6 +3,7 @@ package commands
 import (
 	"fmt"
 	"os"
+	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
@@ -35,6 +36,15 @@ func Exec(execFlags *flags.LoginExecFlags, cmdline []string) error {
 		return nil
 	}
 
+	awsCreds, err := sharedCreds.Load()
+	if err != nil {
+		return errors.Wrap(err, "error loading credentials")
+	}
+
+	if awsCreds.Expires.Sub(time.Now()) < 0 {
+		return errors.New("error aws credentials have expired")
+	}
+
 	ok, err := checkToken(execFlags.Profile)
 	if err != nil {
 		return errors.Wrap(err, "error validating token")
@@ -47,12 +57,7 @@ func Exec(execFlags *flags.LoginExecFlags, cmdline []string) error {
 		return errors.Wrap(err, "error logging in")
 	}
 
-	id, secret, token, err := sharedCreds.Load()
-	if err != nil {
-		return errors.Wrap(err, "error loading credentials")
-	}
-
-	return shell.ExecShellCmd(cmdline, shell.BuildEnvVars(id, secret, token))
+	return shell.ExecShellCmd(cmdline, shell.BuildEnvVars(awsCreds))
 }
 
 func checkToken(profile string) (bool, error) {
diff --git a/cmd/saml2aws/commands/login.go b/cmd/saml2aws/commands/login.go
index 66db5d5..ee5f94b 100644
--- a/cmd/saml2aws/commands/login.go
+++ b/cmd/saml2aws/commands/login.go
@@ -18,9 +18,6 @@ import (
 	"github.com/versent/saml2aws/pkg/flags"
 )
 
-// MaxDurationSeconds the maximum duration in seconds for an STS session
-const MaxDurationSeconds = 3600
-
 // Login login to ADFS
 func Login(loginFlags *flags.LoginExecFlags) error {
 
@@ -51,8 +48,6 @@ func Login(loginFlags *flags.LoginExecFlags) error {
 		os.Exit(1)
 	}
 
-	fmt.Printf("Authenticating as %s ...\n", loginDetails.Username)
-
 	err = loginDetails.Validate()
 	if err != nil {
 		return errors.Wrap(err, "error validating login details")
@@ -65,6 +60,8 @@ func Login(loginFlags *flags.LoginExecFlags) error {
 		return errors.Wrap(err, "error building IdP client")
 	}
 
+	fmt.Printf("Authenticating as %s ...\n", loginDetails.Username)
+
 	samlAssertion, err := provider.Authenticate(loginDetails)
 	if err != nil {
 		return errors.Wrap(err, "error authenticating to IdP")
@@ -82,40 +79,19 @@ func Login(loginFlags *flags.LoginExecFlags) error {
 		return errors.Wrap(err, "error storing password in keychain")
 	}
 
-	data, err := base64.StdEncoding.DecodeString(samlAssertion)
-	if err != nil {
-		return errors.Wrap(err, "error decoding saml assertion")
-	}
-
-	roles, err := saml2aws.ExtractAwsRoles(data)
-	if err != nil {
-		return errors.Wrap(err, "error parsing aws roles")
-	}
-
-	if len(roles) == 0 {
-		fmt.Println("No roles to assume")
-		fmt.Println("Please check you are permitted to assume roles for the AWS service")
-		os.Exit(1)
-	}
-
-	awsRoles, err := saml2aws.ParseAWSRoles(roles)
-	if err != nil {
-		return errors.Wrap(err, "error parsing aws roles")
-	}
-
-	role, err := resolveRole(awsRoles, samlAssertion, loginFlags)
+	role, err := selectAwsRole(samlAssertion, loginFlags)
 	if err != nil {
 		return errors.Wrap(err, "Failed to assume role, please check you are permitted to assume the given role for the AWS service")
 	}
 
 	fmt.Println("Selected role:", role.RoleARN)
 
-	err = loginToStsUsingRole(role, sharedCreds, samlAssertion)
+	awsCreds, err := loginToStsUsingRole(account, role, samlAssertion)
 	if err != nil {
 		return errors.Wrap(err, "error logging into aws role using saml assertion")
 	}
 
-	return nil
+	return saveCredentials(awsCreds, sharedCreds)
 }
 
 func buildIdpAccount(loginFlags *flags.LoginExecFlags) (*cfg.IDPAccount, error) {
@@ -186,6 +162,31 @@ func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *flags.LoginExecFla
 	return loginDetails, nil
 }
 
+func selectAwsRole(samlAssertion string, loginFlags *flags.LoginExecFlags) (*saml2aws.AWSRole, error) {
+	data, err := base64.StdEncoding.DecodeString(samlAssertion)
+	if err != nil {
+		return nil, errors.Wrap(err, "error decoding saml assertion")
+	}
+
+	roles, err := saml2aws.ExtractAwsRoles(data)
+	if err != nil {
+		return nil, errors.Wrap(err, "error parsing aws roles")
+	}
+
+	if len(roles) == 0 {
+		fmt.Println("No roles to assume")
+		fmt.Println("Please check you are permitted to assume roles for the AWS service")
+		os.Exit(1)
+	}
+
+	awsRoles, err := saml2aws.ParseAWSRoles(roles)
+	if err != nil {
+		return nil, errors.Wrap(err, "error parsing aws roles")
+	}
+
+	return resolveRole(awsRoles, samlAssertion, loginFlags)
+}
+
 func resolveRole(awsRoles []*saml2aws.AWSRole, samlAssertion string, loginFlags *flags.LoginExecFlags) (*saml2aws.AWSRole, error) {
 	var role = new(saml2aws.AWSRole)
 
@@ -220,11 +221,11 @@ func resolveRole(awsRoles []*saml2aws.AWSRole, samlAssertion string, loginFlags
 	return role, nil
 }
 
-func loginToStsUsingRole(role *saml2aws.AWSRole, sharedCreds *awsconfig.CredentialsProvider, samlAssertion string) error {
+func loginToStsUsingRole(account *cfg.IDPAccount, role *saml2aws.AWSRole, samlAssertion string) (*awsconfig.AWSCredentials, error) {
 
 	sess, err := session.NewSession()
 	if err != nil {
-		return errors.Wrap(err, "failed to create session")
+		return nil, errors.Wrap(err, "failed to create session")
 	}
 
 	svc := sts.New(sess)
@@ -233,25 +234,36 @@ func loginToStsUsingRole(role *saml2aws.AWSRole, sharedCreds *awsconfig.Credenti
 		PrincipalArn:    aws.String(role.PrincipalARN), // Required
 		RoleArn:         aws.String(role.RoleARN),      // Required
 		SAMLAssertion:   aws.String(samlAssertion),     // Required
-		DurationSeconds: aws.Int64(MaxDurationSeconds), // 1 hour
+		DurationSeconds: aws.Int64(int64(account.SessionDuration)),
 	}
 
 	fmt.Println("Requesting AWS credentials using SAML assertion")
 
 	resp, err := svc.AssumeRoleWithSAML(params)
 	if err != nil {
-		return errors.Wrap(err, "error retrieving STS credentials using SAML")
+		return nil, errors.Wrap(err, "error retrieving STS credentials using SAML")
 	}
 
-	err = sharedCreds.Save(aws.StringValue(resp.Credentials.AccessKeyId), aws.StringValue(resp.Credentials.SecretAccessKey), aws.StringValue(resp.Credentials.SessionToken))
+	return &awsconfig.AWSCredentials{
+		AWSAccessKey:     aws.StringValue(resp.Credentials.AccessKeyId),
+		AWSSecretKey:     aws.StringValue(resp.Credentials.SecretAccessKey),
+		AWSSessionToken:  aws.StringValue(resp.Credentials.SessionToken),
+		AWSSecurityToken: aws.StringValue(resp.Credentials.SessionToken),
+		PrincipalARN:     aws.StringValue(resp.AssumedRoleUser.Arn),
+		Expires:          resp.Credentials.Expiration.Local(),
+	}, nil
+}
+
+func saveCredentials(awsCreds *awsconfig.AWSCredentials, sharedCreds *awsconfig.CredentialsProvider) error {
+	err := sharedCreds.Save(awsCreds)
 	if err != nil {
 		return errors.Wrap(err, "error saving credentials")
 	}
 
-	fmt.Println("Logged in as:", aws.StringValue(resp.AssumedRoleUser.Arn))
+	fmt.Println("Logged in as:", awsCreds.PrincipalARN)
 	fmt.Println("")
 	fmt.Println("Your new access key pair has been stored in the AWS configuration")
-	fmt.Printf("Note that it will expire at %v\n", resp.Credentials.Expiration.Local())
+	fmt.Printf("Note that it will expire at %v\n", awsCreds.Expires)
 	fmt.Println("To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile", sharedCreds.Profile, "ec2 describe-instances).")
 
 	return nil
diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go
index 1fcfb56..4540ede 100644
--- a/cmd/saml2aws/main.go
+++ b/cmd/saml2aws/main.go
@@ -60,6 +60,7 @@ func main() {
 	app.Flag("role", "The ARN of the role to assume.").StringVar(&commonFlags.RoleArn)
 	app.Flag("aws-urn", "The URN used by SAML when you login.").StringVar(&commonFlags.AmazonWebservicesURN)
 	app.Flag("skip-prompt", "Skip prompting for parameters during login.").BoolVar(&commonFlags.SkipPrompt)
+	app.Flag("session-duration", "The duration of your AWS Session.").IntVar(&commonFlags.SessionDuration)
 
 	// `configure` command and settings
 	cmdConfigure := app.Command("configure", "Configure a new IDP account.")
diff --git a/pkg/awsconfig/awsconfig.go b/pkg/awsconfig/awsconfig.go
index 618877f..2c69b11 100644
--- a/pkg/awsconfig/awsconfig.go
+++ b/pkg/awsconfig/awsconfig.go
@@ -4,8 +4,9 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"time"
 
-	"github.com/mitchellh/go-homedir"
+	homedir "github.com/mitchellh/go-homedir"
 	"github.com/sirupsen/logrus"
 
 	"github.com/pkg/errors"
@@ -25,10 +26,12 @@ var (
 
 // AWSCredentials represents the set of attributes used to authenticate to AWS with a short lived session
 type AWSCredentials struct {
-	AWSAccessKey     string `ini:"aws_access_key_id"`
-	AWSSecretKey     string `ini:"aws_secret_access_key"`
-	AWSSessionToken  string `ini:"aws_session_token"`
-	AWSSecurityToken string `ini:"aws_security_token"`
+	AWSAccessKey     string    `ini:"aws_access_key_id"`
+	AWSSecretKey     string    `ini:"aws_secret_access_key"`
+	AWSSessionToken  string    `ini:"aws_session_token"`
+	AWSSecurityToken string    `ini:"aws_security_token"`
+	PrincipalARN     string    `ini:"x_principal_arn"`
+	Expires          time.Time `ini:"x_security_token_expires"`
 }
 
 // CredentialsProvider loads aws credentials file
@@ -46,7 +49,7 @@ func NewSharedCredentials(profile string) *CredentialsProvider {
 
 // CredsExists verify that the credentials exist
 func (p *CredentialsProvider) CredsExists() (bool, error) {
-	filename, err := p.filename()
+	filename, err := p.resolveFilename()
 	if err != nil {
 		return false, err
 	}
@@ -63,8 +66,8 @@ func (p *CredentialsProvider) CredsExists() (bool, error) {
 }
 
 // Save persist the credentials
-func (p *CredentialsProvider) Save(id, secret, token string) error {
-	filename, err := p.filename()
+func (p *CredentialsProvider) Save(awsCreds *AWSCredentials) error {
+	filename, err := p.resolveFilename()
 	if err != nil {
 		return err
 	}
@@ -72,44 +75,44 @@ func (p *CredentialsProvider) Save(id, secret, token string) error {
 	err = p.ensureConfigExists()
 	if err != nil {
 		if os.IsNotExist(err) {
-			return createAndSaveProfile(filename, p.Profile, id, secret, token)
+			return createAndSaveProfile(filename, p.Profile, awsCreds)
 		}
 		return errors.Wrap(err, "unable to load file")
 	}
 
-	return saveProfile(filename, p.Profile, id, secret, token)
+	return saveProfile(filename, p.Profile, awsCreds)
 }
 
 // Load load the aws credentials file
-func (p *CredentialsProvider) Load() (string, string, string, error) {
-	filename, err := p.filename()
+func (p *CredentialsProvider) Load() (*AWSCredentials, error) {
+	filename, err := p.resolveFilename()
 	if err != nil {
-		return "", "", "", err
+		return nil, err
 	}
 
 	config, err := ini.Load(filename)
 	if err != nil {
-		return "", "", "", err
+		return nil, err
 	}
 
 	iniProfile, err := config.GetSection(p.Profile)
 	if err != nil {
-		return "", "", "", ErrCredentialsNotFound
+		return nil, ErrCredentialsNotFound
 	}
 
 	awsCreds := new(AWSCredentials)
 
 	err = iniProfile.MapTo(awsCreds)
 	if err != nil {
-		return "", "", "", ErrCredentialsNotFound
+		return nil, ErrCredentialsNotFound
 	}
 
-	return awsCreds.AWSAccessKey, awsCreds.AWSSecretKey, awsCreds.AWSSecurityToken, nil
+	return awsCreds, nil
 }
 
 // ensureConfigExists verify that the config file exists
 func (p *CredentialsProvider) ensureConfigExists() error {
-	filename, err := p.filename()
+	filename, err := p.resolveFilename()
 	if err != nil {
 		return err
 	}
@@ -142,31 +145,43 @@ func (p *CredentialsProvider) ensureConfigExists() error {
 	return nil
 }
 
-func (p *CredentialsProvider) filename() (string, error) {
+func (p *CredentialsProvider) resolveFilename() (string, error) {
 	if p.Filename == "" {
-		if p.Filename = os.Getenv("AWS_SHARED_CREDENTIALS_FILE"); p.Filename != "" {
-			return p.Filename, nil
-		}
-
-		name, err := homedir.Expand("~/.aws/credentials")
+		filename, err := locateConfigFile()
 		if err != nil {
-			return "", ErrCredentialsHomeNotFound
+			return "", err
 		}
 
-		logger.WithField("name", name).Debug("Expand")
+		p.Filename = filename
+	}
 
-		// is the filename a symlink?
-		name, err = resolveSymlink(name)
-		if err != nil {
-			return "", errors.Wrap(err, "unable to resolve symlink")
-		}
+	return p.Filename, nil
+}
+
+func locateConfigFile() (string, error) {
+
+	filename := os.Getenv("AWS_SHARED_CREDENTIALS_FILE")
 
-		logger.WithField("name", name).Debug("resolveSymlink")
+	if filename != "" {
+		return filename, nil
+	}
 
-		p.Filename = name
+	name, err := homedir.Expand("~/.aws/credentials")
+	if err != nil {
+		return "", ErrCredentialsHomeNotFound
 	}
 
-	return p.Filename, nil
+	logger.WithField("name", name).Debug("Expand")
+
+	// is the filename a symlink?
+	name, err = resolveSymlink(name)
+	if err != nil {
+		return "", errors.Wrap(err, "unable to resolve symlink")
+	}
+
+	logger.WithField("name", name).Debug("resolveSymlink")
+
+	return name, nil
 }
 
 func resolveSymlink(filename string) (string, error) {
@@ -183,7 +198,7 @@ func resolveSymlink(filename string) (string, error) {
 	return sympath, nil
 }
 
-func createAndSaveProfile(filename, profile, id, secret, token string) error {
+func createAndSaveProfile(filename, profile string, awsCreds *AWSCredentials) error {
 
 	dirPath := filepath.Dir(filename)
 
@@ -197,10 +212,10 @@ func createAndSaveProfile(filename, profile, id, secret, token string) error {
 		return errors.Wrapf(err, "unable to create configuration")
 	}
 
-	return saveProfile(filename, profile, id, secret, token)
+	return saveProfile(filename, profile, awsCreds)
 }
 
-func saveProfile(filename, profile, id, secret, token string) error {
+func saveProfile(filename, profile string, awsCreds *AWSCredentials) error {
 	config, err := ini.Load(filename)
 	if err != nil {
 		return err
@@ -210,12 +225,7 @@ func saveProfile(filename, profile, id, secret, token string) error {
 		return err
 	}
 
-	err = iniProfile.ReflectFrom(&AWSCredentials{
-		AWSAccessKey:     id,
-		AWSSecretKey:     secret,
-		AWSSessionToken:  token,
-		AWSSecurityToken: token,
-	})
+	err = iniProfile.ReflectFrom(awsCreds)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go
index dff6e37..0311849 100644
--- a/pkg/cfg/cfg.go
+++ b/pkg/cfg/cfg.go
@@ -19,6 +19,10 @@ const (
 	// DefaultAmazonWebservicesURN URN used when authenticating to aws using SAML
 	// NOTE: This only needs to be changed to log into GovCloud
 	DefaultAmazonWebservicesURN = "urn:amazon:webservices"
+
+	// DefaultSessionDuration this is the default session duration which can be overridden in the AWS console
+	// see https://aws.amazon.com/blogs/security/enable-federated-api-access-to-your-aws-resources-for-up-to-12-hours-using-iam-roles/
+	DefaultSessionDuration = 3600
 )
 
 // IDPAccount saml IDP account
@@ -30,6 +34,7 @@ type IDPAccount struct {
 	SkipVerify           bool   `ini:"skip_verify"`
 	Timeout              int    `ini:"timeout"`
 	AmazonWebservicesURN string `ini:"aws_urn"`
+	SessionDuration      int    `ini:"aws_session_duration"`
 }
 
 // Validate validate the required / expected fields are set
@@ -58,6 +63,7 @@ func (ia *IDPAccount) Validate() error {
 func NewIDPAccount() *IDPAccount {
 	return &IDPAccount{
 		AmazonWebservicesURN: DefaultAmazonWebservicesURN,
+		SessionDuration:      DefaultSessionDuration,
 	}
 }
 
diff --git a/pkg/dump/dump.go b/pkg/dump/dump.go
index af9415e..99ab98d 100644
--- a/pkg/dump/dump.go
+++ b/pkg/dump/dump.go
@@ -3,11 +3,12 @@ package dump
 import (
 	"net/http"
 	"net/http/httputil"
+	"os"
 )
 
 // RequestString helper method to dump the http request
 func RequestString(req *http.Request) string {
-	data, err := httputil.DumpRequest(req, false)
+	data, err := httputil.DumpRequest(req, dumpContentEnable())
 
 	if err != nil {
 		return ""
@@ -18,7 +19,7 @@ func RequestString(req *http.Request) string {
 
 // ResponseString helper method to dump the http response
 func ResponseString(res *http.Response) string {
-	data, err := httputil.DumpResponse(res, false)
+	data, err := httputil.DumpResponse(res, dumpContentEnable())
 
 	if err != nil {
 		return ""
@@ -26,3 +27,7 @@ func ResponseString(res *http.Response) string {
 
 	return string(data)
 }
+
+func dumpContentEnable() bool {
+	return os.Getenv("DUMP_CONTENT") == "true"
+}
diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go
index 956dffd..265f0df 100644
--- a/pkg/flags/flags.go
+++ b/pkg/flags/flags.go
@@ -1,6 +1,8 @@
 package flags
 
-import "github.com/versent/saml2aws/pkg/cfg"
+import (
+	"github.com/versent/saml2aws/pkg/cfg"
+)
 
 // CommonFlags flags common to all of the `saml2aws` commands (except `help`)
 type CommonFlags struct {
@@ -12,6 +14,7 @@ type CommonFlags struct {
 	Password             string
 	RoleArn              string
 	AmazonWebservicesURN string
+	SessionDuration      int
 	SkipPrompt           bool
 	SkipVerify           bool
 }
@@ -52,4 +55,8 @@ func ApplyFlagOverrides(commonFlags *CommonFlags, account *cfg.IDPAccount) {
 	if commonFlags.AmazonWebservicesURN != "" {
 		account.AmazonWebservicesURN = commonFlags.AmazonWebservicesURN
 	}
+
+	if commonFlags.SessionDuration != 0 {
+		account.SessionDuration = commonFlags.SessionDuration
+	}
 }
diff --git a/pkg/provider/okta/okta.go b/pkg/provider/okta/okta.go
index 354812d..8bd1f9f 100644
--- a/pkg/provider/okta/okta.go
+++ b/pkg/provider/okta/okta.go
@@ -160,6 +160,8 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		return samlAssertion, errors.Wrap(err, "unable to locate saml response")
 	}
 
+	logger.WithField("samlAssertion", samlAssertion).Debug("complete")
+
 	return samlAssertion, nil
 }
 
diff --git a/pkg/shell/env.go b/pkg/shell/env.go
index 1c3a083..3d6c1e6 100644
--- a/pkg/shell/env.go
+++ b/pkg/shell/env.go
@@ -1,14 +1,18 @@
 package shell
 
-import "fmt"
+import (
+	"fmt"
+
+	"github.com/versent/saml2aws/pkg/awsconfig"
+)
 
 // BuildEnvVars build an array of env vars in the format required for exec
-func BuildEnvVars(id, secret, token string) []string {
+func BuildEnvVars(awsCreds *awsconfig.AWSCredentials) []string {
 	return []string{
-		fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", id),
-		fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", secret),
-		fmt.Sprintf("AWS_SESSION_TOKEN=%s", token),
-		fmt.Sprintf("AWS_SECURITY_TOKEN=%s", token),
-		fmt.Sprintf("EC2_SECURITY_TOKEN=%s", token),
+		fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", awsCreds.AWSAccessKey),
+		fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", awsCreds.AWSSecretKey),
+		fmt.Sprintf("AWS_SESSION_TOKEN=%s", awsCreds.AWSSessionToken),
+		fmt.Sprintf("AWS_SECURITY_TOKEN=%s", awsCreds.AWSSecurityToken),
+		fmt.Sprintf("EC2_SECURITY_TOKEN=%s", awsCreds.AWSSecurityToken),
 	}
 }
diff --git a/saml.go b/saml.go
index c517622..e3dacb8 100644
--- a/saml.go
+++ b/saml.go
@@ -2,6 +2,7 @@ package saml2aws
 
 import (
 	"fmt"
+	"strconv"
 
 	"github.com/beevik/etree"
 )
@@ -33,6 +34,43 @@ func (e ErrMissingElement) Error() string {
 	return fmt.Sprintf("missing %s element", e.Tag)
 }
 
+// ExtractSessionDuration this will attempt to extract a session duration from the assertion
+// see https://aws.amazon.com/SAML/Attributes/SessionDuration
+func ExtractSessionDuration(data []byte) (int64, error) {
+
+	doc := etree.NewDocument()
+	if err := doc.ReadFromBytes(data); err != nil {
+		return 0, err
+	}
+
+	assertionElement := doc.FindElement(".//Assertion")
+	if assertionElement == nil {
+		return 0, ErrMissingAssertion
+	}
+
+	// log.Printf("tag: %s", assertionElement.Tag)
+
+	//Get the actual assertion attributes
+	attributeStatement := assertionElement.FindElement(childPath(assertionElement.Space, attributeStatementTag))
+	if attributeStatement == nil {
+		return 0, ErrMissingElement{Tag: attributeStatementTag}
+	}
+
+	attributes := attributeStatement.FindElements(childPath(assertionElement.Space, attributeTag))
+
+	for _, attribute := range attributes {
+		if attribute.SelectAttrValue("Name", "") != "https://aws.amazon.com/SAML/Attributes/SessionDuration" {
+			continue
+		}
+		atributeValues := attribute.FindElements(childPath(assertionElement.Space, attributeValueTag))
+		for _, attrValue := range atributeValues {
+			return strconv.ParseInt(attrValue.Text(), 10, 64)
+		}
+	}
+
+	return 0, nil
+}
+
 // ExtractAwsRoles given an assertion document extract the aws roles
 func ExtractAwsRoles(data []byte) ([]string, error) {
 


Test patch:
diff --git a/pkg/awsconfig/awsconfig_test.go b/pkg/awsconfig/awsconfig_test.go
index 3217b1a..7d8c5f8 100644
--- a/pkg/awsconfig/awsconfig_test.go
+++ b/pkg/awsconfig/awsconfig_test.go
@@ -4,26 +4,37 @@ import (
 	"os"
 	"testing"
 
+	"github.com/sirupsen/logrus"
+
 	"github.com/stretchr/testify/assert"
 )
 
 func TestUpdateSamlConfig(t *testing.T) {
 	os.Remove(".credentials")
 
+	logrus.SetLevel(logrus.DebugLevel)
+
 	sharedCreds := &CredentialsProvider{".credentials", "saml"}
 
 	exist, err := sharedCreds.CredsExists()
 	assert.Nil(t, err)
 	assert.True(t, exist)
 
-	err = sharedCreds.Save("testid", "testsecret", "testtoken")
+	awsCreds := &AWSCredentials{
+		AWSAccessKey:     "testid",
+		AWSSecretKey:     "testsecret",
+		AWSSessionToken:  "testtoken",
+		AWSSecurityToken: "testtoken",
+	}
+
+	err = sharedCreds.Save(awsCreds)
 	assert.Nil(t, err)
 
-	id, secret, token, err := sharedCreds.Load()
+	awsCreds, err = sharedCreds.Load()
 	assert.Nil(t, err)
-	assert.Equal(t, "testid", id)
-	assert.Equal(t, "testsecret", secret)
-	assert.Equal(t, "testtoken", token)
+	assert.Equal(t, "testid", awsCreds.AWSAccessKey)
+	assert.Equal(t, "testsecret", awsCreds.AWSSecretKey)
+	assert.Equal(t, "testtoken", awsCreds.AWSSessionToken)
 
 	os.Remove(".credentials")
 }
diff --git a/pkg/cfg/cfg_test.go b/pkg/cfg/cfg_test.go
index 791dadb..5319f4d 100644
--- a/pkg/cfg/cfg_test.go
+++ b/pkg/cfg/cfg_test.go
@@ -32,12 +32,14 @@ func TestNewConfigManagerLoad(t *testing.T) {
 		Provider:             "keycloak",
 		MFA:                  "sms",
 		AmazonWebservicesURN: DefaultAmazonWebservicesURN,
+		SessionDuration:      3600,
 	}, idpAccount)
 
 	idpAccount, err = cfgm.LoadIDPAccount("test1234")
 	require.Nil(t, err)
 	require.Equal(t, &IDPAccount{
 		AmazonWebservicesURN: DefaultAmazonWebservicesURN,
+		SessionDuration:      3600,
 	}, idpAccount)
 }
 
@@ -56,6 +58,7 @@ func TestNewConfigManagerLoadVerify(t *testing.T) {
 		Provider:             "keycloak",
 		MFA:                  "sms",
 		AmazonWebservicesURN: DefaultAmazonWebservicesURN,
+		SessionDuration:      3600,
 	}, idpAccount)
 
 	idpAccount, err = cfgm.LoadVerifyIDPAccount("test1234")
diff --git a/pkg/shell/env_test.go b/pkg/shell/env_test.go
index 4d175eb..b594fa7 100644
--- a/pkg/shell/env_test.go
+++ b/pkg/shell/env_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/versent/saml2aws/pkg/awsconfig"
 )
 
 func TestBuildEnvVars(t *testing.T) {
@@ -16,5 +17,12 @@ func TestBuildEnvVars(t *testing.T) {
 		"EC2_SECURITY_TOKEN=567",
 	}
 
-	assert.Equal(t, expectedArray, BuildEnvVars("123", "345", "567"))
+	awsCreds := &awsconfig.AWSCredentials{
+		AWSAccessKey:     "123",
+		AWSSecretKey:     "345",
+		AWSSecurityToken: "567",
+		AWSSessionToken:  "567",
+	}
+
+	assert.Equal(t, expectedArray, BuildEnvVars(awsCreds))
 }
diff --git a/saml_test.go b/saml_test.go
index f6272e2..aa5d8e0 100644
--- a/saml_test.go
+++ b/saml_test.go
@@ -15,3 +15,12 @@ func TestExtractAwsRoles(t *testing.T) {
 	assert.Nil(t, err)
 	assert.Len(t, roles, 2)
 }
+
+func TestExtractSessionDuration(t *testing.T) {
+	data, err := ioutil.ReadFile("testdata/assertion.xml")
+	assert.Nil(t, err)
+
+	duration, err := ExtractSessionDuration(data)
+	assert.Nil(t, err)
+	assert.Equal(t, int64(28800), duration)
+}
diff --git a/testdata/assertion.xml b/testdata/assertion.xml
index 68c98cb..f22b940 100644
--- a/testdata/assertion.xml
+++ b/testdata/assertion.xml
@@ -44,6 +44,9 @@
         <AttributeValue>arn:aws:iam::123123123123:saml-provider/ExampleADFS,arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSBuild</AttributeValue>
         <AttributeValue>arn:aws:iam::123123123123:saml-provider/ExampleADFS,arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd</AttributeValue>
       </Attribute>
+      <saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/SessionDuration" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">28800</saml2:AttributeValue>
+      </saml2:Attribute>
     </AttributeStatement>
     <AuthnStatement AuthnInstant="2016-09-10T02:54:39.227Z" SessionIndex="_f85be5f5-584c-4711-8c9d-5b13c4c49f89">
       <AuthnContext>