Reverse proxy for guest user access?

I’ve got this use case where I want to be able to give guests or employees limited access. Things like to open the garage door to the shop…

I’ve read a bunch of threads on guest access and not found anything quite like I want. Ideally the guest can access a URL and they get access to one specific dashboard. For employees I’d like the same, but ideally keep some track of who did what and when.

I’ve been using kiosk mode to secure my phones dashboard when it’s locked (android device control), but that works because you can’t modify the URL path to disable kiosk mode.

Tonight, I wondered if I could create a dashboard and use a reverse proxy to access just that dash, that might keep people from being able to append queries that break out of kiosk mode right? From a security/access control perspective I could do authentication on the reverse proxy. A downside to this is that HA would see everyone as one user, so there’s no logging about who did what if I had multiple users.

Just spit balling here, could that work and be “secure”? Any other ideas?

I create JavaScript webpage and linked it to HA API with auth token.

Page has “doorbell” and “open with code” button.

I created OTP sensor for each user. They can enter OTP to allow opening. This prevent sharing and requires time of use authorization. It also allows me to see who entered code to open

There is automation that verifies OTP code along with any other rules, like time of day limits, and executes appropriate action. In this case it either opens gate, sends image to me and rings bell or if wrong code it rings the bell and send notification.

Since the page only sends info to HA (the OTP code) there is no possibility of sidestepping this to open

If interested I can share later

1 Like

This sounds like a really solid solution.

I don’t fully understand where the token will be that it’s effective, but can’t be reverse engineered, however I know very little about JavaScript.

If you’d be willing to share, I think I would benefit, as well as some others who’ve looked for something similar.

Thank you!

Site send http post with OTP as data. This gets redirected by my proxy server to weblink with bearer token to send data to HA automation. I need to check if it is possible to retrieve the redirect link which would also mean grabbing bearer token but for now I’m not terribly concerned and actually more concerned about someone with access to the webpage spamming the doorbell

I will edit my first post with code. Worst case maybe someone can improve it. I’m no coder but can make useful junk every once in a while :grin:

I’m following you know, at least in concept. I’m happy to help trying to exploit it too. :grin:

In my case it’s only going to be exposed via secure WiFi, and highly unlikely any person connecting will have the background to realize and exploit a vulnerability, but… I’d rather prefer to keep it properly locked down.

1 Like

HA AUTOMATION

alias: ACTION_OTPWebhookGateOpen
description: ""
trigger:
  - platform: webhook
    allowed_methods:
      - POST
      - PUT
      - GET
      - HEAD
    local_only: false
    webhook_id: webhook2
    id: code
  - platform: webhook
    allowed_methods:
      - POST
      - PUT
      - GET
      - HEAD
    local_only: false
    webhook_id: webhook1
    id: doorbell
condition: []
action:
  - if:
      - condition: trigger
        id:
          - code
      - condition: template
        value_template: "{{ (trigger.json) == (states.sensor.person_otp.state) }}"
        enabled: true
    then:
      - service: switch.turn_on
        target:
          entity_id: switch.gate_switch_3
        data: {}
        enabled: true
      - service: notify.mobile_app_myphone
        data:
          data:
            entity_id: camera.frigate_gate03
          message: The Gate was opened using Code
    else:
      - if:
          - condition: state
            entity_id: siren.doorbell_play_tone
            state: "off"
            for:
              hours: 0
              minutes: 0
              seconds: 10
            enabled: true
mode: single

EDIT

It’s not in this code but the else condition needs a then action. Should be simple enough to add. I deleted by mistake when sanitizing for posting here

1 Like

NGINX PROXY

    #############################################
    #                 HTTPS MYSITE         #
    #############################################
    server {
        listen               443 ssl;
        http2 on;
        server_name         mysite.com;
        access_log           /var/log/nginx/access/mysite.log;
        error_log            /var/log/nginx/error/mysite.log;
        ssl_certificate      /etc/cloudflare/mysite.pem;
        ssl_certificate_key  /etc/cloudflare/mysite.key;


        ssl_protocols        TLSv1.3 TLSv1.2;
        ssl_ciphers          HIGH:!aNULL:!MD5;
        ssl_stapling          on;
        ssl_stapling_verify   on;
        ssl_session_timeout  5m;

        location / {
            root /var/www/mysite;
        }


        location /doorbell {
            return 308 https://mysite.com/api/webhook/webhook1;
        }

        location /gatecode {
            return 308 https://mysite.com/api/webhook/webhook2;
        }



    } #end mysite


