:books: Today I Learned

How to display an image protected by header-based authentication

programming JavaScript javascript web-apis

I recently had to work with an API that serves images protected by header-based authentication. You have to send a bearer token in the Authorization header for all requests, including images. How do you display such an image in a web page?

<img src='https://api.example.com/secret-image.png' />

This won’t work because an <img> tag cannot send a custom header. You’ll probably get some flavor of 401 Unauthorized error. It would be trivial to make it work if authentication was based on an URL query parameter or a cookie, but it’s not as easy with a header.

Fetch the image

You’ll have to make the HTTP call to get the image yourself so you can send that header. For example, you could do this using the Fetch API:

function fetchWithAuthentication(url, authToken) {
  const headers = new Headers();
  headers.set('Authorization', `Bearer ${authToken}`);
  return fetch(url, { headers });
}

The fetch response contains binary data, for example PNG data. You need to insert this data into the DOM somehow.

Embed the image with a Base64 data URL

My first attempt was to embed the image using a data URL. I used this function I found on Stack Overflow to perform the binary-to-Base64 conversion:

function arrayBufferToBase64(buffer: ArrayBuffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

You can use the arrayBuffer method of the fetch Response to get the array buffer to pass to the Base64 conversion function, then build a properly formatted data URL and use it as the source of the image:

async function displayProtectedImage(
  imageId, imageUrl, authToken
) {
  // Fetch the image.
  const response = await fetchWithAuthentication(
    imageUrl, authToken
  );

  // Convert the data to Base64 and build a data URL.
  const binaryData = await response.arrayBuffer();
  const base64 = arrayBufferToBase64(binaryData);
  const dataUrl = `data:image/png;base64,${base64}`;

  // Update the source of the image.
  const imageElement = getElementById(imageId);
  imageElement.src = dataUrl;
}

Here’s how to use it:

const imageId = 'some-image';
const imageUrl = 'https://api.example.com/secret-image.png';
const authToken = 'changeme';
displayProtectedImage(imageId, imageUrl, authToken);

Yay!

So slow…

It works but I ran into an issue: if you try to display several images like this at the same time and they are large enough, converting all this binary data to Base64 will slow down your UI.

This is because JavaScript in the browser runs on a single-threaded event loop. Since your JavaScript runs in the same thread as the UI, any sufficiently heavy calculation will temporary block everything, making your site feel unresponsive. (I recommend Philip Roberts’ excellent video What the heck is an event loop anyway? if you want to learn more about this topic.)

Maybe a web worker could be a solution to make sure this work is done in a separate thread, but I did not try going down that path.

Use an object URL

In the end, I used the URL Web API, specifically its createObjectURL function. If you have a blob of binary data, you can pass it to this function to create a blob: URL that points to this data in memory:

URL.createObjectURL(blob);
// blob:http://example.com/e88f2e72-94c6-4f79-a40d-fc6749ce

If your blob contains image data, you can use this new URL as the source of an <img> tag. The advantage of this technique compared to constructing a data URL is that you do not have to process the image data at all. This blob URL is simply a pointer to the existing data in memory, with no extra computation required. The data is stored in the blob URL store, a feature of the File API.

You can get the data of a fetch Response as a blob by using its blob function. Let’s rewrite the displayProtectedImage function to take advantage of object URLs:

async function displayProtectedImage(
  imageId, imageUrl, authToken
) {
  // Fetch the image.
  const response = await fetchWithAuthentication(
    imageUrl, authToken
  );

  // Create an object URL from the data.
  const blob = await response.blob();
  const objectUrl = URL.createObjectURL(blob);

  // Update the source of the image.
  const imageElement = getElementById(imageId);
  imageElement.src = objectUrl;
}

You can use it the same way as the previous version:

const imageId = 'some-image';
const imageUrl = 'https://api.example.com/secret-image.png';
const authToken = 'changeme';
displayProtectedImage(imageId, imageUrl, authToken);

Collect the garbage

The memory referenced by object URLs is released automatically when the document is unloaded. However, if you’re writing a single page application with no page refresh or if you generally care about memory consumption and performance, you should let the browser know when this memory can be released by calling the revokeObjectURL function:

URL.revokeObjectURL(someObjectUrl);

In the case of an image, it’s easy to do it automatically as soon as the image is done loading by using its onload callback. Once the image is displayed, the object URL and the referenced data are no longer needed:

imageElement.src = objectUrl;
imageElement.onload = () => URL.revokeObjectUrl(objectUrl);

Here’s an updated displayProtectedImage function that does this:

async function displayProtectedImage(
  imageId, imageUrl, authToken
) {
  // Fetch the image.
  const response = await fetchWithAuthentication(
    imageUrl, authToken
  );

  // Create an object URL from the data.
  const blob = await response.blob();
  const objectUrl = URL.createObjectURL(blob);

  // Update the source of the image.
  const imageElement = getElementById(imageId);
  imageElement.src = objectUrl;
  imageElement.onload = () => URL.revokeObjectUrl(objectUrl);
}

May your UI remain swift.