0

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.
Sri
  • 342
  • 4
  • 17
orome
  • 45,163
  • 57
  • 202
  • 418
  • Might explain your issue https://stackoverflow.com/questions/57168148/unable-to-resolve-not-a-valid-key-value-pair-missing-equal-sign-in-authoriza – JeanMGirard Mar 04 '23 at 06:07

2 Answers2

1

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.

orome
  • 45,163
  • 57
  • 202
  • 418
  • Unfortunately the second method [has (fatal) issues as well](https://github.com/aws-amplify/amplify-category-api/issues/1335): once an override is in place it is impossible to add paths the the API using the CLI (though no errors or messages indicate that paths are failing to add). – orome Mar 08 '23 at 17:28
0

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()
Dave
  • 7,552
  • 4
  • 22
  • 26
  • That gives 403. Why do I need that if the user has already been authenticated? The API should have already added the necessary auth headers, adding them again would just mess things up, no? – orome Feb 23 '23 at 22:56
  • Check the network tab to see what goes out on the wire. The doc page I sent illustrates two ways to add the auth header. You can also use CURL or WGET to poke your API outside your react app and verify the server-side works as expected. – Dave Feb 23 '23 at 22:59
  • What "network tab"? – orome Feb 23 '23 at 23:06
  • In the browser's dev tools network tab/panel. Check the API post request that gets send to verify the Authentication header is missing. – Dave Feb 23 '23 at 23:10
  • I'm calling this from a React Native mobile app. But the real problem is that Amplify seems by default to create things that require diving right into all kinds of AWS services to get them working out of the box. Maybe I could try Postman somehow, but it's not even clear what URL to use to access the API. It's not available in the CLI and the "consoles" — well — – orome Feb 23 '23 at 23:18
  • I mostly use GraphQL between client and server. I think my Amplify REST API base-url was displayed by `amplify status`. I feel your pain with Amplify. When it's not helpful things can get really sticky. You can also check the Lambda's logs. I assume this is failing at API Gateway and the Lambda never gets invoked, so there will not be any logs. Knowing with certainty will narrow your debugging. Did you try adding the auth header? Test w/ access token and id token, will only take you a few minutes to pass/fail that idea. – Dave Feb 23 '23 at 23:28
  • Yeah never had any of these issues with GraphQL. Same client works fine with GraphQL APIs with DataStore (and no need for additional authentication in the calls). – orome Feb 24 '23 at 00:23
  • So I can see the URL for the API in `amplify status`. Any idea how to invoke that (e.g., from Postman) with the necessary payload? – orome Feb 24 '23 at 00:46
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/252094/discussion-between-dave-and-orome). – Dave Feb 24 '23 at 00:49
  • I can confirm that I'm calling correctly and that on paths where authentication is not required the API works. (See updated question.) – orome Feb 25 '23 at 17:52
  • I've used both `getAccessToken` and `getIdToken` and confirmed that the tokens are Cognito users in jwt.io. No change in outcome. – orome Feb 25 '23 at 21:49
  • It turns out to that (in contradiction to the documentation) to get an Amplify API working with Amplify (Cognito) Auth, you need to make modifications to the API Gateway **outside** of Amplify, by first [creating a Cognito user pool authorizer in API Gateway console](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-enable-cognito-user-pool.html), and then adding the Cognito user pool authorizer to a method of the API, and then "redeploy" the API (whatever that means). None of which has anything do do with Amplify! (And who knows how it interacts with Amplify.) – orome Mar 03 '23 at 15:33
  • Oh wait — [now I'm told](https://github.com/aws-amplify/amplify-category-api/issues/1326#issuecomment-1453804059) to use a [different method](https://docs.amplify.aws/cli/restapi/override/#add-a-cognito-user-pool-authorizer-to-your-rest-api) (which doesn't work). – orome Mar 03 '23 at 19:20