Single-use download links | DoingCloudStuff

Single-use download links

author: Vincent Chan

On occasion, you might want to let someone be able to download a file that you have stored in your private S3 bucket. However, since you don't want to provide that person with perpetual access, you add an expiration and, so, the URL becomes invalid after a preset time-to-live (TTL). If this is all you require, then a presigned url will suffice.

If, instead, you are perhaps more paranoid and wish to further limit the access someone can have to your private bucket, then a different solution is required. In this post, I cover a few ways you can create a download link that can only be used once.

Basic idea

Suppose we provided the link https://made-up.lnk/download?token=abc to a user so that he can use it to download some file, say some-file.pdf. How do we prevent this link from being used multiple times?

The basic idea here is that we simply store the token (e.g. "abc"), file location, and a number of times used field (initialized to 0) in a database table. Then, when the user does use the above link, our server would receive the request. You then query the database table using the provided token and

  • if the record associated with the given token exists, you return the file to the user and then delete the record;
  • if the record associated with the given token does not exist, you return an error message.

An example pipeline

In the overview above, we were only explaining how a single-use download link might work under the assumption that the single-use download link exists and have made its way to the intended user. To properly understand the whole process, we need to explain the creation phase as well. As such, let us provide a more concrete example starting from the creation phase.

The creation phase

Creating a single-use download link

In the creation phase, we have

  1. The user makes a GET request to your server for a presigned url (e.g. https://made-up.lnk/get-link?file=file.pdf).
  2. The request is received by the API Gateway, which then passes the message to a lambda.
  3. The lambda function checks if the requested file even exists in your S3 bucket.
  4. If the file exists and you're okay with allowing it to be downloaded, then you create a unique token (e.g. abc) and create a new record in DynamoDB with the token and file location.
  5. Then, the lambda returns the download link with the token (e.g. https://made-up.lnk/download?token=abc) to the API Gateway, which then forwards the token to the user.

The usage phase

Using a single-use download link Note: Although this diagram looks identical to the one in the previous section, the numbering is different.

Then, when the user does want to use the download link you provided:

  1. The user makes a GET request to the the download link you provided (e.g. https://made-up.lnk/download?token=abc).
  2. Your API Gateway server receives the request and passes it to a lambda.
  3. The lambda function checks with DynamoDB if (a) such a token exists in your DynamoDB table and (b) that no one has used it before. If one or both of the conditions failed, then an error message is passed back to API Gateway and, hence, the user. But if both conditions are okay, then the record is updated to reflect that someone has used it (so that condition (b) would fail in the future).
  4. The lambda the downloads the file itself
  5. and returns it to the user.

Caveat

While the solution above does work, it only works for files smaller than a certain size, namely, 6MB, the maximum size that a lambda function's return message can be. If the your files are 6MB or larger, then modifications and/or trade-offs have to be made.

Alternative 1: Server-based

Using a single-use download link (server-based)

If you truly require the download link be single-use and your files are potentially 6MB or larger, then the only options I can see are server-based, either an EC2 instance or cluster (e.g. EKS and ECS). In such a case, the process is essentially the same. The only difference is that requests are made directly to your server(s) rather than to API Gateway as a proxy to lambda.

Alternative 2: Serverless version

Using a single-use download link (serverless)

If you wish to avoid servers or clusters, and wish to use only serverless technologies, then I know no way of creating a single-use download link for files 6MB or larger. However, serverless remains possible if we qualify "single-use" with "approximately."

The solution in this more lenient scenario is to make use of S3 presigned URLs and redirects.

During the creation of an S3 presigned URL, it is possible to specify a TTL (time-to-live), a time interval after which the presigned URL becomes invalid and unusable. By setting an extremely short TTL (but one that's still larger than the latency involved in returning the presigned URL to the user) and coupling it with a redirect, it is possible to make the URL act as if it's single-use. (The hope is that the TTL is too short for the user to use a 2nd time before the link expires.)

Setting up a redirect

When using lambda and API Gateway, to return a redirect, your lambda should return a JSON object in the following format.

{
    "isBase64Encoded": false,
    "statusCode": 302,
    "headers": {
        "Location": `${the presigned URL}`,
        "Access-Control-Allow-Origin": "*"
    },
    "body": {}
}

The status code 302 is what tells your browser that it should "redirect" itself to a link specified in the headers under "Location."