webhooks are created at https://yourHA.com/config/cloud/account

1 Like

HTML SITE

<!DOCTYPE html>
<html lang="en">
<body>


<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Welcome to MY Home </title>


<style>
body {
  color: #FFFFFF;
  text-shadow: 10px 10px 10px #000000;
  background-image: url('photo.jpg');
  width: 100%;
  min-height: 100vh;
  background-repeat: no-repeat;
  background-position: 75% center;
  background-attachment: fixed;
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
  background-size: cover;
}


.btn {
  background: #3498db;
  background-image: -webkit-linear-gradient(top, #3498db, #2980b9);
  background-image: -moz-linear-gradient(top, #3498db, #2980b9);
  background-image: -ms-linear-gradient(top, #3498db, #2980b9);
  background-image: -o-linear-gradient(top, #3498db, #2980b9);
  background-image: linear-gradient(to bottom, #3498db, #2980b9);
  -webkit-border-radius: 33;
  -moz-border-radius: 33;
  border-radius: 33px;
  -webkit-box-shadow: 4px 5px 3px #697cdb;
  -moz-box-shadow: 4px 5px 3px #697cdb;
  box-shadow: 4px 5px 3px #697cdb;
  font-family: Arial;
  color: #f5ebf5;
  font-size: 20px;
  padding: 10px 20px 10px 20px;
  text-decoration: none;
  margin: 12px;
}

.btn:hover {
  background: #29dde3;
  background-image: -webkit-linear-gradient(top, #29dde3, #3498db);
  background-image: -moz-linear-gradient(top, #29dde3, #3498db);
  background-image: -ms-linear-gradient(top, #29dde3, #3498db);
  background-image: -o-linear-gradient(top, #29dde3, #3498db);
  background-image: linear-gradient(to bottom, #29dde3, #3498db);
  text-decoration: none;
}
</style>


</head>


<input type="text" id="currentDateTime">
<h1>CMy House</h1>
<h2>My Address</h2>



  <button class="btn" onclick="Doorbell()">Ring Doorbell</button> 
  <button class="btn" onclick="OpenGate()">Open Gate</button>


  <script>
    function Doorbell() {
      let phonenum = prompt("Please Enter Your Phone Number", "");
      var request = new XMLHttpRequest();
      request.open("POST", "https://mysite.com/doorbell");

      request.setRequestHeader('Content-type', 'application/json');

      request.send(JSON.stringify(phonenum));
    }
  </script>

  <script>
    function OpenGate() {
      let code = prompt("Enter Code", "");
      
      if (code != null) {
      var request = new XMLHttpRequest();
      request.open("POST", "https://mysite.com/gatecode");

      request.setRequestHeader('Content-type', 'application/json');

      request.send(JSON.stringify(code));
      }
    }
  </script>

  <script>
    var today = new Date();
    var date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
    var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
    var dateTime = date+' '+time;
    document.getElementById("currentDateTime").value = dateTime;
  </script>	

</html>
1 Like

It’s gonna take me a bit to implement and test this. I’ll post back when I get it set up. :call_me_hand:t3: Thank you!

Alright, finally getting to this… I have a couple questions if you don’t mind.

Is your mysite.com a different subdomain or something than your main URL to home assistant? I’d assume so, but it’s unclear from your nginx snippit. I’m running into CORS errors, and the locations don’t appear to be triggering the automation. Wondering if that’s my issue. I am using two subdomains.

I get an automation trigger if I hit the URL from a browser or CURL, but not from the button. When I hit the button I get nothing. Finally thought to check the browser console…

I did try disabling CORS, however I still get an error. I’m almost certain my issue is from the proxy configuration, however I’m rather inexperienced. Most of my success has been via NPM, with a few backend tweaks. Do you have any suggestions on where to start?

nginx.conf
#############################################
    #                 HTTPS MYSITE         #
    #############################################
    server {
        listen               443 ssl;
        http2 on;
        server_name         guest.lab.ha;
        access_log           /var/log/nginx/access/mysite.log;
        error_log            /var/log/nginx/error/mysite.log;
        
        include conf.d/include/letsencrypt-acme-challenge.conf;
        include conf.d/include/ssl-ciphers.conf;
        ssl_certificate /etc/letsencrypt/live/npm-7/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/npm-7/privkey.pem;


        ssl_protocols        TLSv1.3 TLSv1.2;
        ssl_ciphers          HIGH:!aNULL:!MD5;
        ssl_stapling          on;
        ssl_stapling_verify   on;
        ssl_session_timeout  5m;

        location / {
          root /data/guest_server_npm;
        }

        location /doorbell {
          return 308 https://ha.lab.ha/api/webhook/-xxxxxxxxxxxxxxxxxxxx;
        }

        location /garage {
          return 308 https://ha.lab.ha/api/webhook/-xxxxxxxxxxxxxxxxxxxx;
        }


    } #end mysite

And, a few questions for when I get the trigger working:

  • What type of sensor / config are you using to affect the state of sensor.person_otp.state?
  • Where do the phone numbers get logged? Do you put that in the notification message that gets sent to the device?

Thanks!

Yes. I should have made that more clear. The 308 redirects to the HA main site and API. This webpage exist in separate domain. Both domains are my own private.

Did you try other browsers or do you have some pop up blocker? I’m not expert at this but CORS errors seem to be browser side error not webserver error. I’m guessing your browser doesn’t like the redirect. I first tested with direct api call from the button and later moved it into nginx redirect. You can expire the token after testing to be sure it didn’t get picked up and leaked

I do nothing here. OTP sensor automatically creates tokens every minute.

I will post automation if I didn’t already. I never created log for numbers since only my daughter used this. The data received is placed in the message sent in ha notification. Really it currently excepts anything including code FYI. Need to filter inputs in nginx or html code at some point.

Above I said I left something out of automation so I repost below

AUTOMATION

alias: ACTION_OTPWebhookGateOpen
description: ""
mode: single
triggers:
  - allowed_methods:
      - POST
      - PUT
      - GET
      - HEAD
    local_only: false
    webhook_id: action-otpwebhookgateopen-<token1>
    trigger: webhook
  - allowed_methods:
      - POST
      - PUT
      - GET
      - HEAD
    local_only: false
    webhook_id: action-otpwebhookgateopen-<token2>
    id: doorbell
    trigger: webhook
conditions: []
actions:
  - if:
      - condition: trigger
        id:
          - code
      - condition: or
        conditions:
          - condition: template
            value_template: "{{ (trigger.json) == (states.sensor.person_otp.state) }}"
    then:
      - target:
          entity_id: switch.gate_switch_3
        data: {}
        enabled: true
        action: switch.turn_on
      - data:
          message: The Gate was opened using Code
          data:
            entity_id: camera.frigate_gate01
            tag: ACTION_OTPWebhookGateOpen
        action: notify.mobile_app_iphone1
      - data:
          message: The Gate was opened using Code
          data:
            entity_id: camera.frigate_gate01
            tag: ACTION_OTPWebhookGateOpen
        action: notify.mobile_app_iphone2
    else:
      - if:
          - condition: state
            entity_id: siren.doorbell_play_tone
            state: "off"
            for:
              hours: 0
              minutes: 0
              seconds: 10
            enabled: true
        then:
          - data:
              tone: "5"
            enabled: true
            target:
              entity_id: siren.doorbell_play_tone
            action: siren.turn_on
          - data:
              message: Gate Access Requested {{trigger.json}}
              data:
                entity_id: camera.frigate_gate01
                tag: ACTION_OTPWebhookGateOpen
            action: notify.mobile_app_iphone1
          - data:
              message: Gate Access Requested {{trigger.json}}
              data:
                entity_id: camera.frigate_gate01
                tag: ACTION_OTPWebhookGateOpen
            action: notify.mobile_app_iphone2


10-4

Silly me about the OTP sensor. I didn’t think it might be an integration. :joy: Makes sense now.

Are you using Nabu Casa to expose your webhooks? I’m running everything local, and wondering if that’s why I’m running into proxy errors.

Thanks for all that, and the rest of the automation too!

Even sillier me…

My website had my ha.x.com url on the buttons instead of guest.x.com. Once I caught that all the rabbithole of CORS went away…

Now the final issue I’m having is getting the json info to get caught by the automation.
EDIT - I was missing a { at the beginning of the template that read the trigger.

I’ve got this working in a with test actions, but I’m gonna swap everything to the real deal and share a code snippets of what finally worked.

Security FYI - the webhook string is visible to anyone visiting the website and inspecting the headers. I don’t think it’s a security issue as long as you don’t allow anything critical (like a door/gate/garage) to be triggered without the OTP condition.

Posted a writeup of my whole setup here. Thanks for all the help @tmjpugh!