amazon-archives / amazon-quicksight-embedding-sample

A QuickSight dashboard embedding sample for web apps.
Apache License 2.0
61 stars 40 forks source link

Get Dashboard URL Using Enhanced Authentication Flow and Token Role Mapping #12

Open jra85 opened 4 years ago

jra85 commented 4 years ago

Hi, I'm working to embed a dashboard in an application that utilizes a Cognito User Pool as its federated identity provider, and to further control access to AWS resources, those users are assigned to Cognito groups that are associated with a Role. This setup requires that the identity pool's Authenticated Role Selection is set to "Choose Role From Token". However, when I attempt to get the Open ID token in my web application using the CognitoIdentity.getOpenIdToken API from the AWS SDK, the following error occurs: "Basic (classic) flow is not supported with RoleMappings, please use enhanced flow."

I did attempt modifying the embedding sample's OpenID Lambda script to leverage the ID token that's provided by Cognito (rather than the Open ID token) and calling CognitoIdentity.getCredentialsForIdentity rather than STS.assumeRoleWithWebIdentity to obtain the access keys to use with QuickSight, but upon executing the Lambda I get the error "QuickSightUserNotFoundException: Could not find user information in QuickSight". I don't have insight into what username it's actually looking for though to know how to correct my approach. Perhaps since the script is no longer using assumeRoleWithWebIdentity the role name is not prefixed to the username?

The "Allow Basic (Classic) Flow" option is enabled for the identity pool, but that does not resolve it. It does seem that this option needs to be enabled for the Lambda example to work as it is, but I use the enhanced flow everywhere else in my application, so being able to embed the dashboard without requiring the basic flow to be enabled would be ideal.

Any insight is appreciated as I'd really prefer to continue using the enhanced flow and token-based role mapping with this application.

Thanks!

peejayess commented 4 years ago

Hi,

Have you quicksight.registerUser prior to getting the embedded URL?

You still need to register the user in Quicksight so you can then go into the Quicksight console and share the dashboard with them. I think you may also need to use STS.assumeRoleWithWebIdentity because it takes a RoleSessionName which will be the same as the SessionName taken by quicksight.registerUser which will ensure the quickSight.getDashboardEmbedUrl call matches the same user. CognitoIdentity.getCredentialsForIdentity doesn't take a session name so not sure how the credentials from that would find the correct user in Quicksight.

I'm no expert and have just spent a week trying, and finally succeeding, to get Quicksight dashboard embedded in an Amplify React app. I used the default auth_role setup by Amplify Auth to "quicksight:GetDashboardEmbedUrl" so not as complicated was what you're doing. If you're doing anything similar then let me know because I implemented differently to the example because of working in Amplify world.

Regards, Peter

jra85 commented 4 years ago

Thanks for your reply! I'm using the same approach that the OpenID Lambda sample uses for registering the user, which is that the Lambda function's execution role (named "LambdaExecutionRole" in the sample) is responsible for registering the user without explicitly assuming any other role, and LambdaExecutionRole has IAM permissions for the quicksight:RegisterUser action. This appears to work fine.

From there, the sample code then uses STS.assumeRoleWithWebIdentity to assume the Cognito federated identity's default authenticated role ("CognitoAuthorizedRole" in the sample) using a token to get Credentials to obtain the QuickSight embed URL. The use of STS.assumeRoleWithWebIdentity to assume the role and get the Credentials object for a token is the "basic" auth flow, whereas AWS also offers (and promotes the use of) the "enhanced" auth flow, which uses CognitoIdentity.getCredentialsForIdentity to obtain the Credentials object for a token: https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html

There are benefits to using the enhanced auth flow that I'd like to leverage in my application, namely the ability for the Cognito Federated Identity to automatically choose the user's role based on their token rather than explicitly having to assume roles using STS.assumeRoleWithWebIdentity when needed. For example, if a user is assigned to a Cognito user group and that group has a role associated with it, the user would automatically assume that role when using the enhanced auth flow. However, I cannot configure my Federated Identity to choose the role based on the token if I need to support the basic auth flow and the use of STS.assumeRoleWithWebIdentity, so that's why I'm trying to get the sample to work using the enhanced auth flow instead.

