Authorization by username and password (#668)

* Auth

* Logout

* Lint fix

* Small hassio fix

* Reverted uppercase

* Secrets editor

* Reverted secrets editor

* Reverted log height

* Fix default username
This commit is contained in:
Nikolay Vasilchuk 2019-10-13 14:52:02 +03:00 committed by Otto Winter
parent 38dfab11b4
commit 1a763ae974
5 changed files with 58 additions and 30 deletions

View file

@ -4,6 +4,9 @@ FROM ${BUILD_FROM}
COPY . . COPY . .
RUN pip2 install --no-cache-dir -e . RUN pip2 install --no-cache-dir -e .
ENV USERNAME=""
ENV PASSWORD=""
WORKDIR /config WORKDIR /config
ENTRYPOINT ["esphome"] ENTRYPOINT ["esphome"]
CMD ["/config", "dashboard"] CMD ["/config", "dashboard"]

View file

@ -477,7 +477,11 @@ def parse_args(argv):
help="Create a simple web server for a dashboard.") help="Create a simple web server for a dashboard.")
dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.", dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.",
type=int, default=6052) type=int, default=6052)
dashboard.add_argument("--password", help="The optional password to require for all requests.", dashboard.add_argument("--username", help="The optional username to require "
"for authentication.",
type=str, default='')
dashboard.add_argument("--password", help="The optional password to require "
"for authentication.",
type=str, default='') type=str, default='')
dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.", dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.",
action='store_true') action='store_true')

View file

