Repository: localstack/localstack

Issue:
Unable to retrieve credentials with assume_role_with_saml
# Type of request: This is a ...

[X] bug report
[ ] feature request

# Detailed description

When I try to retrieve credentials with `assume_role_with_saml` using boto3 v1.16.26 the following error occured:

```
  File "/Users/benjamin.brabant/Projects/OCTO/DSI/awscli-saml-sso/.venv/lib/python3.8/site-packages/botocore/parsers.py", line 505, in _do_error_parse
    root = self._parse_xml_string_to_dom(xml_contents)
  File "/Users/benjamin.brabant/Projects/OCTO/DSI/awscli-saml-sso/.venv/lib/python3.8/site-packages/botocore/parsers.py", line 454, in _parse_xml_string_to_dom
    raise ResponseParserError(
botocore.parsers.ResponseParserError: Unable to parse response (not well-formed (invalid token): line 1, column 0), invalid XML received. Further retries may succeed:
b'{"status": "running"}'
```

## Expected behavior

I expected to retrieve credentials from API in the following form:
```
{
    "Credentials": {
        "AccessKeyId": "ASIA...",
        "SecretAccessKey": "...",
        "SessionToken": "FQoGZXIvYXdzEBYaDwL8pPz/cNvhUKkibZTashetWcPahlTMbaBUvDwXxjiehDkRQGYYUQrTrMdv7+6SinGiDNBiB7ZKEoyfDja6vhHwnBP2UcY/XozN+MFFPGEMhHcsUqPApwOErN37uHAM5kIOukhGlNmIPvPVWZtDoWryAuygKbqZTWwKecCwtURG2I0KF8MpS+s6SaG6EOUl5OJf/mJJQvH725q2VOWUk7HBezFCIXO+t3L8SzMygdt2FNzwUenhazYvDs2ngSlsbFbAaeeMHikZrWgTs6GkUv1uyAknpTRnInmwBDHb7SZAqpDmc7Q9+b+NXTcO1qzx/eMarHHlFQyeEEI3BEc=",
        "Expiration": "2020-12-06T18:54:38.114Z"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AROA3X42LBCD9KGW7O43L:benjamin.brabant",
        "Arn": "arn:aws:sts::123456789012:assumed-role/Role.Admin/benjamin.brabant"
    },
    "Subject": "AROA3X42LBCD9KGW7O43L:benjamin.brabant",
    "SubjectType": "persistent",
    "Issuer": "http://localhost:3000/",
    "Audience": "https://signin.aws.amazon.com/saml",
    "NameQualifier": "B64EncodedStringOfHashOfIssuerAccountIdAndUserId="
}
```


## Actual behavior

In localstack logs, I have the following information:

```
localstack    | 2020-12-07T09:36:13:INFO:localstack.services.edge: Unable to find forwarding rule for host "localhost:4577", path "/", target header "", auth header "", data "b'Action=AssumeRoleWithSAML&Version=2011-06-15&RoleArn=arn%3Aaws%3Aiam%3A%3A000000000000%3Arole%2FRole'..."
```

I also try to call localstack from awscli with the following command:
```
AWS_ACCESS_KEY_ID='_not_needed_locally_' AWS_SECRET_ACCESS_KEY='_not_needed_locally_' aws --endpoint-url=http://localhost:4577 sts assume-role-with-saml \
    --role-arn arn:aws:iam::000000000000:role/Role.Admin \
    --principal-arn arn:aws:iam::000000000000:saml-provider/SamlExampleProvider \
    --saml-assertion $(cat samlresponse.xml | base64)
```

Which respond only `'AssumeRoleWithSAMLResult'`

# Steps to reproduce

## Command used to start LocalStack

I start localstack through docker-compose:

```
  localstack:
    image: localstack/localstack:0.12.2
    container_name: localstack
    restart: always
    ports:
      - "4577:4566"
      - "4592:4592"
    environment:
      LOCALSTACK_SERVICES: iam, sts, s3
      LOCALSTACK_START_WEB: 0
      LOCALSTACK_DATA_DIR: /tmp/localstack/data
      LOCALSTACK_DOCKER_HOST: unix:///var/run/docker.sock
      LOCALSTACK_S3_BUCKET_NAME: example-bucket
      LOCALSTACK_DEBUG: 1
    volumes:
      - localstack-s3-data:/tmp/localstack
      - ./docker/localstack:/docker-entrypoint-initaws.d
```