You may be correct that since getCredentialsForIdentity does not appear to have the concept of a Session Name (which I'm using the user's e-mail address as the value for), this approach might not be possible. Perhaps the QuickSight.getDashboardEmbedUrlAPI needs to be enhanced to support using the enhanced auth flow and specifying the session name/username as a parameter when using IAM authentication?

That said, I have embedding working using the basic auth flow, but for my purposes supporting the basic auth flow is not ideal and I was hoping there would be an alternative solution.

peejayess commented 4 years ago

I think you are right that the API needs updating to support enhanced auth flow. I think a proper example when using Amplify Auth would be good too. I tried using the tokens retrieved from cognitoIdentity.getCredentialsForIdentity (i.e. because I already had them in Amplify Auth world so it would save me a call) to call QuickSight.getDashboardEmbedUrl but it wouldn't accept them. Unfortunately, I can't rember the exact reasons why.

We can only hope!

jra85 commented 4 years ago

Out of curiosity, when you say you're using Amplify to get the tokens to get the QuickSight embed URL, are you using the Auth.currentCredentials() API to get them? And are you making that call from within the Lambda function that calls QuickSight.getDashboardEmbedUrl, or from your React app? I wonder if I could potentially use that approach instead to avoid having to use STS.assumeRoleWithWebIdentity.

peejayess commented 4 years ago

Using Auth.*** won't work out of the box in the lamda because its missing all the config in aws-exports.js in the web app src. I think a lot of that comfig probably is available in the Request attributes.

I tired what you are suggesting but calling Auth.currentCredentials() in the web app and passing the results to the lambda but that didn't work. What I'm currently doing is passing Auth.currentAuthenticatedUser()).getSignInUserSession().getIdToken() from web app to Lambda. Then in the lambda:

casendler commented 4 years ago

Hi Peter - do you have an example of how you are using cognitoIdentity.getOpenIdToken in your lambda to get the OpenID token back? We have been stuck at this point for ~week now with our QS embedded dashboard enablement.

peejayess commented 4 years ago

Hi Casendler,

Sorry, its been a while and I've moved onto another project so I can't remember exactly but here is the code I used. It looks like I was retrieving th emailAddress as well so you can ignore that bit if you don't need it. Let me know if you need any more help.

const getUserInfo = async function(
  cognitoUserPoolIdJwtToken,
  cognitoUserPoolAccessJwtToken,
  cognitoUserPoolId,
  cognitoIdentityId
) {
  const cognitoISP = new AWS.CognitoIdentityServiceProvider({
    apiVersion: "2016-04-18",
    region: process.env.AWS_REGION
  });
  console.log("Getting User.  Access token: ", cognitoUserPoolAccessJwtToken);
  const user = await cognitoISP
    .getUser({
      AccessToken: cognitoUserPoolAccessJwtToken
    })
    .promise();
  let emailAddress = user.UserAttributes.find(({ Name }) => Name === "email")
    .Value;

  const cognitoIdentity = new AWS.CognitoIdentity({
    apiVersion: "2014-06-30",
    region: process.env.AWS_REGION
  });
  const loginIdpId = `cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${cognitoUserPoolId}`;

  var idTokenParams = {
    IdentityId: cognitoIdentityId,
    Logins: {
      [loginIdpId]: cognitoUserPoolIdJwtToken
    }
  };

  console.log("Getting Open Id Token.  Params: ", idTokenParams);
  const openIdToken = await cognitoIdentity
    .getOpenIdToken(idTokenParams)
    .promise();

  return {
    emailAddress: emailAddress,
    openIdToken: openIdToken.Token
  };
};

Here are the function parameter details:

cognitoUserPoolIdJwtToken and cognitoUserPoolAccessJwtToken I passed these in on the query string and used the following to get them:

