Handling authentication of images and other assets

Lukas
medialesson
Published in
5 min readMay 13, 2024

--

Fetching and handling secured resources from a server is part of most web developer’s daily business. But when it comes to images and other static assets, we can face unforeseen challenges. This is because HTML elements like the Anchor element <a> or the Image Embed element <img> do not provide any built-in authentication options.

A common solution to this problem is to put an access token to the query parameter of the URL. Azure Storage Accounts provide so-called shared access signature (SAS) tokens, in AWS we can construct “presigned” requests to access Amazon S3 data. However, sending access tokens as part of the URL query parameter has been considered insecure for a long time, and was finally disallowed in recent authorization framework drafts.¹²

Cloud Storage in a Virtual Network

In a project, we recently faced the following situation: The cloud storage was part of a virtual network and therefore not directly accessible. The backend provided endpoints to fetch images and other assets. It expected bearer token authentication with the “Authorization” request header.

A simplified implementation of such an endpoint with Microsoft’s Azure Blob Storage client library for .NET looked like this:

BlobServiceClient service = new(...);
BlobContainerClient container = service.GetBlobContainerClient(...);
BlobClient blob = container.GetBlobClient(...);

Azure.Response<BlobProperties>? properties =
await blobClient.GetPropertiesAsync(cancellationToken: ...);

ContentDisposition contentDisposition = new(properties.Value.ContentDisposition);

IResult result = Results.File(
fileStream,
contentDisposition.DispositionType,
contentDisposition.FileName
);

Besides the actual file content, the endpoints also return the file’s type information and the original filename. The latter becomes important if we don’t want to end up with random file names. More on this later.

Object URLs

With the endpoints described above, we can now fetch every image and other assets via a common XHR (XML HTTP Request). In our Angular project, we made use of the Microsoft Authentication Library (MSAL), which comes with a built-in interceptor solution for authentication. Therefore, we only had to make sure to always use Angular’s HTTP client implementation for all requests. The “Authorization” header would then get automatically added to each request.

But regardless of which way we choose to make our XHRs and how we ensure that the “Authorization” header is set, we can request and receive a standardized Blob object. Since we didn’t want to bloat our models with additional blob objects or Base64 encoded strings, we always converted them to native Object URLs. These can be treated like any normal image or asset URL.³

Lazy Loading and Third-Party Libraries

It’s not unlikely that images and other assets have a size of several megabytes. Loading assets without need can become a serious performance problem. The (lazy) loading feature of the Image Embed element <img> is a great way to avoid unnecessary network traffic. Unfortunately, this and other native features as well as third-party libraries will not always work as expected when working with object URLs. When the object URLs get created, the asset was downloaded already. Thus, it requires customized solutions to not load images and other assets too early.

For images, we extended an existing approach of an Angular pipe named httpImgSrc. Besides loading the blob and creating the object URLs, it also takes care of a loading indicator, a fallback error image and cleaning up the memory.

<img [src]="someServerUrl | httpImgSrc">

This pipe doesn’t work with Angular’s NgOptimizedImaged directive for lazy loading. But the recently introduced Deferrable Views might help to remedy this issue.

Downloading with Original File Names

If we have an object URL, we can pass it to the Anchor element in the common way.

<a href="someObjectUrl">

But when creating object URLs, the original filename gets lost. When users want to download an asset, they receive an arbitrary UUID with the correct file extension. Using the Object URL’s constructor that accepts a File definition with filename didn’t help in our case. Instead, we had to keep track of the original filename ourselves and pass it to the Anchor element via the download attribute.

<a href="someObjectUrl" download="originalFileName">

To initialize a download process programmatically, we used the FileSaver.js library. It implements the same approach as described above without rendering the Anchor element.

Caching

When displaying the same images frequently, caching is an efficient way to avoid unnecessary network requests. The browser will create new object URLs, even for identical blobs. But we can tell the browser to cache and not request the same asset URL over and over again. To achieve this, the server must add the response header Cache-Control to each request of an image. We chose the values private to make only the browser cache images, and max-age to limit the lifetime of cached images.

Choosing a reasonable lifetime of cached values can depend on various factors and security concerns. In our case, we chose the lifetime of the refresh token.

Cleanup

When working with object URLs, there is always one thing to consider. As the MDN states, “the URL lifetime is tied to the document in the window on which it was created”. And, “if there are safe times when you can explicitly unload them, you should do so”.

In our particular case, we revoked object URLs when components got destroyed. In cases where this didn’t work, we set timers with an interval similar to the lifetime of the refresh token. Using an authentication process with redirects can also help to clean up the memory.

Conclusion

The described approach to handle secured images and assets on a website comes with a couple of issues. Lazy loading and third-party libraries may not work as expected, the original file names can get lost, and it requires custom caching and cleanup strategies. But compared to any query string authentication approach, it can be considered a more secure alternative, while requiring fewer server-side modifications.

Originally published at https://lukket.me on May 13, 2024.

  1. OAuth 2.0 recommends not to use URL query parameters for authentication since 2012. The OAuth 2.1 draft states that developers must not use the approach in 2024.↩︎
  2. Please refer to Microsoft’s blog post on potential security risks and best practices when working with SAS tokens.↩︎
  3. The Anchor element <a> or the Image Embed element <img> accept object URLs like any other valid URL.↩︎

--

--