Repository: Versent/saml2aws
Pull request #79 (2017-11-01 23:18:18)

refactor(Hostname) Changed hostname to URL to better reflect current values.


Patch:
diff --git a/aws_account.go b/aws_account.go
index f485a93..9c3bedf 100644
--- a/aws_account.go
+++ b/aws_account.go
@@ -12,6 +12,8 @@ import (
 	"github.com/pkg/errors"
 )
 
+const awsURL = "https://signin.aws.amazon.com/saml"
+
 // AWSAccount holds the AWS account name and roles
 type AWSAccount struct {
 	Name  string
@@ -20,16 +22,15 @@ type AWSAccount struct {
 
 // ParseAWSAccounts extract the aws accounts from the saml assertion
 func ParseAWSAccounts(samlAssertion string) ([]*AWSAccount, error) {
-	awsURL := "https://signin.aws.amazon.com/saml"
 
 	res, err := http.PostForm(awsURL, url.Values{"SAMLResponse": {samlAssertion}})
 	if err != nil {
-		return nil, errors.Wrap(err, "error retieving AWS login form")
+		return nil, errors.Wrap(err, "error retrieving AWS login form")
 	}
 
 	data, err := ioutil.ReadAll(res.Body)
 	if err != nil {
-		return nil, errors.Wrap(err, "error retieving AWS login body")
+		return nil, errors.Wrap(err, "error retrieving AWS login body")
 	}
 
 	return ExtractAWSAccounts(data)
diff --git a/cmd/saml2aws/commands/login.go b/cmd/saml2aws/commands/login.go
index c5edf17..f0bbb20 100644
--- a/cmd/saml2aws/commands/login.go
+++ b/cmd/saml2aws/commands/login.go
@@ -23,8 +23,9 @@ const MaxDurationSeconds = 3600
 type LoginFlags struct {
 	IdpAccount string
 	//Provider   string
-	Profile    string
-	Hostname   string
+	Profile string
+	//Hostname   string
+	URL        string
 	Username   string
 	Password   string
 	RoleArn    string
@@ -76,7 +77,7 @@ func Login(loginFlags *LoginFlags) error {
 		os.Exit(1)
 	}
 
-	err = credentials.SaveCredentials(loginDetails.Hostname, loginDetails.Username, loginDetails.Password)
+	err = credentials.SaveCredentials(loginDetails.URL, loginDetails.Username, loginDetails.Password)
 	if err != nil {
 		return errors.Wrap(err, "error storing password in keychain")
 	}
@@ -147,9 +148,9 @@ func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *LoginFlags) (*cred
 
 	// fmt.Printf("loginFlags %+v\n", loginFlags)
 
-	loginDetails := &creds.LoginDetails{Hostname: account.Hostname, Username: account.Username}
+	loginDetails := &creds.LoginDetails{URL: account.URL, Username: account.Username}
 
-	fmt.Printf("Using IDP Account %s to access %s https://%s\n", loginFlags.IdpAccount, account.Provider, account.Hostname)
+	fmt.Printf("Using IDP Account %s to access %s %s\n", loginFlags.IdpAccount, account.Provider, account.URL)
 
 	err := credentials.LookupCredentials(loginDetails)
 	if err != nil {
@@ -261,8 +262,8 @@ func loginToStsUsingRole(role *saml2aws.AWSRole, samlAssertion string, profile s
 }
 
 func applyFlagOverrides(loginFlags *LoginFlags, account *cfg.IDPAccount) {
-	if loginFlags.Hostname != "" {
-		account.Hostname = loginFlags.Hostname
+	if loginFlags.URL != "" {
+		account.URL = loginFlags.URL
 	}
 
 	if loginFlags.Username != "" {
diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go
index bfcba7e..e4440a0 100644
--- a/cmd/saml2aws/main.go
+++ b/cmd/saml2aws/main.go
@@ -53,7 +53,7 @@ func configureLoginFlags(app *kingpin.Application) *commands.LoginFlags {
 	app.Flag("skip-verify", "Skip verification of server certificate.").Short('s').BoolVar(&c.SkipVerify)
 	// app.Flag("timeout", "Override the default HTTP client timeout in seconds.").Short('t').IntVar(&c.Timeout)
 	// app.Flag("provider", "The type of SAML IDP provider.").Short('i').Default("ADFS").EnumVar(&c.Provider, "ADFS", "ADFS2", "Ping", "JumpCloud", "Okta", "KeyCloak")
-	app.Flag("hostname", "The hostname of the SAML IDP server used to login.").StringVar(&c.Hostname)
+	app.Flag("URL", "The URL of the SAML IDP server used to login.").StringVar(&c.URL)
 	app.Flag("username", "The username used to login.").StringVar(&c.Username)
 	app.Flag("password", "The password used to login.").Envar("SAML2AWS_PASSWORD").StringVar(&c.Password)
 	app.Flag("role", "The ARN of the role to assume.").StringVar(&c.RoleArn)
diff --git a/helper/credentials/saml.go b/helper/credentials/saml.go
index 4d802ad..ce3bce1 100644
--- a/helper/credentials/saml.go
+++ b/helper/credentials/saml.go
@@ -9,7 +9,7 @@ import (
 // LookupCredentials lookup an existing set of credentials and validate it.
 func LookupCredentials(loginDetails *creds.LoginDetails) error {
 
-	username, password, err := CurrentHelper.Get(fmt.Sprintf("https://%s", loginDetails.Hostname))
+	username, password, err := CurrentHelper.Get(fmt.Sprintf("%s", loginDetails.URL))
 	if err != nil {
 		return err
 	}
@@ -21,10 +21,10 @@ func LookupCredentials(loginDetails *creds.LoginDetails) error {
 }
 
 // SaveCredentials save the user credentials.
-func SaveCredentials(hostname, username, password string) error {
+func SaveCredentials(url, username, password string) error {
 
 	creds := &Credentials{
-		ServerURL: fmt.Sprintf("https://%s", hostname),
+		ServerURL: fmt.Sprintf("%s", url),
 		Username:  username,
 		Secret:    password,
 	}
diff --git a/input.go b/input.go
index 0b25eca..0c13bc5 100644
--- a/input.go
+++ b/input.go
@@ -3,6 +3,7 @@ package saml2aws
 import (
 	"bufio"
 	"fmt"
+	"net/url"
 	"os"
 	"strconv"
 	"strings"
@@ -34,7 +35,7 @@ func PromptForConfigurationDetails(idpAccount *cfg.IDPAccount) error {
 
 	fmt.Println("")
 
-	idpAccount.Hostname = promptFor("Hostname [%s]", idpAccount.Hostname)
+	idpAccount.URL = promptForURL("URL [%s]", idpAccount.URL)
 	idpAccount.Username = promptFor("Username [%s]", idpAccount.Username)
 
 	fmt.Println("")
@@ -141,3 +142,25 @@ func promptFor(promptString, defaultValue string) string {
 
 	return val
 }
+
+func promptForURL(promptString, defaultValue string) string {
+	var rawURL string
+
+	// do while
+	for {
+		rawURL = prompt.String(promptString, defaultValue)
+
+		if rawURL == "" {
+			rawURL = defaultValue
+		}
+
+		_, err := url.ParseRequestURI(rawURL)
+		if err != nil {
+			fmt.Println("please enter a valid url eg https://id.example.com")
+		} else {
+			break
+		}
+	}
+
+	return rawURL
+}
diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go
index 75d619e..c59fbdf 100644
--- a/pkg/cfg/cfg.go
+++ b/pkg/cfg/cfg.go
@@ -1,6 +1,8 @@
 package cfg
 
 import (
+	"net/url"
+
 	"github.com/fatih/structs"
 	"github.com/mitchellh/go-homedir"
 	"github.com/pkg/errors"
@@ -15,7 +17,7 @@ var DefaultConfigPath = "~/.saml2aws"
 
 // IDPAccount saml IDP account
 type IDPAccount struct {
-	Hostname   string `ini:"hostname"`
+	URL        string `ini:"url"`
 	Username   string `ini:"username"`
 	Provider   string `ini:"provider"`
 	MFA        string `ini:"mfa"`
@@ -23,10 +25,15 @@ type IDPAccount struct {
 	Timeout    int    `ini:"timeout"`
 }
 
-// Validate validate the required / exptected fields are set
+// Validate validate the required / expected fields are set
 func (ia *IDPAccount) Validate() error {
-	if ia.Hostname == "" {
-		return errors.New("Hostname empty in idp account")
+	if ia.URL == "" {
+		return errors.New("URL empty in idp account")
+	}
+
+	_, err := url.Parse(ia.URL)
+	if err != nil {
+		return errors.New("URL parse failed")
 	}
 
 	if ia.Provider == "" {
diff --git a/pkg/cfg/example/saml2aws.ini b/pkg/cfg/example/saml2aws.ini
index bc4b211..2a6d6bb 100644
--- a/pkg/cfg/example/saml2aws.ini
+++ b/pkg/cfg/example/saml2aws.ini
@@ -1,11 +1,13 @@
 [wolfeidau]
-hostname = id.wolfe.id.au
 username = mark@wolfe.id.au
 provider = keycloak
 mfa      = totp
 
 [test123]
-hostname = id.whatever.com
-username = abc@whatever.com
-provider = keycloak
-mfa      = sms
\ No newline at end of file
+username    = abc@whatever.com
+provider    = keycloak
+mfa         = sms
+url         = https://id.whatever.com
+skip_verify = false
+timeout     = 0
+
diff --git a/pkg/creds/creds.go b/pkg/creds/creds.go
index 08519c3..3e695bf 100644
--- a/pkg/creds/creds.go
+++ b/pkg/creds/creds.go
@@ -6,13 +6,13 @@ import "errors"
 type LoginDetails struct {
 	Username string
 	Password string
-	Hostname string
+	URL      string
 }
 
 // Validate validate the login details
 func (ld *LoginDetails) Validate() error {
-	if ld.Hostname == "" {
-		return errors.New("Empty hostname")
+	if ld.URL == "" {
+		return errors.New("Empty URL")
 	}
 	if ld.Username == "" {
 		return errors.New("Empty username")
diff --git a/pkg/provider/adfs/adfs.go b/pkg/provider/adfs/adfs.go
index 54429d2..88ad82e 100644
--- a/pkg/provider/adfs/adfs.go
+++ b/pkg/provider/adfs/adfs.go
@@ -48,10 +48,11 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 
 // Authenticate authenticate to ADFS and return the data from the body of the SAML assertion.
 func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
+
 	var authSubmitURL string
 	var samlAssertion string
 
-	adfsURL := fmt.Sprintf("https://%s/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=urn:amazon:webservices", loginDetails.Hostname)
+	adfsURL := fmt.Sprintf("%s/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=urn:amazon:webservices", loginDetails.URL)
 
 	res, err := ac.client.Get(adfsURL)
 	if err != nil {
diff --git a/pkg/provider/adfs2/adfs2.go b/pkg/provider/adfs2/adfs2.go
index 0a1e289..6e3eafc 100644
--- a/pkg/provider/adfs2/adfs2.go
+++ b/pkg/provider/adfs2/adfs2.go
@@ -57,7 +57,7 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		},
 	}
 
-	url := fmt.Sprintf("https://%s/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=urn:amazon:webservices", loginDetails.Hostname)
+	url := fmt.Sprintf("%s/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=urn:amazon:webservices", loginDetails.URL)
 	req, err := http.NewRequest("GET", url, nil)
 	if err != nil {
 		return samlAssertion, err
diff --git a/pkg/provider/jumpcloud/jumpcloud.go b/pkg/provider/jumpcloud/jumpcloud.go
index ea022c2..20cfa39 100644
--- a/pkg/provider/jumpcloud/jumpcloud.go
+++ b/pkg/provider/jumpcloud/jumpcloud.go
@@ -46,7 +46,7 @@ func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 	mfaRequired := false
 
 	authForm := url.Values{}
-	jumpCloudURL := fmt.Sprintf("https://%s", loginDetails.Hostname)
+	jumpCloudURL := loginDetails.URL
 
 	res, err := jc.client.Get(jumpCloudURL)
 	if err != nil {
diff --git a/pkg/provider/keycloak/keycloak.go b/pkg/provider/keycloak/keycloak.go
index 7cca728..4c86652 100644
--- a/pkg/provider/keycloak/keycloak.go
+++ b/pkg/provider/keycloak/keycloak.go
@@ -47,9 +47,7 @@ func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 	var samlAssertion string
 	authForm := url.Values{}
 
-	samlAssertion = ""
-
-	keyCloakURL := fmt.Sprintf("https://%s", loginDetails.Hostname)
+	keyCloakURL := loginDetails.URL
 
 	// fmt.Printf("KeyCloak URL: %s\n\n", keyCloakURL)
 
diff --git a/pkg/provider/okta/okta.go b/pkg/provider/okta/okta.go
index 064cfee..5341030 100644
--- a/pkg/provider/okta/okta.go
+++ b/pkg/provider/okta/okta.go
@@ -77,8 +77,7 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
 	var samlAssertion string
 
-	oktaEntryURL := fmt.Sprintf("https://%s", loginDetails.Hostname)
-	oktaURL, err := url.Parse(oktaEntryURL)
+	oktaURL, err := url.Parse(loginDetails.URL)
 	if err != nil {
 		return samlAssertion, errors.Wrap(err, "error building oktaURL")
 	}
@@ -426,7 +425,7 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 	q := req.URL.Query()
 	q.Add("checkAccountSetupComplete", "true")
 	q.Add("token", oktaSessionToken)
-	q.Add("redirectUrl", oktaEntryURL)
+	q.Add("redirectUrl", loginDetails.URL)
 	req.URL.RawQuery = q.Encode()
 
 	res, err = oc.client.Do(req)
diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go
index 2a8249c..d654144 100644
--- a/pkg/provider/pingfed/pingfed.go
+++ b/pkg/provider/pingfed/pingfed.go
@@ -53,7 +53,7 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 
 	authForm := url.Values{}
 
-	pingFedURL := fmt.Sprintf("https://%s/idp/startSSO.ping?PartnerSpId=urn:amazon:webservices", loginDetails.Hostname)
+	pingFedURL := fmt.Sprintf("%s/idp/startSSO.ping?PartnerSpId=urn:amazon:webservices", loginDetails.URL)
 
 	logger.WithField("url", pingFedURL).Debug("GET")
 
@@ -71,8 +71,6 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		updateLoginFormData(authForm, s, loginDetails)
 	})
 
-	//spew.Dump(authForm)
-
 	doc.Find("form").Each(func(i int, s *goquery.Selection) {
 		action, ok := s.Attr("action")
 		if !ok {
@@ -85,7 +83,7 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		return "", fmt.Errorf("unable to locate IDP authentication form submit URL")
 	}
 
-	ac.authSubmitURL = fmt.Sprintf("https://%s%s", loginDetails.Hostname, ac.authSubmitURL)
+	ac.authSubmitURL = fmt.Sprintf("%s%s", loginDetails.URL, ac.authSubmitURL)
 
 	//log.Printf("id authentication url: %s", authSubmitURL)
 


Test patch:
diff --git a/cmd/saml2aws/commands/login_test.go b/cmd/saml2aws/commands/login_test.go
index 0ab2ea7..4261af9 100644
--- a/cmd/saml2aws/commands/login_test.go
+++ b/cmd/saml2aws/commands/login_test.go
@@ -11,10 +11,10 @@ import (
 
 func TestResolveLoginDetailsWithFlags(t *testing.T) {
 
-	loginFlags := &LoginFlags{Hostname: "id.example.com", Username: "wolfeidau", Password: "testtestlol", SkipPrompt: true}
+	loginFlags := &LoginFlags{URL: "https://id.example.com", Username: "wolfeidau", Password: "testtestlol", SkipPrompt: true}
 
 	idpa := &cfg.IDPAccount{
-		Hostname: "id.example.com",
+		URL:      "https://id.example.com",
 		MFA:      "none",
 		Provider: "Ping",
 		Username: "wolfeidau",
@@ -22,7 +22,7 @@ func TestResolveLoginDetailsWithFlags(t *testing.T) {
 	loginDetails, err := resolveLoginDetails(idpa, loginFlags)
 
 	assert.Empty(t, err)
-	assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", Hostname: "id.example.com"}, loginDetails)
+	assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", URL: "https://id.example.com"}, loginDetails)
 }
 
 func TestResolveRoleSingleEntry(t *testing.T) {
diff --git a/input_test.go b/input_test.go
index 50a0362..b1be18c 100644
--- a/input_test.go
+++ b/input_test.go
@@ -10,7 +10,7 @@ func TestLoginDetails_Validate(t *testing.T) {
 	type fields struct {
 		Username string
 		Password string
-		Hostname string
+		URL      string
 	}
 	tests := []struct {
 		name    string
@@ -19,16 +19,16 @@ func TestLoginDetails_Validate(t *testing.T) {
 	}{
 		// TODO: Add test cases.
 		{name: "hostname missing error", fields: fields{}, wantErr: true},
-		{name: "username missing error", fields: fields{Hostname: "id.example.com"}, wantErr: true},
-		{name: "password missing error", fields: fields{Hostname: "id.example.com", Username: "test"}, wantErr: true},
-		{name: "ok", fields: fields{Hostname: "id.example.com", Username: "test", Password: "test"}, wantErr: false},
+		{name: "username missing error", fields: fields{URL: "id.example.com"}, wantErr: true},
+		{name: "password missing error", fields: fields{URL: "id.example.com", Username: "test"}, wantErr: true},
+		{name: "ok", fields: fields{URL: "id.example.com", Username: "test", Password: "test"}, wantErr: false},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			ld := &creds.LoginDetails{
 				Username: tt.fields.Username,
 				Password: tt.fields.Password,
-				Hostname: tt.fields.Hostname,
+				URL:      tt.fields.URL,
 			}
 			if err := ld.Validate(); (err != nil) != tt.wantErr {
 				t.Errorf("LoginDetails.Validate() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/pkg/cfg/cfg_test.go b/pkg/cfg/cfg_test.go
index d6c0132..24f6b07 100644
--- a/pkg/cfg/cfg_test.go
+++ b/pkg/cfg/cfg_test.go
@@ -26,7 +26,7 @@ func TestNewConfigManagerLoad(t *testing.T) {
 
 	idpAccount, err := cfgm.LoadIDPAccount("test123")
 	require.Nil(t, err)
-	require.Equal(t, &IDPAccount{Hostname: "id.whatever.com", Username: "abc@whatever.com", Provider: "keycloak", MFA: "sms"}, idpAccount)
+	require.Equal(t, &IDPAccount{URL: "https://id.whatever.com", Username: "abc@whatever.com", Provider: "keycloak", MFA: "sms"}, idpAccount)
 
 	idpAccount, err = cfgm.LoadIDPAccount("test1234")
 	require.Nil(t, err)
@@ -40,9 +40,9 @@ func TestNewConfigManagerLoadVerify(t *testing.T) {
 
 	require.NotNil(t, cfgm)
 
-	idpAccount, err := cfgm.LoadIDPAccount("test123")
+	idpAccount, err := cfgm.LoadVerifyIDPAccount("test123")
 	require.Nil(t, err)
-	require.Equal(t, &IDPAccount{Hostname: "id.whatever.com", Username: "abc@whatever.com", Provider: "keycloak", MFA: "sms"}, idpAccount)
+	require.Equal(t, &IDPAccount{URL: "https://id.whatever.com", Username: "abc@whatever.com", Provider: "keycloak", MFA: "sms"}, idpAccount)
 
 	idpAccount, err = cfgm.LoadVerifyIDPAccount("test1234")
 	require.Equal(t, err, ErrIdpAccountNotFound)
@@ -55,12 +55,15 @@ func TestNewConfigManagerSave(t *testing.T) {
 	require.Nil(t, err)
 
 	err = cfgm.SaveIDPAccount("testing2", &IDPAccount{
-		Hostname: "test123",
+		URL:      "https://id.whatever.com",
 		MFA:      "none",
-		Provider: "Ping",
-		Username: "testetst",
+		Provider: "keycloak",
+		Username: "abc@whatever.com",
 	})
 	require.Nil(t, err)
+	idpAccount, err := cfgm.LoadVerifyIDPAccount("testing2")
+	require.Nil(t, err)
+	require.Equal(t, &IDPAccount{URL: "https://id.whatever.com", Username: "abc@whatever.com", Provider: "keycloak", MFA: "none"}, idpAccount)
 
 	os.Remove(throwAwayConfig)
 
diff --git a/pkg/creds/creds_test.go b/pkg/creds/creds_test.go
index 7b053e2..58e089b 100644
--- a/pkg/creds/creds_test.go
+++ b/pkg/creds/creds_test.go
@@ -14,7 +14,7 @@ func TestValidateEmptyLoginDetails(t *testing.T) {
 
 	require.Error(t, err)
 }
-func TestValidateEmptyHostnameLoginDetails(t *testing.T) {
+func TestValidateEmptyURLLoginDetails(t *testing.T) {
 
 	ld := &LoginDetails{Username: "test", Password: "test"}
 
@@ -26,7 +26,7 @@ func TestValidateEmptyHostnameLoginDetails(t *testing.T) {
 
 func TestValidateEmptyUsernameLoginDetails(t *testing.T) {
 
-	ld := &LoginDetails{Hostname: "test", Password: "test"}
+	ld := &LoginDetails{URL: "https://test.com", Password: "test"}
 
 	err := ld.Validate()
 
@@ -35,7 +35,7 @@ func TestValidateEmptyUsernameLoginDetails(t *testing.T) {
 }
 func TestValidateEmptyPasswordLoginDetails(t *testing.T) {
 
-	ld := &LoginDetails{Hostname: "test", Username: "test"}
+	ld := &LoginDetails{URL: "https://test.com", Username: "test"}
 
 	err := ld.Validate()
 
@@ -44,7 +44,7 @@ func TestValidateEmptyPasswordLoginDetails(t *testing.T) {
 
 func TestValidateLoginDetails(t *testing.T) {
 
-	ld := &LoginDetails{Hostname: "test", Username: "test", Password: "test"}
+	ld := &LoginDetails{URL: "https://test.com", Username: "test", Password: "test"}
 
 	err := ld.Validate()