Authenticating with external auth and oauth2_proxy to bypass homeassistant auth

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.

3 Likes

Commenting late to the party to say thanks for sharing these detailed instructions. I’ve used your github with great success to implement oauth2.

Really appreciate it.

I have since realized that you do not want to pass the utf8 parameter in the NJS script when reading the json file. If you have more than 2 values, you will start to get memory corruption errors in nginx. Just omit the second param for fs.readFileSync.

1 Like

I’m still trying to figure out a way around google’s decision to prevent authentication from within a webview on the iOS app. Seems like just changing the useragent from the nginx request isn’t sufficient. Thoughts?

Sorry. I just use the PWA app on android. I don’t own any apple devices. I’m guessing you will have to bypass oauth for the ios app.

Have you had a look into Home Assistant Authentication Providers? It would allow for a universal way to authenticate your Home Assistant Instance against a third party OAuth Authorization Server.

Came here to say thanks for this post! This is a nice workaround to the current lack of better support for external authentication providers.

I had to build the NJS module for my Raspberry Pi, but after doing that, I was able to get authentication working following this example using Keycloak :slight_smile:

I appreciate you taking the time to share this with the community!

1 Like

Hey I think it’s a great project! I am trying to do a similar project without relying on Home Assistant authentication. I am a Web developer with Nodejs and ExpressJS but I am new to Home Assistant, I would like to know if you could explain the logic or provide a repository about this project to understand how you solved it. Again, thank you very much! :smiley:

Wishing this project moved forward, HASS is the only thing that I can’t use with Azure AD.