如何防止使用AWS Amplify API时出现403错误?

huangapple go评论76阅读模式
英文:

How do I prevent 403 error with AWS Amplify API?

问题

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

通用信息
- 名称: MyFunction
- 运行时: python

资源访问权限
- 未配置

定期调用
- 未配置

Lambda层
- 未配置

环境变量:
- 未配置

秘密配置
- 未配置

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 they 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.
英文:

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.

答案1

得分: 2

Amplify REST APIs 默认情况下不使用 Cognito 授权。要使用 Cognito 授权,您有两个选项:

在 API Gateway 中创建 Cognito Authorizer

  1. 在 API Gateway 控制台中创建 Cognito 用户池授权者,将 "Authorization" 作为令牌源值提供。
  2. 将 Cognito 用户池授权者添加到已分配 IAM 身份验证的 Amplify CLI 中的资源路径的任何方法。
  3. 部署 API。
  4. 然后在请求中的 发送到 API Gateway 中添加 "Authorization" 标头,如下所示:
headers: {
    Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}`
}

这显然是一个不太好的方法,因为它在 Amplify 之外进行,会导致不可预测的结果。不过,有一种稍微更符合 Amplify 风格的方法。

覆盖 REST API 以与 Cognito 用户池一起使用

首先执行以下命令:

amplify override api

创建一个存根文件 overrides.tsamplify/backend/api/<resource-name>/ 下(如果尚不存在),然后在 overrides.ts 文件的存根 override(...) 函数中添加以下内容3

// 为 Cloud Formation 模板添加一个参数,用于用户池的 ID
resources.addCfnParameter({
    type: "String",
    description: "要连接的现有用户池的 ID。如果更改此值,将不会为您创建用户池。",
    default: "NONE",
  },
  "AuthCognitoUserPoolId",
  { "Fn::GetAtt": ["auth<your auth name here>", "Outputs.UserPoolId"], }
);

// 使用上面定义的 AuthCognitoUserPoolId 参数创建授权者
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}' },
      ],
    },
  },
});

// 对于 REST API 中的每个路径
for (const path in resources.restApi.body.paths) {
  // 将 Authorization 标头添加为请求的参数
  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",
      },
    ]
  );
  // 使用新的 Cognito 用户池授权者进行安全性设置
  resources.restApi.addPropertyOverride(
    `Body.paths.${path}.x-amazon-apigateway-any-method.security`,
    [ { Cognito: [], }, ]
  );
}

其中 <your auth name here>amplify/backend/auth 文件夹中的名称。

这与 API Gateway 选项相比并不完全好(其中包含了很多神秘的代码),但至少它是由 Amplify 管理的,具有为您提供身份验证标头的优势(不需要上述步骤 4 中的代码)。

(请注意,在进行这些更改后,可能在推送后会收到错误,但可以安全地忽略这些错误。)


所有这些都引出了一个问题,为什么 Amplify 不会默认支持 REST 中的 Cognito(例如,在 amplify update api 中选择的选项),因为这几乎肯定是最常见的情况。

英文:

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/&lt;resource-name&gt;/ (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&#39;s ID
resources.addCfnParameter({
    type: &quot;String&quot;,
    description: &quot;The id of an existing User Pool to connect. If this is changed, a user pool will not be created for you.&quot;,
    default: &quot;NONE&quot;,
  },
  &quot;AuthCognitoUserPoolId&quot;,
  { &quot;Fn::GetAtt&quot;: [&quot;auth&lt;your auth name here&gt;&quot;, &quot;Outputs.UserPoolId&quot;], }
);

// Create the authorizer using the AuthCognitoUserPoolId parameter defined above
resources.restApi.addPropertyOverride(&quot;Body.securityDefinitions&quot;, {
  Cognito: {
    type: &quot;apiKey&quot;,
    name: &quot;Authorization&quot;,
    in: &quot;header&quot;,
    &quot;x-amazon-apigateway-authtype&quot;: &quot;cognito_user_pools&quot;,
    &quot;x-amazon-apigateway-authorizer&quot;: {
      type: &quot;cognito_user_pools&quot;,
      providerARNs: [
        { &#39;Fn::Sub&#39;: &#39;arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${AuthCognitoUserPoolId}&#39; },
      ],
    },
  },
});

// 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][&quot;x-amazon-apigateway-any-method&quot;]
        .parameters,
      {
        name: &quot;Authorization&quot;,
        in: &quot;header&quot;,
        required: false,
        type: &quot;string&quot;,
      },
    ]
  );
  // Use your new Cognito User Pool authorizer for security
  resources.restApi.addPropertyOverride(
    `Body.paths.${path}.x-amazon-apigateway-any-method.security`,
    [ { Cognito: [], }, ]
  );
}

where &lt;your auth name here&gt; 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

得分: 0

需要发送授权头部。这个Amplify API文档页面有详细信息。您可以通过在应用的配置中添加一个函数或每个API调用来注入它。我不确定为什么您需要自己添加授权头部,我看过一些教程,它们没有使用授权头部,但受保护的调用似乎可以工作。也许您需要在main.tsx中的应用周围添加<Authenticator.Provider> HOC。

文档中的示例:

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();

编辑
尽管我期望会得到401或403的错误,而不是502。但是您缺少身份验证,所以我认为上面的代码将解决问题。

编辑2
由于您看到的是502而不是403,问题可能是您没有正确调用端点。检查API.put()的前两个参数。也许您可以在有REST API工作时,调整以适应您的需求,可以参考这个教程

编辑3
进展不错。将您的令牌放入jwt.io以进行测试。尝试使用访问令牌而不是身份令牌。

(await Auth.currentSession()).getAccessToken().getJwtToken()
英文:

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 &lt;Authenticator.Provider&gt; HOC around your app in main.tsx.

Example from doc:

async function postData() {
const apiName = &#39;MyApiName&#39;;
const path = &#39;/path&#39;;
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()

huangapple
  • 本文由 发表于 2023年2月24日 06:23:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/75550927.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定