Repository: Versent/saml2aws
Pull request #78 (2017-10-31 10:40:47)

doc(README) Updated readme and some more testing and fixes


Patch:
diff --git a/Makefile b/Makefile
index 9d77937..20aaf02 100644
--- a/Makefile
+++ b/Makefile
@@ -42,6 +42,10 @@ compile: deps
 lint:
 	gometalinter --vendor ./...
 
+# gofmt and goimports all go files
+fmt:
+	find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done
+
 install:
 	go install ./cmd/saml2aws
 
@@ -64,4 +68,4 @@ clean:
 	rm ./glide
 	rm -fr ./build
 
-.PHONY: default deps compile lint dist release test clean
+.PHONY: default deps compile lint fmt dist release test clean
diff --git a/README.md b/README.md
index 4b55fff..b8f512f 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ How to Implement a General Solution for Federated API/CLI Access Using SAML 2.0]
 
 The process goes something like this:
 
+* Setup an account alias, either using the default or given a name
 * Prompt user for credentials
 * Log in to Identity Provider using form based authentication
 * Build a SAML assertion containing AWS roles
@@ -37,11 +38,17 @@ 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).
-  -p, --profile="saml"  The AWS profile to save the temporary credentials
-  -s, --skip-verify     Skip verification of server certificate.
-  -i, --provider="ADFS" The type of SAML IDP provider.
-      --version         Show application version.
+      --help                   Show context-sensitive help (also try --help-long and --help-man).
+      --verbose                Enable verbose logging
+      --version                Show application version.
+  -a, --idp-account="default"  The name of the configured IDP account
+  -p, --profile="saml"         The AWS profile to save the temporary credentials
+  -s, --skip-verify            Skip verification of server certificate.
+      --hostname=HOSTNAME      The hostname 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.
+      --skip-prompt            Skip prompting for parameters during login.
 
 Commands:
   help [<command>...]
@@ -55,16 +62,11 @@ Commands:
   exec [<command>...]
     Exec the supplied command with env vars from STS token.
 
-```
-saml2aws will default to using ADFS 3.x as the Identity Provider. To use another provider, use the `--provider` flag:
 
-| IdP          |                         |
-| ------------ | ----------------------- |
-| ADFS 2.x     | `--provider=ADFS2`      |
-| PingFederate | `--provider=Ping`       |
-| JumpCloud    | `--provider=JumpCloud`  |
-| Okta         | `--provider=Okta`       |
-| KeyCloak     | `--provider=KeyCloak`   |
+  configure
+    Configure a new IDP account.
+
+```
 
 # Install
 
@@ -94,12 +96,12 @@ Install the AWS CLI see https://docs.aws.amazon.com/cli/latest/userguide/install
 brew install awscli
 ```
 
-Configure an empty default profile with your region of choice.
+Configure an empty default profile with your region of choice, note the credentials will be overwritten when you first login and are supplied to unsure `~/.aws/credentials` file is created.
 
 ```
 $ aws configure
-AWS Access Key ID [None]:
-AWS Secret Access Key [None]:
+AWS Access Key ID [None]: test
+AWS Secret Access Key [None]: test
 Default region name [None]: us-west-2
 Default output format [None]:
 ```
@@ -112,40 +114,12 @@ Log into a service.
 
 ```
 $ saml2aws login
-Hostname [id.example.com]:
-Username [mark.wolfe@example.com]:
-Password: ************
-
-ADFS https://id.example.com
-Authenticating to ADFS...
-Please choose the role you would like to assume:
-[ 0 ]:  arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSBuild
-[ 1 ]:  arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
-Selection: 1
-Selected role: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
-Requesting AWS credentials using SAML assertion
-Saving credentials
-Logged in as: arn:aws:sts::123123123123:assumed-role/AWS-Admin-CloudOPSNonProd/wolfeidau@example.com
-
-Your new access key pair has been stored in the AWS configuration
-Note that it will expire at 2016-09-19 15:59:49 +1000 AEST
-To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile saml ec2 describe-instances).
-```
-
-Run ansible with an expired token present, `exec` verifies the token and requests login.
-
-```
-$ saml2aws exec --skip-verify -- ansible-playbook -e "aws_region=ap-southeast-2" playbook.yml
-Hostname [id.example.com]:
+Using IDP Account default to access Ping https://id.example.com
+To use saved password just hit enter.
 Username [mark.wolfe@example.com]:
 Password: ************
 
