Search code examples
fluttergoamazon-s3pre-signed-url

Presigned URL PutObject with Flutter and Golang


I'm trying to upload images to my S3 bucket using presigned URLs. My bucket has no public access, and the CORS policy is this:

[
    {
        "AllowedHeaders": ["*"],
        "AllowedMethods": ["PUT"],
        "AllowedOrigins": ["*"],
        "ExposeHeaders": []
    }
]

I generate a presigned URL using the Go sdk v2:

func GenerateImagePresignedUrl(
    ctx context.Context,
    client *s3.PresignClient,
    userID string,
) (*sigv4.PresignedHTTPRequest, *string, error) {
    exp := time.Duration(time.Duration(60).Minutes())

    suffix := fmt.Sprintf("%v.jpg", time.Now().Unix())
    key := fmt.Sprintf("mobile-users/profile/%v/%v", userID, suffix)

    params := &s3.PutObjectInput{
        Bucket: aws.String(os.Getenv("S3_BUCKET_NAME")),
        Key:    aws.String(key),
    }
    req, err := client.PresignPutObject(ctx, params, s3.WithPresignExpires(exp))
    if err != nil {
        return nil, nil, err
    }
    return req, &key, nil
}

However, after this point I am completely lost. I cannot seem to match the signature, and I keep getting 403 errors. This is the Flutter code that I'm using:

Future directS3Upload({required S3PresignedObject s3Object, required File file}) async {
  final data = await file.readAsBytes();
  final uri = Uri.parse(s3Object.url); // s3Object.url = presigned URL generated in Go
  final res = await put(uri, body: data);

  print(res.statusCode);
  print(res.body);
  print(res.reasonPhrase);
  print(res.headers);
}

I don't know what I'm missing at this point, as I've tried so many combinations of content types, additional fields, and headers. I am only ever uploading images, never video content, so the content type shouldn't be an issue?


Solution

  • I was able to get on the right track with this related question that was seeing similar issues. I had given permissions to my lambda function to access my bucket and perform PutObject operations, but I only included access to the bucket, and not to any objects within the bucket. I modified my terraform IAM permission from this:

    data "aws_iam_policy_document" "s3_allow_write" {
      statement {
        effect    = "Allow"
        actions   = ["s3:PutObject"]
        resources = [local.s3_digital_content_bucket_arn]
      }
    }
    

    To this:

    data "aws_iam_policy_document" "s3_allow_write" {
      statement {
        effect    = "Allow"
        actions   = ["s3:PutObject"]
        resources = ["${local.s3_digital_content_bucket_arn}/*"]
      }
    }
    

    I needed to set the wildcard at the end of the resource in order for the policy to work. As unexciting as this answer is, it is the answer that has finally fixed my problem. I no longer get 403 responses from my Flutter client.

    I also modified my Go lambda to create the pre-signed URL to match the tutorial code from the AWS docs, just to be safe:

    func GenerateProfileImagePresignedUrl(ctx context.Context, client *s3.PresignClient, key string) (*sigv4.PresignedHTTPRequest, error) {
        params := &s3.PutObjectInput{
            Bucket: aws.String(os.Getenv("S3_BUCKET_NAME")),
            Key:    aws.String(key),
        }
        req, err := client.PresignPutObject(ctx, params, func(opts *s3.PresignOptions) {
            opts.Expires = time.Duration(300 * int64(time.Second))
        })
        if err != nil {
            return nil, err
        }
        return req, nil
    }