and init script in `docker/localstack`:

```
echo "Creating AWS S3 bucket $LOCALSTACK_S3_BUCKET_NAME..."
awslocal s3api create-bucket --bucket $LOCALSTACK_S3_BUCKET_NAME

echo "Creating AWS SAML Provider SamlExampleProvider..."
awslocal iam create-saml-provider --name SamlExampleProvider --saml-metadata-document file:///docker-entrypoint-initaws.d/SAML-Metadata-IDPSSODescriptor.xml

echo "Creating AWS Role.User..."
awslocal iam create-role --role-name Role.User --path / --assume-role-policy-document file:///docker-entrypoint-initaws.d/test-role-trust-relationship-policy.json
awslocal iam put-role-policy --role-name Role.User --policy-name UserPolicy --policy-document file:///docker-entrypoint-initaws.d/test-role-policy.json

echo "Creating AWS Role.Admin..."
awslocal iam create-role --role-name Role.Admin --path / --assume-role-policy-document file:///docker-entrypoint-initaws.d/test-role-trust-relationship-policy.json
awslocal iam put-role-policy --role-name Role.Admin --policy-name AdminPolicy --policy-document file:///docker-entrypoint-initaws.d/test-role-policy.json

echo "... Finished"
```

## Client code (AWS SDK code snippet, or sequence of "awslocal" commands)

```
    client = boto3.client("sts", endpoint_url=endpoint_url)
    sts_response = client.assume_role_with_saml(RoleArn=role_arn, PrincipalArn=principal_arn, SAMLAssertion=assertion)
```


Discussion:

Resolving patch:
diff --git a/localstack/services/edge.py b/localstack/services/edge.py
--- a/localstack/services/edge.py
+++ b/localstack/services/edge.py
@@ -226,6 +226,10 @@ def get_api_from_custom_rules(method, path, data, headers):
 
     if b'Action=AssumeRoleWithWebIdentity' in data_bytes or 'Action=AssumeRoleWithWebIdentity' in path:
         return 'sts', config.PORT_STS
+
+    if b'Action=AssumeRoleWithSAML' in data_bytes or 'Action=AssumeRoleWithSAML' in path:
+        return 'sts', config.PORT_STS
+
     # TODO: move S3 public URLs to a separate port/endpoint, OR check ACLs here first
     stripped = path.strip('/')
     if method in ['GET', 'HEAD'] and '/' in stripped:


Test patch:
diff --git a/tests/integration/test_sts.py b/tests/integration/test_sts.py
--- a/tests/integration/test_sts.py
+++ b/tests/integration/test_sts.py
@@ -1,4 +1,6 @@
 import unittest
+from base64 import b64encode
+
 from localstack.utils.aws import aws_stack
 
 
@@ -22,8 +24,8 @@ def test_assume_role_with_web_identity(self):
         test_role_arn = 'arn:aws:sts::000000000000:role/rd_role'
         test_web_identity_token = 'token'
         response = self.sts_client.assume_role_with_web_identity(RoleArn=test_role_arn,
-                                                                RoleSessionName=test_role_session_name,
-                                                                WebIdentityToken=test_web_identity_token)
+                                                                 RoleSessionName=test_role_session_name,
+                                                                 WebIdentityToken=test_web_identity_token)
 
         self.assertTrue(response['Credentials'])
         self.assertTrue(response['Credentials']['SecretAccessKey'])
@@ -31,6 +33,110 @@ def test_assume_role_with_web_identity(self):
             assume_role_id_parts = response['AssumedRoleUser']['AssumedRoleId'].split(':')
             self.assertEqual(assume_role_id_parts[1], test_role_session_name)
 
