Repository: Versent/saml2aws (Go)

Issue:
Failed to login with OneLogin in v2.36.2
Hi,

I am using saml2aws with OneLogin.
I upgraded saml2aws from `v2.36.1` to `v2.36.2` and login fails.

In v2.36.2, the following error occurred.


```console
❯ saml2aws --version
2.36.2

❯ saml2aws login --skip-prompt --force -a default --role="arn:aws:iam::xxxxx:role/xxxxx"
Using IdP Account default to access OneLogin https://api.us.onelogin.com/
Authenticating as xxxxx@xxxxx ...
Error authenticating to IdP.: HTTP 401: You are not allowed to access Amazon Web Services (AWS) Multi Account from your current IP address
```
I am using an AWS account with source IP address restrictions on login roles.
I downgraded saml2aws to v2.63.1 and the error no longer occurs.

```console
❯ saml2aws --version
2.36.1
❯ saml2aws login --skip-prompt --force  -a default --role="arn:aws:iam::xxxxx:role/xxxxx"
Using IdP Account default to access OneLogin https://api.us.onelogin.com/
Authenticating as xxxxx@xxxxx...
Waiting for approval, please check your OneLogin Protect app ...
 Approved
Selected role: arn:aws:iam::xxxxx:role/xxxxx
Requesting AWS credentials using SAML assertion.
Logged in as: arn:aws:sts::xxxxx:assumed-role/xxxxx/xxxxx@xxxxx

Your new access key pair has been stored in the AWS configuration.
Note that it will expire at 2022-11-28 21:15:08 +0900 JST
```


The following is my environment.

```console
❯ uname -a
Darwin xxxxxx 21.6.0 Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000 arm64 arm Darwin
```


Discussion:
It may occurs when MFA is required whitelisted IP addresses which are defined in MFA policies.
In this case, we have to add `ip_address` field to `saml_assertion` request parameter (cf. https://developers.onelogin.com/api-docs/2/saml-assertions/generate-saml-assertion , https://github.com/Versent/saml2aws/pull/780 ).

In the saml2aws, below is the relevant code:
https://github.com/Versent/saml2aws/blob/0d135dc19e45548630fb7a9a742d7d479f1c061c/pkg/provider/onelogin/onelogin.go#L108
I'll fix the issue in this pull request: https://github.com/Versent/saml2aws/pull/918 .
@nekottyo Could you check this PR's behavior?



Patch:
diff --git a/README.md b/README.md
index db2717cb6..2a7de25f5 100644
--- a/README.md
+++ b/README.md
@@ -179,6 +179,8 @@ Commands:
         --client-secret=CLIENT-SECRET
                                    OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET)
         --subdomain=SUBDOMAIN      OneLogin subdomain of your company account. (env: ONELOGIN_SUBDOMAIN)
+        --mfa-ip-address=MFA-IP-ADDRESS
+                                   IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS)
     -p, --profile=PROFILE          The AWS profile to save the temporary credentials. (env: SAML2AWS_PROFILE)
         --resource-id=RESOURCE-ID  F5APM SAML resource ID of your company account. (env: SAML2AWS_F5APM_RESOURCE_ID)
         --config=CONFIG            Path/filename of saml2aws config file (env: SAML2AWS_CONFIGFILE)
@@ -196,6 +198,8 @@ Commands:
         --client-id=CLIENT-ID    OneLogin client id, used to generate API access token. (env: ONELOGIN_CLIENT_ID)
         --client-secret=CLIENT-SECRET
                                  OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET)
+        --mfa-ip-address=MFA-IP-ADDRESS
+                                 IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS)
         --force                  Refresh credentials even if not expired.
         --credential-process     Enables AWS Credential Process support by outputting credentials to STDOUT in a JSON message.
         --credentials-file=CREDENTIALS-FILE
diff --git a/cmd/saml2aws/commands/login.go b/cmd/saml2aws/commands/login.go
index 86fe3ffd0..97bbb7bd9 100644
--- a/cmd/saml2aws/commands/login.go
+++ b/cmd/saml2aws/commands/login.go
@@ -217,6 +217,13 @@ func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *flags.LoginExecFla
 		loginDetails.ClientSecret = loginFlags.CommonFlags.ClientSecret
 	}
 
