Amplify-js: Storage.get() with Authorization header instead of signed Query String

Created on 4 Apr 2020  路  7Comments  路  Source: aws-amplify/amplify-js

Is your feature request related to a problem? Please describe.

Currently images uploaded via the Storage api cannot be cached at all. Calling Storage.get() will return an URL which changes every single time.

To work around this and keep the URL static, S3 supports http header based authorization.

We are using react-native and would like to use react-native-fast-image to load images and cache them.

react-native-fast-image support specifying custom HTTP headers for requests. Being able to supply the Authorization header with the signature there should completely resolve this.

Describe the solution you'd like

There should be a method like Storage.getSignedHeaders(...) to retrieve the headers that should be sent with the request.

Describe alternatives you've considered

I could create the signature myself. Perhaps aws-sdk could be used directly to create the signature as well, but I haven't been able to find how.

Presigned URLs seem to be prevalent, while the authorization header is quite obscured.

Any pointers on how the Authorization header could be created manually would be great.

I believe the credentials can be extracted from amplify via the example here: https://aws-amplify.github.io/docs/js/authentication#working-with-aws-service-objects

How could those be passed on to aws-sdk to retrieve the authorization header?

Alternatively, a library like aws4 might also help.

Storage feature-request

Most helpful comment

It should be the url without any query strings. I'm not using Amplify storage any more so I'm not sure what Storage.get returns.

url should be something like https://your-project-name.s3-eu-west-1.amazonaws.com/avatars/someimage.jpg If it includes anything like ?response-content-disposition=inline&X-Amz-Security-... after it, just get rid of it.

All 7 comments

I noticed there is a https://github.com/aws-amplify/amplify-js/blob/master/packages/core/src/Signer.ts#L290 but couldn't find any documentation.

In the mean time I was able to implement this using react-native-aws4 and it works beautifully with react-native-fast-image

Leaving some code here as it may help someone:

import aws4 from "react-native-aws4";
function getS3SignedHeaders(path: string, credentials: any) {
  const url = new URL(path);

  const opts = {
    region: aws_exports.aws_user_files_s3_bucket_region,
    service: "s3",
    method: "GET",
    host: url.hostname,
    path: `${url.pathname}${url.search}`
  };

  return aws4.sign(opts, credentials).headers;
}
    ...
    import { Auth } from "aws-amplify";
    credentials = Auth.essentialCredentials(
      await Auth.currentCredentials()
    );
   ...
   <FastImage
      source={{ uri, { headers: getS3SignedHeaders(uri, credentials) }}}
      style={{ width: size, height: size, borderRadius: size }}
    />

Alternative implementation without the need for any external packages. Some magic was needed, ideally this could be exposed in a friendlier manner.

import { Signer, ICredentials } from "@aws-amplify/core";

export function getS3SignedHeaders(path: string, credentials: ICredentials) {
  const signature = Signer.sign(
    {
      url: path,
      method: "GET",
      headers: {
        "x-amz-content-sha256":
          "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // this is the SHA256 of an empty string ("")
      }
    },
    {
      access_key: credentials.accessKeyId,
      secret_key: credentials.secretAccessKey,
      session_token: credentials.sessionToken
    },
    { region: aws_exports.aws_user_files_s3_bucket_region, service: "s3" }
  );

  return signature.headers;
}

Alternative implementation without the need for any external packages. Some magic was needed, ideally this could be exposed in a friendlier manner.

import { Signer, ICredentials } from "@aws-amplify/core";

export function getS3SignedHeaders(path: string, credentials: ICredentials) {
  const signature = Signer.sign(
    {
      url: path,
      method: "GET",
      headers: {
        "x-amz-content-sha256":
          "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // this is the SHA256 of an empty string ("")
      }
    },
    {
      access_key: credentials.accessKeyId,
      secret_key: credentials.secretAccessKey,
      session_token: credentials.sessionToken
    },
    { region: aws_exports.aws_user_files_s3_bucket_region, service: "s3" }
  );

  return signature.headers;
}

This misses out on some images.

   <FastImage
      source={{ uri, { headers: getS3SignedHeaders(uri, credentials) }}}
      style={{ width: size, height: size, borderRadius: size }}
    />

@andreialecu What are you using for uri here? The value from const url = await Storage.get(fileKey, { level: 'public' });?

It should be the url without any query strings. I'm not using Amplify storage any more so I'm not sure what Storage.get returns.

url should be something like https://your-project-name.s3-eu-west-1.amazonaws.com/avatars/someimage.jpg If it includes anything like ?response-content-disposition=inline&X-Amz-Security-... after it, just get rid of it.

For those interested here's what I ended up doing

import aws4 from 'react-native-aws4';
import awsExports from '../../aws-exports';
import { ICredentials } from '@aws-amplify/core';

function getS3SignedHeaders(path: string, credentials: ICredentials) {
  const url = new URL(path);

  const opts = {
    region: awsExports.aws_user_files_s3_bucket_region,
    service: 's3',
    method: 'GET',
    host: url.hostname,
    path: `${url.pathname}${url.search}`,
  };

  return aws4.sign(opts, credentials).headers;
}

const source = { uri: '', priority: FastImage.priority.normal, headers: {} };
if (data.item.imageUrl && creds) {
  source.uri = data.item.imageUrl.split('?')[0];
  source.headers = getS3SignedHeaders(source.uri, creds);
}

Thanks @andreialecu

Was this page helpful?
0 / 5 - 0 ratings