+    def test_assume_role_with_saml(self):
+        account_id = '000000000000'
+        role_name = 'test-role'
+        provider_name = 'TestProvFed'
+        fed_name = 'testuser'
+
+        saml_assertion = """
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+    ID="_00000000-0000-0000-0000-000000000000" Version="2.0"
+    IssueInstant="2012-01-01T12:00:00.000Z" Destination="https://signin.aws.amazon.com/saml"
+    Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified">
+  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost/</Issuer>
+  <samlp:Status>
+    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </samlp:Status>
+  <Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion"
+    ID="_00000000-0000-0000-0000-000000000000"
+    IssueInstant="2012-12-01T12:00:00.000Z" Version="2.0">
+    <Issuer>http://localhost:3000/</Issuer>
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+      <ds:SignedInfo>
+        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+        <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+        <ds:Reference URI="#_00000000-0000-0000-0000-000000000000">
+          <ds:Transforms>
+            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+          </ds:Transforms>
+          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+          <ds:DigestValue>
+            NTIyMzk0ZGI4MjI0ZjI5ZGNhYjkyOGQyZGQ1NTZjODViZjk5YTY4ODFjOWRjNjkyYzZmODY2ZDQ4NjlkZjY3YSAgLQo=
+          </ds:DigestValue>
+        </ds:Reference>
+      </ds:SignedInfo>
+      <ds:SignatureValue>
+        NTIyMzk0ZGI4MjI0ZjI5ZGNhYjkyOGQyZGQ1NTZjODViZjk5YTY4ODFjOWRjNjkyYzZmODY2ZDQ4NjlkZjY3YSAgLQo=
+      </ds:SignatureValue>
+      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+        <ds:X509Data>
+          <ds:X509Certificate>
+            NTIyMzk0ZGI4MjI0ZjI5ZGNhYjkyOGQyZGQ1NTZjODViZjk5YTY4ODFjOWRjNjkyYzZmODY2ZDQ4NjlkZjY3YSAgLQo=
+          </ds:X509Certificate>
+        </ds:X509Data>
+      </KeyInfo>
+    </ds:Signature>
+    <Subject>
+      <NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">
+      7ca82df9-1bad-4dd3-9b2b-adb68b554282
+      </NameID>
+      <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <SubjectConfirmationData
+            NotOnOrAfter="2012-01-01T13:00:00.000Z"
+            Recipient="https://signin.aws.amazon.com/saml"/>
+      </SubjectConfirmation>
+    </Subject>
+    <Conditions NotBefore="2012-01-01T12:00:00.000Z" NotOnOrAfter="2012-01-01T13:00:00.000Z">
+      <AudienceRestriction>
+        <Audience>urn:amazon:webservices</Audience>
+      </AudienceRestriction>
+    </Conditions>
+    <AttributeStatement>
+      <Attribute Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName">
+        <AttributeValue>{fed_name}</AttributeValue>
+      </Attribute>
+      <Attribute Name="https://aws.amazon.com/SAML/Attributes/Role">
+        <AttributeValue>
+            arn:aws:iam::{account_id}:saml-provider/{provider_name},arn:aws:iam::{account_id}:role/{role_name}
+        </AttributeValue>
+      </Attribute>
+      <Attribute Name="https://aws.amazon.com/SAML/Attributes/SessionDuration">
+        <AttributeValue>900</AttributeValue>
+      </Attribute>
+    </AttributeStatement>
+    <AuthnStatement AuthnInstant="2012-01-01T12:00:00.000Z" SessionIndex="_00000000-0000-0000-0000-000000000000">
+      <AuthnContext>
+        <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
+      </AuthnContext>
+    </AuthnStatement>
+  </Assertion>
+</samlp:Response>""".format(
+            account_id=account_id,
+            role_name=role_name,
+            provider_name=provider_name,
+            fed_name=fed_name,
+        ).replace(
+            '\n', ''
+        )
+
+        role_arn = 'arn:aws:iam::{account_id}:role/{role_name}' \
+            .format(account_id=account_id, role_name=role_name)
+        principal_arn = 'arn:aws:iam:{account_id}:saml-provider/{provider_name}' \
+            .format(account_id=account_id, provider_name=provider_name)
+        base64_saml_assertion = b64encode(saml_assertion.encode('utf-8')).decode('utf-8')
+        response = self.sts_client.assume_role_with_saml(RoleArn=role_arn,
+                                                         PrincipalArn=principal_arn,
+                                                         SAMLAssertion=base64_saml_assertion)
+
+        self.assertTrue(response['Credentials'])
+        self.assertTrue(response['Credentials']['SecretAccessKey'])
+        if response['AssumedRoleUser']['AssumedRoleId']:
+            assume_role_id_parts = response['AssumedRoleUser']['AssumedRoleId'].split(':')
+            self.assertEqual(assume_role_id_parts[1], fed_name)
+
     def test_get_federation_token(self):
         token_name = 'TestName'
         response = self.sts_client.get_federation_token(Name=token_name)