Closed vchepkov closed 10 months ago
Seeing the same here.
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
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.
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"
]
}
}
Is there a workaround until there is a patch?
A dev at our place linked to this as a temporary workaround: https://blog.knoldus.com/how-to-make-assumerolewithsaml-calls-with-aws-cli/amp/
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.
Here's the full payload, if it's helpful:
{
"invalid_accounts": {},
"RelayState": null,
"name": null,
"roles_accounts": {
"accountName (accountId)": [
"arn:aws:iam::NUMBER:role/friendly_name",
]
},
"foreign_accounts": {},
"region": "REGION",
"portal": null,
"SAMLResponse": "<reallyLongString>",
"problems": "{}",
"policy": null
}
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.
If anyone wants to carry forward ^ into a PR and update the testdata, please feel free to do so. I'm about to go on an international flight, so it might take me a bit if I do it :P Consider the code to be MIT-licensed just like the repository is.
Another quick workaround, in case you don't want to mess with the saml2aws code or the assume role API calls directly:
aws configure export-credentials --format env
.It appears the login page update was rolled back on the AWS side.
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.
Great rally though!
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
}
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.
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 🫡
Can we parse the saml assertion to get the roles not depending on the frontend? Something like:
import (
"encoding/xml"
"github.com/crewjam/saml"
)
roles := []string{}
var response saml.Response
err := xml.Unmarshal(samlAssertion, &response)
if err != nil {
return []string{}, errors.Wrap(err, "error unmarshal saml assertion")
}
for _, attributeStatement := range response.Assertion.AttributeStatements {
for _, attribute := range attributeStatement.Attributes {
if attribute.Name == "https://aws.amazon.com/SAML/Attributes/Role" {
for _, attributeValue := range attribute.Values {
roles = append(roles, attributeValue.Value)
}
}
}
}
The AWS account names can still be fetched from frontend. I think for AWS, they also depending on the saml assertion to form the frontend, and directly parse saml assertion can reduce the impact of changes from AWS side. Just a thought.
@vchepkov (and anyone who is interested in this issue), for house-keeping, are you still experiencing this issue?
I am not, but only because AWS rolled back the changes and never re-applied them again
Thanks for the follow up @vchepkov and thanks everyone for this input. Let's hope it was just a mistake that won't regress.
To everyone, I'll close it for now.
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.