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:
-
the
Authorization
header doesn't exist. If the browser notices that we have manually set theAuthorization
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. -
before any request goes to the browser, it is intercepted and it's outgoing bearer
Authorization
header is renamed toX-Custom-Authorization
, leaving theAuthorization
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:
- background.js
- manifest.json
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"]
);
{
"manifest_version": 2,
"name": "Outbound Header Modifier",
"version": "1.0.0",
"permissions": ["webRequest", "webRequestBlocking", "https://example.com/*"],
"background": {
"scripts": ["background.js"],
"persistent": true
}
}
Conclusion
Whatever the method, what we're doing is making sure that the right layers in the stack see the appropriate information.