saml2aws: saml2aws fails to login to newly revamped login page

Attempting to login to AWS now fail with the following error:

Failed to assume role. Please check whether you are permitted to assume the given role for the AWS service.: No accounts available.

About this issue

  • Original URL
  • State: closed
  • Created 10 months ago
  • Reactions: 77
  • Comments: 21 (2 by maintainers)

Most upvoted comments

It appears the login page update was rolled back on the AWS side.

Here’s a quick patch of something that seems to be working locally:

diff --git a/aws_account.go b/aws_account.go
index ff28c3a..3cca122 100644
--- a/aws_account.go
+++ b/aws_account.go
@@ -2,10 +2,13 @@ package saml2aws
 
 import (
 	"bytes"
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"net/url"
+	"strings"
 
 	"github.com/PuerkitoBio/goquery"
 	"github.com/pkg/errors"
@@ -41,17 +44,53 @@ func ExtractAWSAccounts(data []byte) ([]*AWSAccount, error) {
 		return nil, errors.Wrap(err, "failed to build document from response")
 	}
 
-	doc.Find("fieldset > div.saml-account").Each(func(i int, s *goquery.Selection) {
-		account := new(AWSAccount)
-		account.Name = s.Find("div.saml-account-name").Text()
-		s.Find("label").Each(func(i int, s *goquery.Selection) {
-			role := new(AWSRole)
-			role.Name = s.Text()
-			role.RoleARN, _ = s.Attr("for")
-			account.Roles = append(account.Roles, role)
-		})
-		accounts = append(accounts, account)
-	})
+	b64data, ok := doc.Find("meta[name=data]").Attr("content")
+	if !ok {
+		return nil, errors.New("failed to find meta[name=data] in AWS response")
+	}
+
+	// decode the base64 encoded data
+	data, err = base64.StdEncoding.DecodeString(b64data)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode base64 data")
+	}
+
+	type dataResponse struct {
+		InvalidAccounts struct{}            `json:"invalid_accounts"`
+		RelayState      any                 `json:"RelayState"`
+		Name            any                 `json:"name"`
+		RolesAccounts   map[string][]string `json:"roles_accounts"`
+		ForeignAccounts struct{}            `json:"foreign_accounts"`
+		Region          string              `json:"region"`
+		Portal          any                 `json:"portal"`
+		Problems        string              `json:"problems"`
+		Policy          any                 `json:"policy"`
+	}
+
+	dr := &dataResponse{}
+	if err := json.Unmarshal(data, dr); err != nil {
+		return nil, errors.Wrap(err, "failed to unmarshal data")
+	}
+
+	// for each account map to our structure
+	for account, roles := range dr.RolesAccounts {
+		name := strings.TrimSpace(strings.Split(account, "(")[0])
+
+		awsAccount := &AWSAccount{
+			Name: name,
+		}
+
+		for _, role := range roles {
+			awsRole := &AWSRole{
+				Name:    role,
+				RoleARN: role,
+			}
+
+			awsAccount.Roles = append(awsAccount.Roles, awsRole)
+		}
+
+		accounts = append(accounts, awsAccount)
+	}
 
 	return accounts, nil
 }

I make no guarantees for how well it works for all edgecases 😃

Huge thanks to @MichaelPalmer1 for discovering the meta tag which made this a lot easier.

Seeing the same here.

The role data can be pulled from the <meta name="data" content="[base64 encoded json object containing role information]">.

This makes it a lot cleaner actually.

When decoded, this is the essential piece:

{
   "roles_accounts": {
      "account-name (111111111111)": [
        "arn:aws:iam::111111111111:role/role-123",
        "arn:aws:iam::111111111111:role/role-456"
      ]
   }
}

The https://signin.aws.amazon.com/saml UI has changed and now there are dashes on the account numbers and there weren’t before and the page is drastically different. Looks like saml2aws scrapes this screen from code above found by @ReagentX . And there’s no fieldset on the page any longer.