@ -46,19 +46,22 @@ class DashboardSettings(object):
def __init__(self): def __init__(self):
self.config_dir = '' self.config_dir = ''
self.password_digest = '' self.password_digest = ''
self.username = ''
self.using_password = False self.using_password = False
self.on_hassio = False self.on_hassio = False
self.cookie_secret = None self.cookie_secret = None
def parse_args(self, args): def parse_args(self, args):
self.on_hassio = args.hassio self.on_hassio = args.hassio
password = args.password or os.getenv('PASSWORD', '')
if not self.on_hassio: if not self.on_hassio:
self.using_password = bool(args.password) self.username = args.username or os.getenv('USERNAME', '')
self.using_password = bool(password)
if self.using_password: if self.using_password:
if IS_PY2: if IS_PY2:
self.password_digest = hmac.new(args.password).digest() self.password_digest = hmac.new(password).digest()
else: else:
self.password_digest = hmac.new(args.password.encode()).digest() self.password_digest = hmac.new(password.encode()).digest()
self.config_dir = args.configuration[0] self.config_dir = args.configuration[0]
@property @property
@ -79,7 +82,7 @@ class DashboardSettings(object):
def using_auth(self): def using_auth(self):
return self.using_password or self.using_hassio_auth return self.using_password or self.using_hassio_auth
def check_password(self, password): def check_password(self, username, password):
if not self.using_auth: if not self.using_auth:
return True return True
@ -87,7 +90,7 @@ class DashboardSettings(object):
password = hmac.new(password).digest() password = hmac.new(password).digest()
else: else:
password = hmac.new(password.encode()).digest() password = hmac.new(password.encode()).digest()
return hmac.compare_digest(self.password_digest, password) return username == self.username and hmac.compare_digest(self.password_digest, password)
def rel_path(self, *args): def rel_path(self, *args):
return os.path.join(self.config_dir, *args) return os.path.join(self.config_dir, *args)
@ -585,16 +588,14 @@ PING_REQUEST = threading.Event()
class LoginHandler(BaseHandler): class LoginHandler(BaseHandler):
def get(self): def get(self):
if settings.using_hassio_auth: if is_authenticated(self):
self.render_hassio_login() self.redirect('/')
return else:
self.write('<html><body><form action="./login" method="post">' self.render_login_page()
'Password: <input type="password" name="password">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
def render_hassio_login(self, error=None): def render_login_page(self, error=None):
self.render("templates/login.html", error=error, **template_args()) self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth,
has_username=bool(settings.username), **template_args())
def post_hassio_login(self): def post_hassio_login(self):
import requests import requests
@ -615,20 +616,34 @@ class LoginHandler(BaseHandler):
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Error during Hass.io auth request: %s", err) _LOGGER.warning("Error during Hass.io auth request: %s", err)
self.set_status(500) self.set_status(500)
self.render_hassio_login(error="Internal server error") self.render_login_page(error="Internal server error")
return return
self.set_status(401) self.set_status(401)
self.render_hassio_login(error="Invalid username or password") self.render_login_page(error="Invalid username or password")
def post_native_login(self):
username = str(self.get_argument("username", '').encode('utf-8'))
password = str(self.get_argument("password", '').encode('utf-8'))
if settings.check_password(username, password):
self.set_secure_cookie("authenticated", cookie_authenticated_yes)
self.redirect("/")
return
error_str = "Invalid username or password" if settings.username else "Invalid password"
self.set_status(401)
self.render_login_page(error=error_str)
def post(self): def post(self):
if settings.using_hassio_auth: if settings.using_hassio_auth:
self.post_hassio_login() self.post_hassio_login()
return else:
self.post_native_login()
password = str(self.get_argument("password", ''))
if settings.check_password(password): class LogoutHandler(BaseHandler):
self.set_secure_cookie("authenticated", cookie_authenticated_yes) @authenticated
self.redirect("/") def get(self):
self.clear_cookie("authenticated")
self.redirect('./login')
_STATIC_FILE_HASHES = {} _STATIC_FILE_HASHES = {}
@ -681,6 +696,7 @@ def make_app(debug=False):
app = tornado.web.Application([ app = tornado.web.Application([
(rel + "", MainRequestHandler), (rel + "", MainRequestHandler),
(rel + "login", LoginHandler), (rel + "login", LoginHandler),
(rel + "logout", LogoutHandler),
(rel + "logs", EsphomeLogsHandler), (rel + "logs", EsphomeLogsHandler),
(rel + "upload", EsphomeUploadHandler), (rel + "upload", EsphomeUploadHandler),
(rel + "compile", EsphomeCompileHandler), (rel + "compile", EsphomeCompileHandler),

View file

@ -38,8 +38,9 @@
</div> </div>
<ul id="dropdown-nav-actions" class="select-action dropdown-content card-dropdown-action"> <ul id="dropdown-nav-actions" class="select-action dropdown-content card-dropdown-action">
<li><a id="logout-button" href="{{ relative_url }}logout">Logout</a></li>
<li><a id="update-all-button" data-node="{{ escape(config_dir) }}">Update All</a></li> <li><a id="update-all-button" data-node="{{ escape(config_dir) }}">Update All</a></li>
<li><a id="secrets-button" class="action-edit" data-node="secrets.yaml">Secrets</a></li> <li><a id="secrets-button" class="action-edit" data-node="secrets.yaml">Secrets Editor</a></li>
</ul> </ul>
</nav> </nav>

View file

@ -31,19 +31,23 @@
<form action="./login" method="post"> <form action="./login" method="post">
<div class="card-content"> <div class="card-content">
<span class="card-title">Enter credentials</span> <span class="card-title">Enter credentials</span>
{% if hassio %}
<p> <p>
Please login using your Home Assistant credentials. Please login using your Home Assistant credentials.
</p> </p>
{% end %}
{% if error is not None %} {% if error is not None %}
<p class="error"> <p class="error">
{{ escape(error) }} {{ escape(error) }}
</p> </p>
{% end %} {% end %}
<div class="row"> <div class="row">
{% if has_username or hassio %}
<div class="input-field col s12"> <div class="input-field col s12">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" class="validate" name="username" id="username" /> <input type="text" class="validate" name="username" id="username" />
</div> </div>
{% end %}
<div class="input-field col s12"> <div class="input-field col s12">
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" class="validate" name="password" id="password" /> <input type="password" class="validate" name="password" id="password" />