As I have posted here before, I use oauth2_proxy for authentication rather than relying on homeassistant’s auth features. I use this to provide a consistent authentication method across all applications I host on my server, and I am not interested in having an extra step for authentication just for homeassistant. However, I do really want to be able to leverage any user specific functionality that is coming based on the new auth system.
After some poking around, I was able to find a way to leverage the External Auth feature designed for apps and get nginx to pass through a token based on the email address of the user logged in with oauth2_proxy.
I really wish there was a simpler way to accomplish this by performing HMAC validation of the signature headers from the proxy, but the approach to auth was too unique and opinionated for me to pull it off any other way.
Here is what I did:
Inject an externalApp script
Inject a script into the homeassistant frontend app with nginx’s sub_filter
to set the required window.externalApp
variables:
sub_filter '<head>' '<head><script>window.externalApp={getExternalAuth:function(){fetch("/api/get_token").then((resp) => resp.text()).then(function(data) {window.externalAuthSetToken(true,{"access_token":data,"expires_in":2592000});}).catch(function(error) {window.externalAuthSetToken(false);});},revokeExternalAuth:function(){window.externalAuthRevokeToken(false);}};</script>';
sub_filter_once on;
Formatted, the script looks like:
window.externalApp = {
getExternalAuth: function() {
fetch("/api/get_token").then((resp) => resp.text()).then(function(data) {
window.externalAuthSetToken(true, {
"access_token": data,
"expires_in": 2592000
});
}).catch(function(error) {
window.externalAuthSetToken(false);
});
},
revokeExternalAuth: function() {
window.externalAuthRevokeToken(false);
}
};
It fetches an auth token from a new endpoint. And gives it a very long expiration time.
Tell the frontend to use external auth in a normal browser
In order to get the frontend to use external auth, you have to load the frontend with external_auth=1
in the query string, but there isn’t a way to force a user to load a url with a query string parameter. However, if the frontend determines that you are are not authenticated, it makes a request to /auth/authorize
and the backend redirects to the built-in login page. I used nginx to intercept requests to /auth/authorize
and redirect like this:
location /auth/authorize {
# tell nginx that this request requires authentication with oauth2_proxy
auth_request /oauth2/auth;
# redirect back to the frontend and tell it to use external_auth
return 301 /?external_auth=1;
}
If you are not already authenticated with oauth2_proxy, this will redirect you to login with oauth2_proxy. If you are already logged in, redirect back to the frontend and tell it to use external auth.
Implement the endpoint to return an access token for the user
Now you have to implement the /api/get_token
endpoint that the script in the first step calls. I used the njs
nginx module to read a json file and return a long-lived access token based on the users email.
In the nginx config:
location /api/get_token {
# require the user to be authenticated for this request
auth_request /oauth2/auth;
# set a variable for the logged in users email
auth_request_set $email $upstream_http_x_auth_request_email;
# call the javascript function to lookup and return a token
js_content get_token;
}
NJS script:
var fs = require('fs');
var token_string = fs.readFileSync('/etc/nginx/tokens.json', 'utf8');
var tokens = JSON.parse(token_string);
function get_token(r) {
var email = r.variables["email"];
r.headersOut['Content-Type'] = 'text/plain';
if (email == '') {
r.return(500, "No email found");
}
var token = tokens[email];
if (token == undefined) {
r.return(500, "No token found");
}
return r.return(200, token);
}
token.json
:
{
"[email protected]": "<long-lived access token>",
"[email protected]": "<long-lived access token>"
}
I chose to have this return a long lived access token, but I am sure there would be a way to generate a new access token with the trusted_networks
provider each time with some additional work.
There are a few more details required to get this working which depend on your specific setup. My full nginx config is here for reference.