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.
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
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.getreturns.urlshould be something likehttps://your-project-name.s3-eu-west-1.amazonaws.com/avatars/someimage.jpgIf it includes anything like?response-content-disposition=inline&X-Amz-Security-...after it, just get rid of it.