skip to Main Content

I’ve created an AWS Amplify function with amplify add function resulting in the following basic configuration:

General information
- Name: MyFunction
- Runtime: python

Resource access permission
- Not configured

Scheduled recurring invocation
- Not configured

Lambda layers
- Not configured

Environment variables:
- Not configured

Secrets configuration
- Not configured

I then added a REST API using amplify add api that uses this function, and added a path with "create" and "read" access for authenticated users resulting in the following policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": [
                "arn:aws:execute-api:us-west-2:...:.../staging/POST/reply/*/*",
                "arn:aws:execute-api:us-west-2:...:.../staging/POST/reply/*",
                "arn:aws:execute-api:us-west-2:...:.../staging/GET/reply/*/*",
                "arn:aws:execute-api:us-west-2:...:.../staging/GET/reply/*"            ],
            "Effect": "Allow"
        }
    ]
}

But when I invoke the API from my app, with a logged in authenticated user (who has no trouble using my GraphQL API via DataStore) I get a 403 error.

I can’t figure out what’s happening here. What would cause a 403 error in this case? This is all pretty much out of the box from the Amplify CLI. What’s wrong with the authentication I’m providing?


The code for the Lambda function (generated by the CLI, with no further edits) is:

def handler(event, context):
  return {
      'statusCode': 200,
      'headers': {
          'Access-Control-Allow-Headers': '*',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
      },
      'body': json.dumps('Hello from your new Amplify Python lambda!')
  }

The invoking code (copied from the Amplify documentation) is:

import ... { API } from 'aws-amplify'

// ...

    const callLambdaFunction = async () => {
        try {
            const response = await API.post( 'Chat', '/reply/whatever', {
                body: { data },
                headers: {
                    Authorization: `Bearer ${ (await Auth.currentSession())
                        .getIdToken()
                        .getJwtToken() }`,
                },
            } )

            setResult( response )
        }
        catch ( error ) {
            console.log( error )
        }
    }

Some notes:

  1. Why do I even need to provide authentication? Doesn’t the API.post already know about the currently authenticated user and append the necessary headers? DataStore does.
  2. What do "read", "create", etc. mean in the amplify api CLI? How do the relate to what the endpoint does or is, or who can access it. Is it a secret code for "GET", "POST", etc.?
  3. I’ve tried pasting the JWT I get from Auth.currentSession into Postman but get nonsense:
    {
        "message": "'eyJhbG...0HMs' not a valid key=value pair (missing equal-sign) in Authorization header: 'Bearer eyJhbG...0HMs'."
    }
    

    even if I just paste random text.

2

Answers


  1. Chosen as BEST ANSWER

    Amplify REST APIs do not use Cognito authorization by default. In order to use Cognito authorization you have two options:

    Create a Cognito Authorizer in API Gateway

    1. Create a Cognito user pool authorizer in API Gateway console, providing "Authorization" as the token source value.
    2. Add the Cognito user pool authorizer each of the ANY methods of the resource path that have been assignee IAM authentication in the Amplify CLI
    3. Deploy API
    4. Then add "Authorization" header in the request sent to API Gateway as in the OP:
    headers: {
        Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}`
    }
    

    This is clearly a terrible way to proceed, since it takes place outside of Amplify and will have unpredictable results downstream. There is however a slightly better approach that's a bit more Amplify-ish.

    Override the REST API for use with Cognito user pools

    First execute

    amplify override api
    

    to create a stub overrides.ts file under amplify/backend/api/<resource-name>/ (if one is not already present) then add the following inside of the stub override(...) function in overrides.ts:

    // Add a parameter to your Cloud Formation Template for the User Pool's ID
    resources.addCfnParameter({
        type: "String",
        description: "The id of an existing User Pool to connect. If this is changed, a user pool will not be created for you.",
        default: "NONE",
      },
      "AuthCognitoUserPoolId",
      { "Fn::GetAtt": ["auth<your auth name here>", "Outputs.UserPoolId"], }
    );
    
    // Create the authorizer using the AuthCognitoUserPoolId parameter defined above
    resources.restApi.addPropertyOverride("Body.securityDefinitions", {
      Cognito: {
        type: "apiKey",
        name: "Authorization",
        in: "header",
        "x-amazon-apigateway-authtype": "cognito_user_pools",
        "x-amazon-apigateway-authorizer": {
          type: "cognito_user_pools",
          providerARNs: [
            { 'Fn::Sub': 'arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${AuthCognitoUserPoolId}' },
          ],
        },
      },
    });
    
    // For every path in your REST API
    for (const path in resources.restApi.body.paths) {
      // Add the Authorization header as a parameter to requests
      resources.restApi.addPropertyOverride(
        `Body.paths.${path}.x-amazon-apigateway-any-method.parameters`,
        [
          ...resources.restApi.body.paths[path]["x-amazon-apigateway-any-method"]
            .parameters,
          {
            name: "Authorization",
            in: "header",
            required: false,
            type: "string",
          },
        ]
      );
      // Use your new Cognito User Pool authorizer for security
      resources.restApi.addPropertyOverride(
        `Body.paths.${path}.x-amazon-apigateway-any-method.security`,
        [ { Cognito: [], }, ]
      );
    }
    

    where <your auth name here> is the name of the folder in amplify/backend/auth.

    This is not a whole lot better than the API Gateway option (that's a lot a mysterious code), but at least it's managed my Amplify, and has the advantage of providing the authentication headers for you (no need for the code in the OP or step 4 above).

    (Note that you may get errors when you push after making these changes, but you can safely ignore those.)


    All of this begs the question of why Amplify doesn't support Cognito for REST out of the box (e.g. as a choice in amplify update api) since that is almost certainly the most common scenario.


  2. Looks like you need to send up the authorization header.
    This Amplify API Docs page has the details. You can have it injected by adding a function into the app’s config or per API call. I’m not sure why you need to add the auth header yourself and I’ve seen tutorials where it’s not used and protected calls seem to work. It may be you need to to add the <Authenticator.Provider> HOC around your app in main.tsx.

    Example from doc:

    async function postData() {
      const apiName = 'MyApiName';
      const path = '/path';
      const myInit = {
        headers: {
          Authorization: `Bearer ${(await Auth.currentSession())
            .getIdToken()
            .getJwtToken()}`
        }
      };
    
      return await API.post(apiName, path, myInit);
    }
    
    postData();
    

    EDIT:
    Although I’d expect a 401 or 403, not a 502. But you are missing Auth, so I think the above will solve the problem.

    Edit 2
    Since you’re seeing 502 and not 403, the issue could be you’re not calling your endpoint properly. Check the first two parameters to API.put(). Perhaps follow this tutorial, when you have REST API working, alter to fit your needs.

    Edit 3
    Great progress. Put your token into jwt.io to test it. Try using the Access Token instead of IdToken.

        (await Auth.currentSession()).getAccessToken().getJwtToken()
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search