AWS Cognito error message "Invalid base64 SAMLResponse"

While debugging why AWS Cognito was giving the error "Invalid base64 SAMLResponse", I didn't see many good answers on the internet.

TL;DR - double check your SAML attributes

Configuration

  • My SAML Identity Provider (IdP) is AWS SSO, and I'm using IdP initiated SAML from the AWS SSO application page
  • AWS Cognito is the SAML Service Provider (SP)
  • I'm using API Gateway to accept the HTTP POST, which forwards the request to AWS Lambda
  • I'm using AWS Lambda to use Cognito to turn the SAMLResponse into AWS IAM API credentials

My test AWS Lambda code that accepts the SAML Response and asks Cognito for API credentials:

const qs = require("querystring");
const AWS = require("aws-sdk");

AWS.config.update({
    httpOptions: {
        connectTimeout: 3000,
        timeout: 5000,
    },
    region: "us-east-1",
});

exports.handler = async (event, context) => {
    const headers = {
        'Content-Type': 'application/json',
    };
    const body = qs.parse(event.body);

    if(body.SAMLResponse === undefined || body.SAMLResponse === "") {
        return {
            statusCode: 500,
            body: JSON.stringify({error: "SAMLResponse not sent"}),
            headers,
        };
    }

    try {
        const cognito = new AWS.CognitoIdentity();
        const Logins = {
            'arn:aws:iam::12345:saml-provider/TestIDP': body.SAMLResponse,
        };
        const id = await cognito.getId({
            IdentityPoolId: "us-east-1:123456-7890-1234-5678-123456789",
            Logins,
        }).promise();
        if (id.IdentityId === undefined) {
            return {
                statusCode: 500,
                body: JSON.stringify({error: "IdentityId is undefined"}),
                headers,
            };
        }
        const creds = await cognito.getCredentialsForIdentity({
            IdentityId: id.IdentityId,
            Logins,
        }).promise();

        return {
            statusCode: 200,
            body: JSON.stringify(creds),
            headers,
        };
    } catch(e) {
        return {
            statusCode: 500,
            body: JSON.stringify(e),
            headers,
        };
    }
};

Debugging

The error I got when I originally set this up was a very misleading error message:

{
  "message": "Invalid login token. Invalid base64 SAMLResponse",
  "code": "NotAuthorizedException",
  "time": "2021-11-15T10:00:00.000Z",
  "requestId": "12345-1234-1234-1234-1234567",
  "statusCode": 400,
  "retryable": false,
  "retryDelay": 90
}

I used the Network tab of the web developer tools to capture the SAMLResponse and verified that it was encoded properly in base64.  I also verified the XML was correct and not missing any tags.

I tried changing the SAML Audience, but that element turns out to be completely ignored by Cognito.

Solution

After many red herrings and web searches, I found Cognito requires the IAM Role to be sent as a SAML Attribute.  With AWS SSO, this is under the Application's "Attribute Mappings" tab.

Attribute Name https://aws.amazon.com/SAML/Attributes/Role
Attribute Value ${iam-idp-value},${iam-role}
Note: the attribute value can be in either role arn first or idp arn first.  
Just sending the role arn without the idp arn does not work.

For example:
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/Role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">arn:aws:iam::12345:saml-provider/TestIDP,arn:aws:iam::12345:role/Cognito_TestIDPAuth_Role</saml2:AttributeValue>
</saml2:Attribute>

https://aws.amazon.com/SAML/Attributes/RoleSessionName is also accepted as an optional attribute and is used as the UserId if given.  

Testing

Putting this all together, I can get API keys from my service:

{
  "IdentityId": "us-east-1:123456-7890-1234-5678-123456789",
  "Credentials": {
    "AccessKeyId": "ASIAAAAAAAAAAA",
    "SecretKey": "VERYSECRETANDSECURE",
    "SessionToken": "IQ1000SESSIONTOKEN==",
    "Expiration": "2021-11-15T10:00:00.000Z"
  }
}

Then putting those API keys in $AWS_ACCESS_KEY_ID, $AWS_SECRET_ACCESS_KEY, $AWS_SESSION_TOKEN, I can use the AWS command line client with the credentials:

# without RoleSessionName
$ aws sts get-caller-identity
{
    "UserId": "AROAAAAAAAAAAAA:CognitoIdentityCredentials",
    "Account": "12345",
    "Arn": "arn:aws:sts::12345:assumed-role/Cognito_TestIDPAuth_Role/CognitoIdentityCredentials"
}

# with RoleSessionName
$ aws sts get-caller-identity
{
    "UserId": "AROAAAAAAAAAAAA:ddrown@example.com",
    "Account": "12345",
    "Arn": "arn:aws:sts::12345:assumed-role/Cognito_TestIDPAuth_Role/ddrown@example.com"
}