decodeURIComponent(event.cognitoUserPoolIdJwtToken || event.queryStringParameters.cognitoUserPoolIdJwtToken)

Here is the Front end (React) code to send them on the querystring:

 const userSession = (await Auth.currentAuthenticatedUser()).getSignInUserSession();

 const url = await API.get("QuicksightCORSWorkaround", "/embeddedurls", {
          headers: { "Content-Type": "application/json" },
          queryStringParameters: {
            cognitoUserPoolIdJwtToken: userSession.getIdToken().getJwtToken(),
            cognitoUserPoolAccessJwtToken: userSession.getAccessToken().getJwtToken()
          }
        });

cognitoUserPoolId To get the UserPoolId you pass in event.requestContext.identity.cognitoAuthenticationProvider to the following function:

function extractUserPoolDetails(cognitoAuthenticationProvider) {
  // Cognito authentication provider looks like:
  // cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxxxxxx,cognito-idp.us-east-1.amazonaws.com/us-east-1_aaaaaaaaa:CognitoSignIn:qqqqqqqq-1111-2222-3333-rrrrrrrrrrrr
  // Where us-east-1_aaaaaaaaa is the User Pool id
  // And qqqqqqqq-1111-2222-3333-rrrrrrrrrrrr is the User Pool User Id
  const parts = cognitoAuthenticationProvider.split(":");
  const userPoolIdParts = parts[parts.length - 3].split("/");

  return {
    userPoolId: userPoolIdParts[userPoolIdParts.length - 1],
    userPoolUserId: parts[parts.length - 1]
  };
}

cognitoIdentityId

User event.requestContext.identity.cognitoIdentityId

deanthemachine2 commented 4 years ago

I was stuck on this all morning - so I wanted to post the solution I came to with the help of the fine folks at stack overflow (https://stackoverflow.com/questions/59618011/rolesessionname-when-fetching-cognitoidentitycredentials).

Long story short - when you use the 'enhanced' authentication flow, the credentials that get returned do NOT include your session (user name), they include the generic term "CognitoIdentityCredentials" instead. This is why its possible to register the user and then 1 second later the getDashboardEmbedUrl function says the user doesn't exist.

This means that while you can REGISTER a user, you can't use the getDashboardEmbedUrl function as-is.

The workaround is to change the parameters you send to the getDashboardEmbedUrl function.

  1. Change the IdentityType to 'QUICKSIGHT' from 'IAM'
  2. Specify the UserArn - however in this case it's not the ARN of the role, you need to figure out the actual quicksight user ARN (better explanation in the link to stack overflow but here's an example: arn:aws:quicksight:[region]:[account ID]:user/[namespace]/[assumed-role-name]/[session-name])
  3. Make sure the role that you're using has the following permissions, the "quicksight:GetAuthCode" needs to be added beyond the other two that all the other quicksight examples have already.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "quicksight:RegisterUser",
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": "quicksight:GetAuthCode",
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": "quicksight:GetDashboardEmbedUrl",
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

Hope this helps someone - I think it's pretty narrow but pretty important to still be able to use enhanced flow with cognito groups to determine the roles. We are using this for a multi-tenant app where each group has client-specific permissions for other AWS services so getting this working was crucial to even be able to use quicksight.

jra85 commented 4 years ago

Nice, thanks for following up with a solution! I haven't worked on my project in a few months but will eventually get back to it. Like your project, for mine each Cognito group will have permissions for specific services and we'll also use their group to determine how to display certain page functionality, so getting the enhanced authentication flow to work was ideal. I ended up using the basic authentication flow for everything just to support Quicksight embedding, but now I'll consider switching over to use the enhanced flow with this info.

I did experiment with the approach of getting it to work using the QUICKSIGHT IdentityType rather than IAM, but I probably had the ARN format wrong or didn't consider the GetAuthCode permission.

byronsorrells commented 4 years ago

Confirming the approach that @deanthemachine2 took works for me as well. I was struggling mightily with this. Had been going back and forth with AWS support for a few days. @deanthemachine2, thanks for taking time to share your solution.