Search code examples
amazon-web-servicesaws-lambdaaws-api-gatewayamazon-cognitoaws-http-api

Best way to authorize a single HTTP API request in API Gateway in AWS?


For a reason I won't mention here, I need to give a random person with no account a secret link to my HTTP API method which calls a lambda function in AWS. The API method needs to be under authorization, but I am unsure on what is the best approach. Ideally, the link should only be allowed to be used one time. If not possible, perhaps some TTL could suffice.

My backend needs to generate the link using some AWS .NET SDK.

I am relatively new to AWS, and there are just so many options. What should I do? Should I sign the link somehow, should I use AWS Cognito somehow? Should I create a custom lambda authorizer which accesses DynamoDB for some token?

What is the simplest and cheapest way?


Solution

  • You can achieve this using IAM role with only permissions to invoke API, and AWS temporary credentials generated by AWS STS. It is similiar to the process described in this doc. The temporary token generated by AWS STS have a minimum of 15 min TTL and maximum of 12 hours TTL.

    First, create API gateway and its method that uses IAM authorizer. Then you can create an IAM role that contains only the permission to invoke the API endpoint and its method.

    the following is example CDK code to create the aws resources

        const restApi = new apigateway.RestApi(this, 'restApi');
        const resource = restApi.root.addResource('test')
        const myApiMethod = resource.addMethod('GET', testLambdaIntegration, {
          authorizationType: apigateway.AuthorizationType.IAM,
        });
    
        const role = new iam.Role(this, 'role', {
          roleName: 'guestLambdaRole',
          assumedBy: new iam.AccountPrincipal(cdk.Stack.of(this).account),
          inlinePolicies: {
            guestLambdaPolicy: new iam.PolicyDocument({
              statements: [
                new iam.PolicyStatement({
                  actions: ['execute-api:Invoke'],
                  effect: iam.Effect.ALLOW,
                  resources: [myApiMethod.methodArn]
                })
              ]
            }),
          }
        });
    

    Second, you can generate temporary credentials to assume the lambdaGuestRole. Assuming your backend is a lambda on the same aws account, it will be able to assume the lambdaGuestRole, and get the temporary credentials.

    the following is example python code to fetch temporary credentials for the assumed role. It uses the default TTL duration of 1 hour. See AssumeRole doc to customize TTL duration. This token can be provided to your users to invoke the API. The AWS .NET SDK have similar logic to fetch the credentials.

    def get_temp_session_data(assume_role_arn, sts_client):
        response = sts_client.assume_role(
            RoleArn=assume_role_arn, RoleSessionName='lambdaGuestSession')
        temp_credentials = response['Credentials']
        print(f"Assumed role {assume_role_arn} and got temporary credentials.")
    
        session_data = {
            'sessionId': temp_credentials['AccessKeyId'],
            'sessionKey': temp_credentials['SecretAccessKey'],
            'sessionToken': temp_credentials['SessionToken']
        }
        return session_data
    

    Third, your users can use the temporary credentials to invoke your API as needed.

    the following is example python code to invoke API using temporary credentials. Note that the credentials are part of the header, which can not be included in a link.

    def get_from_api(session_data, api_base_url, api_path):
        # Sign the request
        auth = AWSRequestsAuth(
            aws_access_key=session_data['sessionId'],
            aws_secret_access_key=session_data['sessionKey'],
            aws_token=session_data['sessionToken'],
            aws_host=api_base_url,
            aws_region='us-west-2',
            aws_service='execute-api')
        response = requests.get('https://' + api_base_url + api_path, auth=auth, data={})
        return response
    

    You can also generate an URL for AWS Management Console access, and your users can invoke the API from AWS console.

    def construct_federated_url(session_data):
        aws_federated_signin_endpoint = 'https://signin.aws.amazon.com/federation'
    
        # Make a request to the AWS federation endpoint to get a sign-in token.
        # The requests.get function URL-encodes the parameters and builds the query string
        # before making the request.
        response = requests.get(
            aws_federated_signin_endpoint,
            params={
                'Action': 'getSigninToken',
                'SessionDuration': str(datetime.timedelta(hours=12).seconds),
                'Session': json.dumps(session_data)
            })
        signin_token = json.loads(response.text)
        print(f"Got a sign-in token from the AWS sign-in federation endpoint.")
    
        # Make a federated URL that can be used to sign into the AWS Management Console.
        query_string = urllib.parse.urlencode({
            'Action': 'login',
            'Issuer': issuer,
            'Destination': 'https://console.aws.amazon.com/',
            'SigninToken': signin_token['SigninToken']
        })
        federated_url = f'{aws_federated_signin_endpoint}?{query_string}'
        return federated_url