I’m the author of the PR to implement a fix for gimme-aws-creds and I wanted to drop a note here that if it helps folks working to patch saml2aws, I did dump the new NextJS sign-in page before AWS rolled back earlier today. I have that saved as a sanitized mock file for use as a fixture for the tests for gimme-aws-creds. You can review that file at https://github.com/Nike-Inc/gimme-aws-creds/blob/master/tests/fixtures/aws_nextjs.html

Hope that helps author a patch for saml2aws 🫡

It appears the login page update was rolled back on the AWS side.

I was a little worried about that. It makes supporting it a little more painful since we won’t be able to get a good test without the live page… 😢 (jokes on me for not saving it to disk to formulate a testdata entry)

We probably want to support both methods in a PR in case it switches back and forth again, too.

Here is a sample of the HTML for the role table:

<ul class="saml-form_custom_list__LuP_V">
    <li class="saml-form_custom_list_item__H1wPE">
    <div class="flex flex-row">
        <div class="flex-grow">
        <div class="saml-form_account_name_div__dDGkX">aws-account-1 </div>
        <div class="saml-form_account_id_div__Q1PBA">1234-5678-9012</div>
        <div class="saml-form_account_role_div__FhQmy">
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            <p class="saml-form_space__uJWq3"></p>
            </div>
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            <p class="saml-form_space__uJWq3"></p>
            </div>
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            <p class="saml-form_space__uJWq3"></p>
            </div>
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            </div>
        </div>
        </div>
    </div>
    </li>
    <li class="saml-form_custom_list_item__H1wPE">
    <div class="flex flex-row">
        <div class="flex-grow">
        <div class="saml-form_account_name_div__dDGkX">aws-account-2 </div>
        <div class="saml-form_account_id_div__Q1PBA">1234-5678-9012</div>
        <div class="saml-form_account_role_div__FhQmy">
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            <p class="saml-form_space__uJWq3"></p>
            </div>
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            <p class="saml-form_space__uJWq3"></p>
            </div>
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            <p class="saml-form_space__uJWq3"></p>
            </div>
            <div class="saml-form_role_div__BeMu2">
            <a class="saml-form_role_text__Cfxwn" id="arn:aws:iam::123456789012:role/AWS-IAM-Role-Name" data-testid="roleLink-arn:aws:iam::123456789012:role" role="button" aria-label="Account ID 1234-5678-9012 Role Name AWS-IAM-Role-Name" tabindex="0">AWS-IAM-Role-Name</a>
            </div>
        </div>
        </div>
    </div>
    </li>
</ul>

And here is a sample of the JSON blob in <meta name="data" content="[base64 blob]"> after base64 decoding:

{
  "invalid_accounts": {},
  "RelayState": "",
  "name": null,
  "roles_accounts": {
    "aws-account-1 (123456789012)": [
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name",
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name",
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name",
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name"
    ],
    "aws-account-2 (123456789012)": [
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name",
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name",
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name",
      "arn:aws:iam::123456789012:role/AWS-IAM-Role-Name"
    ]
  },
  "foreign_accounts": {},
  "region": "IAD",
  "portal": null,
  "SAMLResponse": "SGVsbG8sIFdvcmxkIQ==",
  "problems": "{}",
  "policy": null
}

Great rally though!

Here’s a quick patch of something that seems to be working locally:

