Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactJS: Render a private image asset from S3

I am using NodeJS backend API server that allows the frontend (ReactJS / React Native) to upload an image using form-data. This is the code I am using:

const s3 = new aws.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: "us-east-1",
});

...

const upload = multer({
  storage: multerS3({
    s3,
    bucket: process.env.AWS_BUCKET,
    acl: 'private',
    metadata(req, file, cb) {
      cb(null, {fieldName: file.fieldname});
    },
    key(req, file, cb) {
      cb(null, Date.now().toString() + '.png');
    }
  })
})

...

app.post('/upload', upload.single('photo'), (req, res, next) => {
  res.json(req.file)
})

The API server works as it should and the image is uploaded in a private secured bucket using AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

However now I want to be able to render this image on ReactJS or React Native. Do you recommend building another NodeJS route called something like /render and somehow stream it to the backend? If yes, how can I do it?

Alternatively, should I render it directly from the frontend and risking exposing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY keys to the user?

like image 668
James Avatar asked Oct 17 '18 08:10

James


People also ask

How do I download from AWS s3 using Getobject in Reactjs?

js file, using React Bootstrap as a dropdown menu to be able to select a particular file name: import React, { useState } from 'react'; import { ListGroup, Dropdown } from 'react-bootstrap'; import AWS from 'aws-sdk'; const InputDownload = () => { const [template, setTemplate] = useState('Choose Template'); AWS.


2 Answers

After a number of trial and errors, I found out that the most elegant solution is to have NodeJS to take care of the communication with S3. The frontend will then GET the image from NodeJS through a route which can be done as follows:

  app.get("/image/:imageId", function(req, res, next) {
    var params = { Bucket: keys.AWS_BUCKET, Key: req.params.imageId };
    s3.getObject(params, function(err, data) {
      if (err) {
        return res.send({ error: err });
      }
      res.send(data.Body);
    });
  });

Why do I think that this is more elegant than the solution proposed by luboskmac?

For starters, I am not exposing the asset to the public at no point in time. I am only revealing the NodeJS GET route to the end user.

Secondly, if I wanted to restrict access to this route, I can simply use passportJS inside NodeJS to, for example, restrict access to the asset to only logged in users. This is useful in many scenarios, such as in the case of GDPR requirements.

like image 103
James Avatar answered Sep 20 '22 06:09

James


Solution is simple:

  1. Randomize S3 URLs, so that they are not guessable (e.g. use UUIDs)
  2. Make these assets public
  3. Make all S3 folders private, so that if user in one browser have URL to certain asset, he/she can't list contents of S3 folders above.

This way, users would be able to access only assets you send them from server + without AWS API keys (sending keys to browser would be huge security flaw)

This is very same concept e.g. Google Docs, Dropbox uses when you share a link and send it to somebody. Everybody in the world would be able to read that link, but if link is not guessable, asset can be accessed only if you share that link.

like image 25
luboskrnac Avatar answered Sep 18 '22 06:09

luboskrnac