+	// if you supply an mfa_ip_address in a flag or an IDP account it takes precedence
+	if account.MFAIPAddress != "" {
+		loginDetails.MFAIPAddress = account.MFAIPAddress
+	} else if loginFlags.CommonFlags.MFAIPAddress != "" {
+		loginDetails.MFAIPAddress = loginFlags.CommonFlags.MFAIPAddress
+	}
+
 	// log.Printf("loginDetails %+v", loginDetails)
 
 	// if skip prompt was passed just pass back the flag values
diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go
index d6ee181e3..8bb042448 100644
--- a/cmd/saml2aws/main.go
+++ b/cmd/saml2aws/main.go
@@ -90,6 +90,7 @@ func main() {
 	cmdConfigure.Flag("client-id", "OneLogin client id, used to generate API access token. (env: ONELOGIN_CLIENT_ID)").Envar("ONELOGIN_CLIENT_ID").StringVar(&commonFlags.ClientID)
 	cmdConfigure.Flag("client-secret", "OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET)").Envar("ONELOGIN_CLIENT_SECRET").StringVar(&commonFlags.ClientSecret)
 	cmdConfigure.Flag("subdomain", "OneLogin subdomain of your company account. (env: ONELOGIN_SUBDOMAIN)").Envar("ONELOGIN_SUBDOMAIN").StringVar(&commonFlags.Subdomain)
+	cmdConfigure.Flag("mfa-ip-address", "IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS)").Envar("ONELOGIN_MFA_IP_ADDRESS").StringVar(&commonFlags.MFAIPAddress)
 	cmdConfigure.Flag("profile", "The AWS profile to save the temporary credentials. (env: SAML2AWS_PROFILE)").Envar("SAML2AWS_PROFILE").Short('p').StringVar(&commonFlags.Profile)
 	cmdConfigure.Flag("resource-id", "F5APM SAML resource ID of your company account. (env: SAML2AWS_F5APM_RESOURCE_ID)").Envar("SAML2AWS_F5APM_RESOURCE_ID").StringVar(&commonFlags.ResourceID)
 	cmdConfigure.Flag("credentials-file", "The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE)").Envar("SAML2AWS_CREDENTIALS_FILE").StringVar(&commonFlags.CredentialsFile)
@@ -107,6 +108,7 @@ func main() {
 	cmdLogin.Flag("duo-mfa-option", "The MFA option you want to use to authenticate with").Envar("SAML2AWS_DUO_MFA_OPTION").EnumVar(&loginFlags.DuoMFAOption, "Passcode", "Phone Call", "Duo Push")
 	cmdLogin.Flag("client-id", "OneLogin client id, used to generate API access token. (env: ONELOGIN_CLIENT_ID)").Envar("ONELOGIN_CLIENT_ID").StringVar(&commonFlags.ClientID)
 	cmdLogin.Flag("client-secret", "OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET)").Envar("ONELOGIN_CLIENT_SECRET").StringVar(&commonFlags.ClientSecret)
+	cmdLogin.Flag("mfa-ip-address", "IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS)").Envar("ONELOGIN_MFA_IP_ADDRESS").StringVar(&commonFlags.MFAIPAddress)
 	cmdLogin.Flag("force", "Refresh credentials even if not expired.").BoolVar(&loginFlags.Force)
 	cmdLogin.Flag("credential-process", "Enables AWS Credential Process support by outputting credentials to STDOUT in a JSON message.").BoolVar(&loginFlags.CredentialProcess)
 	cmdLogin.Flag("credentials-file", "The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE)").Envar("SAML2AWS_CREDENTIALS_FILE").StringVar(&commonFlags.CredentialsFile)
diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go
index ccfb19cc4..8ab18fd75 100644
--- a/pkg/cfg/cfg.go
+++ b/pkg/cfg/cfg.go
@@ -40,6 +40,7 @@ type IDPAccount struct {
 	Username              string `ini:"username"`
 	Provider              string `ini:"provider"`
 	MFA                   string `ini:"mfa"`
+	MFAIPAddress          string `ini:"mfa_ip_address"` // used by OneLogin
 	SkipVerify            bool   `ini:"skip_verify"`
 	Timeout               int    `ini:"timeout"`
 	AmazonWebservicesURN  string `ini:"aws_urn"`
diff --git a/pkg/creds/creds.go b/pkg/creds/creds.go
index e006216ba..0f846ff02 100644
--- a/pkg/creds/creds.go
+++ b/pkg/creds/creds.go
@@ -4,6 +4,7 @@ package creds
 type LoginDetails struct {
 	ClientID          string // used by OneLogin
 	ClientSecret      string // used by OneLogin
+	MFAIPAddress      string // used by OneLogin
 	Username          string
 	Password          string
 	MFAToken          string
diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go
index 7a3beb6fa..9c741cb94 100644
--- a/pkg/flags/flags.go
+++ b/pkg/flags/flags.go
@@ -13,6 +13,7 @@ type CommonFlags struct {
 	IdpAccount            string
 	IdpProvider           string
 	MFA                   string
+	MFAIPAddress          string
 	MFAToken              string
 	URL                   string
 	Username              string
@@ -75,6 +76,10 @@ func ApplyFlagOverrides(commonFlags *CommonFlags, account *cfg.IDPAccount) {
 		account.MFA = commonFlags.MFA
 	}
 
+	if commonFlags.MFAIPAddress != "" {
+		account.MFAIPAddress = commonFlags.MFAIPAddress
+	}
+
 	if commonFlags.AmazonWebservicesURN != "" {
 		account.AmazonWebservicesURN = commonFlags.AmazonWebservicesURN
 	}
diff --git a/pkg/provider/onelogin/onelogin.go b/pkg/provider/onelogin/onelogin.go
index c4b87b28e..9c37f112e 100644
--- a/pkg/provider/onelogin/onelogin.go
+++ b/pkg/provider/onelogin/onelogin.go
@@ -105,7 +105,7 @@ func (c *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 
 	logger.Debug("Retrieved OneLogin OAuth token:", oauthToken)
 
-	authReq := AuthRequest{Username: loginDetails.Username, Password: loginDetails.Password, AppID: c.AppID, Subdomain: c.Subdomain}
+	authReq := AuthRequest{Username: loginDetails.Username, Password: loginDetails.Password, AppID: c.AppID, Subdomain: c.Subdomain, IPAddress: loginDetails.MFAIPAddress}
 	var authBody bytes.Buffer
 	err = json.NewEncoder(&authBody).Encode(authReq)
 	if err != nil {


Test patch:
diff --git a/cmd/saml2aws/commands/login_test.go b/cmd/saml2aws/commands/login_test.go
index 0e29f3640..bca0442cf 100644
--- a/cmd/saml2aws/commands/login_test.go
+++ b/cmd/saml2aws/commands/login_test.go
@@ -15,7 +15,7 @@ import (
 
 func TestResolveLoginDetailsWithFlags(t *testing.T) {
 
-	commonFlags := &flags.CommonFlags{URL: "https://id.example.com", Username: "wolfeidau", Password: "testtestlol", MFAToken: "123456", SkipPrompt: true}
+	commonFlags := &flags.CommonFlags{URL: "https://id.example.com", Username: "wolfeidau", Password: "testtestlol", MFAIPAddress: "127.0.0.1", MFAToken: "123456", SkipPrompt: true}
 	loginFlags := &flags.LoginExecFlags{CommonFlags: commonFlags}
 
 	idpa := &cfg.IDPAccount{
@@ -27,7 +27,7 @@ func TestResolveLoginDetailsWithFlags(t *testing.T) {
 	loginDetails, err := resolveLoginDetails(idpa, loginFlags)
 
 	assert.Empty(t, err)
-	assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", URL: "https://id.example.com", MFAToken: "123456"}, loginDetails)
+	assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", URL: "https://id.example.com", MFAToken: "123456", MFAIPAddress: "127.0.0.1"}, loginDetails)
 }
 
 func TestOktaResolveLoginDetailsWithFlags(t *testing.T) {