Skip to main content

Authentication Multiplexing

Haven't we all lived some form of the diagram below:

Your real server is behind a reverse proxy. But all ingress traffic comes through a different gated proxy not in your control that locks everything behind it with a Basic Auth.

This is quite a pickle. Because both the basic auth that Nginx is expecting and the bearer token that our server is expecting uses the same Authentication HTTP header.

Basic auth is cool because it is a simple way to password protect a system. The browser itself handles asking the user for password, saving it in memory and making sure all requests have the basic auth header. The HTML/JS code doesn't even have to know if it's password protected.

But it's also inflexible and less secure. Just logging out is a dance. Bearer token based authentication is flexible, can be made much more secure, and allows for a much better user and developer experience.

Eating the Pickle

It would make sense that, for certain applications (for example in-house tools), all incoming traffic is protected by default with a basic auth. But the more complex application-specific authorization is handled with a proper JWT token.

In such cases, we can get by with some JavaScript and Caddy gymnastics. And an extra header: X-Custom-Authorization (arbitrarily named).

Caddy Configuration

Here Caddy is doing the fun things. It sets the value of the Authorization header to whatever is in the X-Custom-Authorization header before any request goes to the upstream server.

example.com {
reverse_proxy localhost:3000{
header_up Authorization {http.request.header.X-Custom-Authorization}
}
}

What's going on in the client browser?

We also need some code on the browser to make sure that:

  1. the Authorization header doesn't exist. If the browser notices that we have manually set the Authorization header, it won't overwrite it with the basic auth credentials. Since asking for, saving, and using the basic auth is handled by the browser, we must allow browser to handle this auth.

  2. before any request goes to the browser, it is intercepted and it's outgoing bearer Authorization header is renamed to X-Custom-Authorization, leaving the Authorization free for the browser to use.

Axios Interceptor

If we had complete control of the client code, we'd just make sure all our requests went through a middleware before passing through. For example, as an Axios interceptor:

axios.interceptors.request.use(
(config) => {
config.headers["X-Custom-Authorization"] = config.headers["Authorization"];
delete config.headers["Authorization"];
return config;
},
(error) => {
return Promise.reject(error);
}
);

Pocketbase

Or as a PocketBase before send hook:

pb.beforeSend = function (url, options) {
if (options.headers["Authorization"]) {
options.headers = Object.assign({}, options.headers, {
"X-Custom-Authorization": options.headers["Authorization"],
});
delete options.headers.Authorization;
}

return { url, options };
};

Chrome Extension

However, we might not always have access to all parts of the client JS code. If its for an internal tool, then a simple chrome extension might suffice:

chrome.webRequest.onBeforeSendHeaders.addListener(
function (details) {
if (details.frameType === "outermost_frame") {
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name === "Authorization") {
details.requestHeaders[i].name = "X-Custom-Authorization";
break;
}
}
}
return { requestHeaders: details.requestHeaders };
},
{ urls: ["<all_urls>"] },
["blocking", "requestHeaders"]
);

Conclusion

Whatever the method, what we're doing is making sure that the right layers in the stack see the appropriate information.