diff --git a/aws_account.go b/aws_account.go
index ff28c3a..3cca122 100644
--- a/aws_account.go
+++ b/aws_account.go
@@ -2,10 +2,13 @@ package saml2aws
 
 import (
 	"bytes"
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"net/url"
+	"strings"
 
 	"github.com/PuerkitoBio/goquery"
 	"github.com/pkg/errors"
@@ -41,17 +44,53 @@ func ExtractAWSAccounts(data []byte) ([]*AWSAccount, error) {
 		return nil, errors.Wrap(err, "failed to build document from response")
 	}
 
-	doc.Find("fieldset > div.saml-account").Each(func(i int, s *goquery.Selection) {
-		account := new(AWSAccount)
-		account.Name = s.Find("div.saml-account-name").Text()
-		s.Find("label").Each(func(i int, s *goquery.Selection) {
-			role := new(AWSRole)
-			role.Name = s.Text()
-			role.RoleARN, _ = s.Attr("for")
-			account.Roles = append(account.Roles, role)
-		})
-		accounts = append(accounts, account)
-	})
+	b64data, ok := doc.Find("meta[name=data]").Attr("content")
+	if !ok {
+		return nil, errors.New("failed to find meta[name=data] in AWS response")
+	}
+
+	// decode the base64 encoded data
+	data, err = base64.StdEncoding.DecodeString(b64data)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode base64 data")
+	}
+
+	type dataResponse struct {
+		InvalidAccounts struct{}            `json:"invalid_accounts"`
+		RelayState      any                 `json:"RelayState"`
+		Name            any                 `json:"name"`
+		RolesAccounts   map[string][]string `json:"roles_accounts"`
+		ForeignAccounts struct{}            `json:"foreign_accounts"`
+		Region          string              `json:"region"`
+		Portal          any                 `json:"portal"`
+		Problems        string              `json:"problems"`
+		Policy          any                 `json:"policy"`
+	}
+
+	dr := &dataResponse{}
+	if err := json.Unmarshal(data, dr); err != nil {
+		return nil, errors.Wrap(err, "failed to unmarshal data")
+	}
+
+	// for each account map to our structure
+	for account, roles := range dr.RolesAccounts {
+		name := strings.TrimSpace(strings.Split(account, "(")[0])
+
+		awsAccount := &AWSAccount{
+			Name: name,
+		}
+
+		for _, role := range roles {
+			awsRole := &AWSRole{
+				Name:    role,
+				RoleARN: role,
+			}
+
+			awsAccount.Roles = append(awsAccount.Roles, awsRole)
+		}
+
+		accounts = append(accounts, awsAccount)
+	}
 
 	return accounts, nil
 }

I make no guarantees for how well it works for all edgecases 😃

Huge thanks to @MichaelPalmer1 for discovering the meta tag which made this a lot easier.

Based on the RCA from AWS, they mentioned Logins to other Regions were not affected and existing authentication sessions were not impacted., so the change only impacted US-EAST-1. I would suggest to keep the old saml-account div tag parser, and add the new logic to better support all regions.

Another quick workaround, in case you don’t want to mess with the saml2aws code or the assume role API calls directly:

  1. Open the AWS console in your browser via whatever login method you use for the account you want to access.
  2. Open CloudShell. Screen Shot 2023-08-21 at 19 47 01
  3. Type aws configure export-credentials --format env.
  4. Copy the resulting output into your shell.

Starting today it appears that the azure login fails to return roles. This was working a few days ago with 0 changes to the saml2aws version, IAM roles, or Azure AD. Not sure if others are experiencing this caused by some change from AWS or Azure?

DEBU[0014] processing SAMLRequest                        provider=AzureAD
DEBU[0014] processing a SAMLResponse                     provider=AzureAD
No accounts available.
github.com/versent/saml2aws/v2/cmd/saml2aws/commands.resolveRole
        github.com/versent/saml2aws/v2/cmd/saml2aws/commands/login.go:302
github.com/versent/saml2aws/v2/cmd/saml2aws/commands.selectAwsRole
        github.com/versent/saml2aws/v2/cmd/saml2aws/commands/login.go:272
github.com/versent/saml2aws/v2/cmd/saml2aws/commands.Login
        github.com/versent/saml2aws/v2/cmd/saml2aws/commands/login.go:131
main.main
        ./main.go:191
runtime.main
        runtime/proc.go:250
runtime.goexit
        runtime/asm_amd64.s:1598
Failed to assume role. Please check whether you are permitted to assume the given role for the AWS service.
github.com/versent/saml2aws/v2/cmd/saml2aws/commands.Login
        github.com/versent/saml2aws/v2/cmd/saml2aws/commands/login.go:133
main.main
        ./main.go:191
runtime.main
        runtime/proc.go:250
runtime.goexit
        runtime/asm_amd64.s:1598