amazon-web-servicesamazon-s3aws-amplifypre-signed-urlamplifyjs

AWS S3 pre-signed URL expires due to short authentication token expiration on Amplify


I have a ReactJs application that retrieves an S3 URL from the API and get a pre-signed version of it using the Amplify Storage library.

This URL is the source of a video, the code is something like this:

           Storage.get(
             outputVideoSrc,
            { expires: 43200 }
           ));

But I have noticed that even tho the expiration time is really long, after ~1 hour the video stops playing, showing a message of Network error and aborting the playback.

If I get the video URL and try to access it from my browser I see an error similar to it:

<Error>
<Code>ExpiredToken</Code>
<Message>The provided token has expired.</Message>
<Token-0>
...

After some googling, I found that this expiration is due to the authentication token being expired, not the pre-signed URL per se. However, I need this URL to work for more than 1 hour, so the user can work with the video for long period of time.

It seems that it is not possible to customize how long the token will last on Amplify: https://github.com/aws-amplify/amplify-js/issues/2714

I have also tried to keep reloading the video after some time in the hope that I get a refreshed token, but:

  1. It doesn't work
  2. Even if it works I need to reload the video, which is terrible UI

Which alternatives do I have in order to make this work and not having the video stop playing all the time?


Solution

  • The only way I have found to fix it is to use a lambda function to pre-sign the URL and use it to resolve a GraphQL field on my schema.

    So, on using the amplify-cli I have added a new function

    amplify function add

    In the example here I'm naming it getUrls

    My function has a code similar to this:

    import boto3
    from botocore.exceptions import ClientError
    
    s3_client = boto3.client('s3')
    bucket = 'my-bucket'
    
    def handler(event, context):
      print('received event:')
      print(event)
    
      urls = []
    
      if 'typeName' in event and event['typeName'] == 'MyType' and event['fieldName'] == 'urls':
        print('Requesting field keys for MyType...', event['source']['keys'])
        for key in event['source']['keys']:
          try:
              signed_url = s3_client.generate_presigned_url(
                'get_object',
                Params={
                  'Bucket': bucket,
                  'Key': key,
                },
                ExpiresIn=86400  # 24 hours
              )
    
              print('Signed URL: ', signed_url)
              if signed_url:
                urls.append(signed_url)
          except ClientError as e:
              print('Error', e)
    
      return urls
    

    Don't forget to add permission to S3:GetObject on the bucket for your function on getUrls-cloudoformation-template.json Add on your PolicyDocument:

                            {
                                "Sid": "S3",
                                "Effect": "Allow",
                                "Action": [
                                    "s3:GetObject"
                                ],
                                "Resource": "arn:aws:s3:::mybucket/*"
                            }
    
    

    Then add it as a resolver for your field on the schema.graphql

    type MyType @model
    {
      id: ID!
      keys: [String!]
      urls [String!] @function(name: "getUrls-${env}")
    }
    

    That is it, when you request for the field urls it will return the pre-signed url valid for 24 hours, you can then change it to any duration you want.