-ADFS https://id.example.com
-Authenticating to ADFS...
-Please choose the role you would like to assume:
-[ 0 ]:  arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSBuild
-[ 1 ]:  arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
-Selection: 1
+Authenticating as mark.wolfe@example.com ...
 Selected role: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
 Requesting AWS credentials using SAML assertion
 Saving credentials
@@ -154,14 +128,6 @@ Logged in as: arn:aws:sts::123123123123:assumed-role/AWS-Admin-CloudOPSNonProd/w
 Your new access key pair has been stored in the AWS configuration
 Note that it will expire at 2016-09-19 15:59:49 +1000 AEST
 To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile saml ec2 describe-instances).
-
-PLAY [create cloudformation stack] *************************************************
-
-...
-
-PLAY RECAP *********************************************************************
-localhost                  : ok=2    changed=0    unreachable=0    failed=0
-
 ```
 
 # Building
diff --git a/cmd/saml2aws/commands/login.go b/cmd/saml2aws/commands/login.go
index 39e5aa3..c5edf17 100644
--- a/cmd/saml2aws/commands/login.go
+++ b/cmd/saml2aws/commands/login.go
@@ -16,6 +16,9 @@ import (
 	"github.com/versent/saml2aws/pkg/creds"
 )
 
+// MaxDurationSeconds the maximum duration in seconds for an STS session
+const MaxDurationSeconds = 3600
+
 // LoginFlags login specific command flags
 type LoginFlags struct {
 	IdpAccount string
@@ -38,28 +41,12 @@ func (lf *LoginFlags) RoleSupplied() bool {
 // Login login to ADFS
 func Login(loginFlags *LoginFlags) error {
 
-	cfgm, err := cfg.NewConfigManager(cfg.DefaultConfigPath)
+	account, err := buildIdpAccount(loginFlags)
 	if err != nil {
-		return errors.Wrap(err, "failed to load configuration")
+		return errors.Wrap(err, "error building login details")
 	}
 
-	account, err := cfgm.LoadVerifyIDPAccount(loginFlags.IdpAccount)
-	if err != nil {
-		if cfg.IsErrIdpAccountNotFound(err) {
-			fmt.Printf("%v\n", err)
-			os.Exit(1)
-		}
-		return errors.Wrap(err, "failed to load idp account")
-	}
-
-	// update username and hostname if supplied
-	applyFlagOverrides(loginFlags, account)
-
-	loginDetails := &creds.LoginDetails{Hostname: account.Hostname, Username: account.Username}
-
-	fmt.Printf("Using IDP Account %s to access %s https://%s\n", loginFlags.IdpAccount, account.Provider, account.Hostname)
-
-	err = resolveLoginDetails(loginDetails, loginFlags)
+	loginDetails, err := resolveLoginDetails(account, loginFlags)
 	if err != nil {
 		fmt.Printf("%+v\n", err)
 		os.Exit(1)
@@ -67,14 +54,14 @@ func Login(loginFlags *LoginFlags) error {
 
 	fmt.Printf("Authenticating as %s ...\n", account.Username)
 
-	provider, err := saml2aws.NewSAMLClient(account)
+	err = loginDetails.Validate()
 	if err != nil {
-		return errors.Wrap(err, "error building IdP client")
+		return errors.Wrap(err, "error validating login details")
 	}
 
-	err = loginDetails.Validate()
+	provider, err := saml2aws.NewSAMLClient(account)
 	if err != nil {
-		return errors.Wrap(err, "error validating login details")
+		return errors.Wrap(err, "error building IdP client")
 	}
 
 	samlAssertion, err := provider.Authenticate(loginDetails)
@@ -122,53 +109,52 @@ func Login(loginFlags *LoginFlags) error {
 
 	fmt.Println("Selected role:", role.RoleARN)
 
-	sess, err := session.NewSession()
+	err = loginToStsUsingRole(role, samlAssertion, loginFlags.Profile)
 	if err != nil {
-		return errors.Wrap(err, "failed to create session")
+		return errors.Wrap(err, "error logging into aws role using saml assertion")
 	}
 
-	svc := sts.New(sess)
+	return nil
+}
 
-	params := &sts.AssumeRoleWithSAMLInput{
-		PrincipalArn:    aws.String(role.PrincipalARN), // Required
-		RoleArn:         aws.String(role.RoleARN),      // Required
-		SAMLAssertion:   aws.String(samlAssertion),     // Required
-		DurationSeconds: aws.Int64(3600),               // 1 hour
+func buildIdpAccount(loginFlags *LoginFlags) (*cfg.IDPAccount, error) {
+	cfgm, err := cfg.NewConfigManager(cfg.DefaultConfigPath)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to load configuration")
 	}
 
-	fmt.Println("Requesting AWS credentials using SAML assertion")
-
-	resp, err := svc.AssumeRoleWithSAML(params)
+	account, err := cfgm.LoadVerifyIDPAccount(loginFlags.IdpAccount)
 	if err != nil {
-		return errors.Wrap(err, "error retrieving STS credentials using SAML")
+		if cfg.IsErrIdpAccountNotFound(err) {
+			fmt.Printf("%v\n", err)
+			os.Exit(1)
+		}
+		return nil, errors.Wrap(err, "failed to load idp account")
 	}
 
-	// fmt.Println("Saving credentials")
-
-	sharedCreds := awsconfig.NewSharedCredentials(loginFlags.Profile)
+	// update username and hostname if supplied
+	applyFlagOverrides(loginFlags, account)
 
-	err = sharedCreds.Save(aws.StringValue(resp.Credentials.AccessKeyId), aws.StringValue(resp.Credentials.SecretAccessKey), aws.StringValue(resp.Credentials.SessionToken))
+	err = account.Validate()
 	if err != nil {
-		return errors.Wrap(err, "error saving credentials")
+		return nil, errors.Wrap(err, "failed to validate account")
 	}
 
-	fmt.Println("Logged in as:", aws.StringValue(resp.AssumedRoleUser.Arn))
-	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.Println("To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile", loginFlags.Profile, "ec2 describe-instances).")
-
-	return nil
+	return account, nil
 }
 
-func resolveLoginDetails(loginDetails *creds.LoginDetails, loginFlags *LoginFlags) error {
+func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *LoginFlags) (*creds.LoginDetails, error) {
 
 	// fmt.Printf("loginFlags %+v\n", loginFlags)
 
+	loginDetails := &creds.LoginDetails{Hostname: account.Hostname, Username: account.Username}
+
+	fmt.Printf("Using IDP Account %s to access %s https://%s\n", loginFlags.IdpAccount, account.Provider, account.Hostname)
+
 	err := credentials.LookupCredentials(loginDetails)
 	if err != nil {
 		if !credentials.IsErrCredentialsNotFound(err) {
-			return errors.Wrap(err, "error loading saved password")
+			return nil, errors.Wrap(err, "error loading saved password")
 		}
 	}
 
@@ -188,14 +174,15 @@ func resolveLoginDetails(loginDetails *creds.LoginDetails, loginFlags *LoginFlag
 
 	// if skip prompt was passed just pass back the flag values
 	if loginFlags.SkipPrompt {
-		return nil
+		return loginDetails, nil
 	}
 
 	err = saml2aws.PromptForLoginDetails(loginDetails)
 	if err != nil {
-		return errors.Wrap(err, "Error occured accepting input")
+		return nil, errors.Wrap(err, "Error occurred accepting input")
 	}
-	return nil
+
+	return loginDetails, nil
 }
 
 func resolveRole(awsRoles []*saml2aws.AWSRole, samlAssertion string, loginFlags *LoginFlags) (*saml2aws.AWSRole, error) {
@@ -232,6 +219,47 @@ func resolveRole(awsRoles []*saml2aws.AWSRole, samlAssertion string, loginFlags
 	return role, nil
 }
 
+func loginToStsUsingRole(role *saml2aws.AWSRole, samlAssertion string, profile string) error {
+
+	sess, err := session.NewSession()
+	if err != nil {
+		return errors.Wrap(err, "failed to create session")
+	}
+
+	svc := sts.New(sess)
+
+	params := &sts.AssumeRoleWithSAMLInput{
+		PrincipalArn:    aws.String(role.PrincipalARN), // Required
+		RoleArn:         aws.String(role.RoleARN),      // Required
+		SAMLAssertion:   aws.String(samlAssertion),     // Required
+		DurationSeconds: aws.Int64(MaxDurationSeconds), // 1 hour
+	}
+
+	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")
+	}
+
+	// fmt.Println("Saving credentials")
+
+	sharedCreds := awsconfig.NewSharedCredentials(profile)
+
+	err = sharedCreds.Save(aws.StringValue(resp.Credentials.AccessKeyId), aws.StringValue(resp.Credentials.SecretAccessKey), aws.StringValue(resp.Credentials.SessionToken))
+	if err != nil {
+		return errors.Wrap(err, "error saving credentials")
+	}
+
+	fmt.Println("Logged in as:", aws.StringValue(resp.AssumedRoleUser.Arn))
+	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.Println("To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile", profile, "ec2 describe-instances).")
+
+	return nil
+}
+
 func applyFlagOverrides(loginFlags *LoginFlags, account *cfg.IDPAccount) {
 	if loginFlags.Hostname != "" {
 		account.Hostname = loginFlags.Hostname
diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go
index a3c2ef4..75d619e 100644
--- a/pkg/cfg/cfg.go
+++ b/pkg/cfg/cfg.go
@@ -23,6 +23,23 @@ type IDPAccount struct {
 	Timeout    int    `ini:"timeout"`
 }
 
+// Validate validate the required / exptected fields are set
+func (ia *IDPAccount) Validate() error {
+	if ia.Hostname == "" {
+		return errors.New("Hostname empty in idp account")
+	}
+
+	if ia.Provider == "" {
+		return errors.New("Provider empty in idp account")
+	}
+
+	if ia.MFA == "" {
+		return errors.New("MFA empty in idp account")
+	}
+
+	return nil
+}
+
 // ConfigManager manage the various IDP account settings
 type ConfigManager struct {
 	configPath string
diff --git a/pkg/dump/dump.go b/pkg/dump/dump.go
index 2821862..af9415e 100644
--- a/pkg/dump/dump.go
+++ b/pkg/dump/dump.go
@@ -5,6 +5,7 @@ import (
 	"net/http/httputil"
 )
 
+// RequestString helper method to dump the http request
 func RequestString(req *http.Request) string {
 	data, err := httputil.DumpRequest(req, false)
 
@@ -15,6 +16,7 @@ func RequestString(req *http.Request) string {
 	return string(data)
 }
 
+// ResponseString helper method to dump the http response
 func ResponseString(res *http.Response) string {
 	data, err := httputil.DumpResponse(res, false)
 
diff --git a/pkg/provider/adfs/adfs.go b/pkg/provider/adfs/adfs.go
index 6d2b82c..54429d2 100644
--- a/pkg/provider/adfs/adfs.go
+++ b/pkg/provider/adfs/adfs.go
@@ -145,7 +145,7 @@ func (ac *Client) vipMFA(authSubmitURL string, res *http.Response) (*http.Respon
 	vipIndex := doc.Find("input#authMethod[value=VIPAuthenticationProviderWindowsAccountName]").Index()
 
 	if vipIndex == -1 {
-		return nil, errors.New("unable to find VIP challenge, no MFA found")
+		return res, nil // if we didn't find the MFA flag then just continue
 	}
 
 	var token = ac.prompter.RequestSecurityCode("000000")
diff --git a/pkg/provider/jumpcloud/jumpcloud.go b/pkg/provider/jumpcloud/jumpcloud.go
index 93d0cd1..ea022c2 100644
--- a/pkg/provider/jumpcloud/jumpcloud.go
+++ b/pkg/provider/jumpcloud/jumpcloud.go
@@ -113,7 +113,7 @@ func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		token := prompt.StringRequired("MFA Token")
 		authForm.Add("otp", token)
 
-		req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode()))
+		req, err = http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode()))
 		if err != nil {
 			return samlAssertion, errors.Wrap(err, "error building MFA authentication request")
 		}
diff --git a/pkg/provider/okta/okta.go b/pkg/provider/okta/okta.go
index a592dc2..9ab6c77 100644
--- a/pkg/provider/okta/okta.go
+++ b/pkg/provider/okta/okta.go
@@ -117,12 +117,12 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		// get duo host, signature & callback
 		verifyReq := VerifyRequest{StateToken: stateToken}
 		verifyBody := new(bytes.Buffer)
-		err := json.NewEncoder(verifyBody).Encode(verifyReq)
+		err = json.NewEncoder(verifyBody).Encode(verifyReq)
 		if err != nil {
 			return samlAssertion, errors.Wrap(err, "error encoding verifyReq")
 		}
 
-		req, err := http.NewRequest("POST", oktaVerify, verifyBody)
+		req, err = http.NewRequest("POST", oktaVerify, verifyBody)
 		if err != nil {
 			return samlAssertion, errors.Wrap(err, "error building verify request")
 		}
@@ -130,12 +130,15 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		req.Header.Add("Content-Type", "application/json")
 		req.Header.Add("Accept", "application/json")
 
-		res, err := oc.client.Do(req)
+		res, err = oc.client.Do(req)
 		if err != nil {
 			return samlAssertion, errors.Wrap(err, "error retrieving verify response")
 		}
 
 		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return samlAssertion, errors.Wrap(err, "error retrieving body from response")
+		}
 		resp = string(body)
 
 		duoHost := gjson.Get(resp, "_embedded.factor._embedded.verification.host").String()
@@ -220,6 +223,10 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		}
 
 		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return samlAssertion, errors.Wrap(err, "error retrieving body from response")
+		}
+
 		resp = string(body)
 
 		duoTxStat := gjson.Get(resp, "stat").String()
@@ -248,6 +255,10 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		}
 
 		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return samlAssertion, errors.Wrap(err, "error retrieving body from response")
+		}
+
 		resp = string(body)
 
 		duoTxResult := gjson.Get(resp, "response.result").String()
@@ -273,7 +284,11 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 				}
 
 				body, err = ioutil.ReadAll(res.Body)
-				resp := string(body)
+				if err != nil {
+					return samlAssertion, errors.Wrap(err, "error retrieving body from response")
+				}
+
+				resp = string(body)
 
 				duoTxResult = gjson.Get(resp, "response.result").String()
 				duoTxCookie = gjson.Get(resp, "response.cookie").String()
@@ -303,7 +318,7 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 
 		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 
-		res, err = oc.client.Do(req)
+		_, err = oc.client.Do(req)
 		if err != nil {
 			return samlAssertion, errors.Wrap(err, "error retrieving verify response")
 		}
diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go
index d911f93..2a8249c 100644
--- a/pkg/provider/pingfed/pingfed.go
+++ b/pkg/provider/pingfed/pingfed.go
@@ -154,6 +154,9 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 
 		//extract form action and csrf token
 		form, actionURL, err = extractFormData(res)
+		if err != nil {
+			return "", errors.Wrap(err, "error extracting authentication form")
+		}
 
 		//contine mfa auth with csrf token
 		req, err = http.NewRequest("POST", actionURL, strings.NewReader(form.Encode()))


Test patch:
diff --git a/aws_account_test.go b/aws_account_test.go
index a45229e..9ea4ec1 100644
--- a/aws_account_test.go
+++ b/aws_account_test.go
@@ -37,16 +37,16 @@ func TestExtractAWSAccounts(t *testing.T) {
 
 func TestAssignPrincipals(t *testing.T) {
 	awsRoles := []*AWSRole{
-		&AWSRole{
+		{
 			PrincipalARN: "arn:aws:iam::000000000001:saml-provider/test-idp",
 			RoleARN:      "arn:aws:iam::000000000001:role/Development",
 		},
 	}
 
 	awsAccounts := []*AWSAccount{
-		&AWSAccount{
+		{
 			Roles: []*AWSRole{
-				&AWSRole{
+				{
 					RoleARN: "arn:aws:iam::000000000001:role/Development",
 				},
 			},
@@ -60,11 +60,11 @@ func TestAssignPrincipals(t *testing.T) {
 
 func TestLocateRole(t *testing.T) {
 	awsRoles := []*AWSRole{
-		&AWSRole{
+		{
 			PrincipalARN: "arn:aws:iam::000000000001:saml-provider/test-idp",
 			RoleARN:      "arn:aws:iam::000000000001:role/Development",
 		},
-		&AWSRole{
+		{
 			PrincipalARN: "arn:aws:iam::000000000002:saml-provider/test-idp",
 			RoleARN:      "arn:aws:iam::000000000002:role/Development",
 		},
diff --git a/cmd/saml2aws/commands/login_test.go b/cmd/saml2aws/commands/login_test.go
index 72cb30e..0ab2ea7 100644
--- a/cmd/saml2aws/commands/login_test.go
+++ b/cmd/saml2aws/commands/login_test.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/versent/saml2aws"
+	"github.com/versent/saml2aws/pkg/cfg"
 	"github.com/versent/saml2aws/pkg/creds"
 )
 
@@ -12,12 +13,16 @@ func TestResolveLoginDetailsWithFlags(t *testing.T) {
 
 	loginFlags := &LoginFlags{Hostname: "id.example.com", Username: "wolfeidau", Password: "testtestlol", SkipPrompt: true}
 
-	loginDetails := &creds.LoginDetails{Hostname: "id.example.com", Username: ""}
-
-	err := resolveLoginDetails(loginDetails, loginFlags)
+	idpa := &cfg.IDPAccount{
+		Hostname: "id.example.com",
+		MFA:      "none",
+		Provider: "Ping",
+		Username: "wolfeidau",
+	}
+	loginDetails, err := resolveLoginDetails(idpa, loginFlags)
 
 	assert.Empty(t, err)
-	assert.Equal(t, loginDetails, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", Hostname: "id.example.com"})
+	assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", Hostname: "id.example.com"}, loginDetails)
 }
 
 func TestResolveRoleSingleEntry(t *testing.T) {