Search code examples
amazon-s3aws-lambdaaws-amplifyserverless-frameworkaws-amplify-cli

Trying to get Image from Lambda but I get Access Denied. With the Amplify client library works well


I have created the S3 Bucket with Serverless Framework like this:

AssetsBucket:
  Type: AWS::S3::Bucket
  DeletionPolicy: Retain
  Properties:
    CorsConfiguration:
      CorsRules:
        - AllowedMethods:
            - GET
            - HEAD
            - PUT
          AllowedOrigins:
            - '*'
          AllowedHeaders:
            - '*'
          ExposedHeaders:
            - 'x-amz-server-side-encryption'
            - 'x-amz-request-id'
            - 'x-amz-id-2'
            - 'ETag'
          MaxAge: 3000

Then, I have created an identity pool and defined the roles I need:

IdentityPool:
  Type: AWS::Cognito::IdentityPool
  Properties:
    AllowUnauthenticatedIdentities: true
    CognitoIdentityProviders:
      - ClientId: !Ref WebUserPoolClient
        ProviderName: !GetAtt CognitoUserPool.ProviderName

CognitoAuthorizedRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Federated: "cognito-identity.amazonaws.com"
          Action:
            - sts:AssumeRoleWithWebIdentity
          Condition:
            StringEquals:
              "cognito-identity.amazonaws.com:aud": !Ref IdentityPool
            ForAnyValue:StringLike:
              "cognito-identity.amazonaws.com:amr": authenticated

CognitoUnAuthorizedRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Federated: "cognito-identity.amazonaws.com"
          Action:
            - sts:AssumeRoleWithWebIdentity
          Condition:
            StringEquals:
              "cognito-identity.amazonaws.com:aud": !Ref IdentityPool
            ForAnyValue:StringLike:
              "cognito-identity.amazonaws.com:amr": unauthenticated

IdentityPoolRoleMapping:
  Type: AWS::Cognito::IdentityPoolRoleAttachment
  Properties:
    IdentityPoolId: !Ref IdentityPool
    Roles:
      authenticated: !GetAtt CognitoAuthorizedRole.Arn
      unauthenticated: !GetAtt CognitoUnAuthorizedRole.Arn

These roles have these rules:

CognitoAuthorizedRole:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "StringLike": {
                    "s3:prefix": [
                        "public/",
                        "public/*",
                        "protected/",
                        "protected/*",
                        "private/${cognito-identity.amazonaws.com:sub}/",
                        "private/${cognito-identity.amazonaws.com:sub}/*"
                    ]
                }
            },
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::MY_BUCKET_HERE"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::MY_BUCKET_HERE/uploads/*",
                "arn:aws:s3:::MY_BUCKET_HERE/public/*",
                "arn:aws:s3:::MY_BUCKET_HERE/protected/${cognito-identity.amazonaws.com:sub}/*",
                "arn:aws:s3:::MY_BUCKET_HERE/private/${cognito-identity.amazonaws.com:sub}/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::MY_BUCKET_HERE/protected/*"
            ],
            "Effect": "Allow"
        }
    ]
}

CognitoUnAuthorizedRole:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "StringLike": {
                    "s3:prefix": [
                        "public/",
                        "public/*",
                        "protected/",
                        "protected/*"
                    ]
                }
            },
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::MY_BUCKET_HERE"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::MY_BUCKET_HERE/public/*",
                "arn:aws:s3:::MY_BUCKET_HERE/protected/*"
            ],
            "Effect": "Allow"
        }
    ]
}

So, here's my problem:

If I call an object with the method Storage.get with the amplify library I get the image without any problem.

But, If I do this in my LAMBDA:

const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const s3Client = new S3Client();

const command = new GetObjectCommand(params);
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 });

return url;

The lambda returns me the URL but when I get this one from the client I get an Access Denied error.

MyLambda:
    handler: functions/My_Lambda.handler
    environment:
      BUCKET: !Ref AssetsBucket
    iamRoleStatements:
      - Effect: Allow
        Action: s3:GetObject
        Resource: !GetAtt AssetsBucket.Arn

I don't know why this error... With the client, the library works great, but If I try something similar from a Lambda it does not work...

Here is more information about the headers:

HEADERS FROM CORRECT IMAGE (Storage.get method):

Response-headers:

HTTP/1.1 200 OK
x-amz-id-2: HERE_THE_VALUE
x-amz-request-id: HERE_THE_VALUE
Date: Sat, 17 Jul 2021 21:10:06 GMT
Last-Modified: Sun, 11 Jul 2021 19:28:42 GMT
ETag: "HERE_THE_VALUE"
Accept-Ranges: bytes
Content-Type: application/octet-stream
Server: AmazonS3
Content-Length: 1338


Request-headers:

