amazon-web-servicesauthenticationauthorizationamazon-cognitolambda-authorizer

Control access for invoking Rest API in API Gateway


I have several API gateway resources which I want to allow other services to invoke them. Say I have these two endpoints:

My clients are some services, they invoke these Rest APIs like the following request: (It is written in Javascript, but they can use any other programming languages, but can't use AWS SDK)

fetch('.../tasks')
.then((tasks) => {
console.log('Tasks:', tasks)
});

I need to check client's permissions as they're calling my API. When a service send a request to /tasks, I should check its permission and see if it doesn't have the required permission, I will return 403 as response.

I want to know what it the best approach to implement it? Should I use AWS Cognito User pool integrated with Identity pool or a Custom authorizer?

If my question is not clear as enough, please comment it, I'll give more information.

I hope someone has related experiences and could help me.


Solution

  • It's late to share what I found, but I'm going to write the approach down for other people who have the same issue with AWS authentication and authorization.

    At first, please pay attention that the following requirement couldn't be implemented via AWS Cognito. In other word, we have to use AWS SDK (as I understood) to gain AWS Credentials.

    My clients are some services, they invoke these Rest APIs like the following request: (It is written in Javascript, but they can use any other programming languages, but can't use AWS SDK)

    Let's take a look at the description of AWS Cognito and its components, based on AWS documentation:

    Amazon Cognito provides authentication, authorization, and user management for your web and mobile apps. Your users can sign in directly with a user name and password, or through a third party such as Facebook, Amazon, Google or Apple.

    The two main components of Amazon Cognito are user pools and identity pools. User pools are user directories that provide sign-up and sign-in options for your app users. Identity pools enable you to grant your users access to other AWS services.

    So just to clarify the explanation above, we could use the user pool component to provide authentication and identity pool to provide access to AWS services and finally authorization.

    What you should bare to mind is authorization is not supported in AWS without using identity pool.

    As there's an informative article regarding how to implement authentication and authorization - which is linked below - I just go through a summary of the related scenario, which I found it useful to know before going through the implementation step.

    http://interworks.com.mk/amazon-cognito-and-api-gateway-aws-iam-authorization/

    enter image description here

    1. Users should authenticate themselves with a user pool. Your app users can sign in either directly through a user pool, or federate through a third-party identity provider (IdP). The sign-in process could occur through either web or API.

    2. After a successful authentication, your web or mobile app will receive user pool tokens for the authenticate user from Amazon Cognito.

    3. You should use those tokens to retrieve AWS credentials that allow your app to access other AWS services.

    4. You could use the received credential to send a request to access to an AWS service which is API Gateway in our example. There are two options for sending a request to AWS API Gateway. The first one is using AWS SDK and the second one is using other libraries like Axios. The point is if you send the request manually (e.g. via axios) you should sign the request before sending it. But if you use AWS SDK, AWS SDK will take care of the signing process by itself and you don't need to do anything.

    There's a sample code which implements this scenario in NodeJs. In this sample we used the Axios library to send the request to API Gateway.

    const AWS = require("aws-sdk");
    const crypto = require("crypto");
    const aws4 = require("aws4");
    const axios = require("axios");
    
    // Client information
    const CLIENT_SECRET = "######";
    const CLIENT_ID = "####";
    const USER_POOL_ID = "####";
    const IDENTITY_POOL_ID = "####";
    const COGNITO_AUTH_PROVIDER =
      "cognito-idp.<REGION>.amazonaws.com/<USER_POOL_ID>";
    
    // AWS config
    AWS.config.region = "XXXX";
    
    // If client_secret is enabled in user pool, client_secret should be converted to a Base64 encoded value called HASH_SECRET
    const createHashSecret = (username) => {
      return crypto
        .createHmac("SHA256", CLIENT_SECRET)
        .update(`${username}${CLIENT_ID}`)
        .digest("base64");
    };
    
    // User information
    const userInfo = {
      username: "###",
      password: "###",
    };
    
    const userPoolData = {
      UserPoolId: USER_POOL_ID,
      ClientId: CLIENT_ID,
    };
    
    /**
     * Add user credential to AWS request sent by HTTP
     * By Signature Version 4
     */
    const generateApiGatewayRequest = (credentials, requestParams) => {
      const { url, hostname, path, method } = requestParams;
      const options = {
        service: "execute-api",
        region: AWS.config.region,
        url,
        hostname,
        path,
        method,
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
      };
    
      // Sign the request by some metadata
      return aws4.sign(options, credentials);
    };
    
    // Create an authentication client to connect to user pool
    const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider(
      userPoolData
    );
    
    // Request to authenticate a user
    cognitoIdentityServiceProvider.initiateAuth({
      AuthFlow: "USER_PASSWORD_AUTH",
      ClientId: CLIENT_ID,
      AuthParameters: {
        USERNAME: userInfo.username,
        PASSWORD: userInfo.password,
        SECRET_HASH: createHashSecret(userInfo.username),
      },
    }, function (err, data) {
      if (err) {
        // Something wrong has happened, like invalid user info, invalid pool and client info, ...
        // The error object contains message, code, time, requestId, statusCode, ...
        console.log(err);
      } else {
        // id token, access token and refresh token have been created.
        console.log(data.AuthenticationResult);
    
        // Request for authorization with Identity pool
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({
          IdentityPoolId: IDENTITY_POOL_ID,
          Logins: {
            [COGNITO_AUTH_PROVIDER]: data.AuthenticationResult.IdToken,
          },
        });
    
        // Check if the request for AWS credential has been done successfully
        AWS.config.credentials.get(function (err) {
          if (err) {
            // There is no received credential
            console.log(err);
          } else {
            // The logged-in user now has a temporary credential to ask for AWS services such as Api Gateway
            // The credential includes accessKeyId, secretAccessKey, sessionToken, etc
            var userCredential = AWS.config.credentials;
    
            // Request protected resources (Api Gateway)
            axios(
              generateApiGatewayRequest(userCredential, {
                url:
                  "https://XXX.execute-api.us-east-1.amazonaws.com/main/tasks",
                hostname: "XXX.execute-api.us-east-1.amazonaws.com",
                path: "/main/tasks",
                method: "GET",
              })
            )
              .then((result) => {
                // Request's been done successfully
                console.log(result);
              })
              .catch((error) => {
                // The error could be either resource errors like Bad Request or Forbidden as the user doesn't have the permission to access the resource
                console.log(error);
              });
          }
        });
      }
    });
    

    For detailed information, please refer to the article, linked above. However, if there's still some ambiguous parts, feel free to comment below.