GET /public/menu_icons/ADMINISTRADOR.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIA4YRKNIM2VGLZX7UR%2F20210717%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210717T211004Z&X-Amz-Expires=900&X-Amz-Security-Token=IQoJ... HTTP/1.1
Host: ecommerce-gateway-develop-assetsbucket-72ahiv6louu0.s3.us-east-2.amazonaws.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-GPC: 1
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://localhost:3000/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

HEADERS FROM LAMBDA ERROR OF IMAGE:

Response-headers:

HTTP/1.1 403 Forbidden
x-amz-request-id: HERE_THE_VALUE
x-amz-id-2: HERE_THE_VALUE
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Sat, 17 Jul 2021 21:10:04 GMT
Server: AmazonS3

Request-headers:

GET /public/menu_icons/ADMINISTRADOR.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIA4YRKNIM2VU4GBIEB%2F20210717%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210717T211003Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJ.... HTTP/1.1
Host: ecommerce-gateway-develop-assetsbucket-72ahiv6louu0.s3.us-east-2.amazonaws.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-GPC: 1
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://localhost:3000/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

Some parts of both headers are different... For example, the LAMBDA headers do not have ETag... What can I do? Thanks!

NEW UPDATE

If I take the url from the LOGS of the lambda, I get this:

https://ecommerce-gateway-develop-assetsbucket-72ahiv6louu0.s3.us-east-2.amazonaws.com/public/menu_icons/REPORTES.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=xxxxxxIM22YARUSUJ%2F2xxxxx17%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210717T230538Z&X-Amz-Expires=3600&X-Amz-Signature=0a37f13e2a7axxxxf4d38cebxxxxxxxxx557c8805057ac6ddffe71c&X-Amz-SignedHeaders=host&x-id=GetObject

Which works great.

Then, If I go to the webpage and I sign in as a user with the method Auth.SignIn from Amplify, and execute Storage.get:

https://ecommerce-gateway-develop-assetsbucket-72ahiv6louu0.s3.us-east-2.amazonaws.com/public/menu_icons/ADMINISTRADOR.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIA4YxxxxxxxxxxF20210717%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210717T230825Z&X-Amz-Expires=900&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEG8aCXVzLWVhc3QtMiJHMEUCIE3FrI0oFvpEIxxxxxxxxdm1bc&X-Amz-Signature=716642f08xxxxxxxxxx988e954b0335a2e04df44e7&X-Amz-SignedHeaders=host&x-amz-user-agent=aws-sdk-js%2F3.6.1%20os%2FLinux%20lang%2Fjs%20md%2Fbrowser%2FChrome_91.0.4472.124%20api%2Fs3%2F3.6.1%20aws-amplify%2F4.1.3_js&x-id=GetObject

Which as well, works great.

But, If I call the lambda, now the url changes:

https://ecommerce-gateway-develop-assetsbucket-72ahiv6louu0.s3.us-east-2.amazonaws.com/public/menu_icons/ADMINISTRADOR.svg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIA4xxxxxxxxxNGRZ7U%2F20210717%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210717T230824Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtZmeLenE30kHskEkfe4%3D&X-Amz-Signature=b9a400dxxxxxxxxx047445657919ff3b993b893fcbb3&X-Amz-SignedHeaders=host&x-id=GetObject

Now this url have more parameters...

Here a example of what i'm doing: https://github.com/MontoyaAndres/test_problem_s3_cognito


Solution

  • I am confused with your explanation of your existing system (sorry!), but the general approach would be one of the following:

    Using Cognito

    Your back-end can use Cognito to authenticate the user and then use AssumeRoleWithWebIdentity to return a set of credentials. The user's client can then use those credentials to directly access AWS services based on the the assigned permissions.

    For example, they might be permitted to access their own subdirectory in an Amazon S3 bucket, or read from a specific DynamoDB table. This can be done by sending requests directly to AWS rather than going via the back-end.

    Using pre-signed URLs

    If your goal is purely to grant access to private objects in Amazon S3, then instead of using Cognito, your back-end can generate Amazon S3 pre-signed URLs that provide time-limited access to private objects.

    Whenever the back-end is generating a page that contains a reference to a private object (eg via <img src=...> tags), it can do the following:

    • The app verifies that the user is entitled to access the private object by checking information in the app's database
    • If the user is entitled to access the private object, the back-end generates a pre-signed URL
    • The pre-signed URL is returned in the HTML page (or even as a direct link)
    • When S3 receives the pre-signed URL, it verifies the signature and, if it is correct, returns the private object

    The benefit of this approach is that the app can determine fine-grained access to individual objects rather than simply using buckets and prefixes to define access. This can be very useful in situations where data is shared between users (eg a photo-sharing app where users can share photos with other users) on a per-object basis.

    Don't mix

    In looking through your code samples, it appears that your Cognito roles are granting access to specific parts of an S3 bucket:

    arn:aws:s3:::MY_BUCKET_HERE/protected/${cognito-identity.amazonaws.com:sub}/*
    

    The clients can then use their Cognito-related credentials to directly access that part of the bucket. There is no need to generate pre-signed URLs.