Packages
livebook
0.14.0-rc.1
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/hooks/app_auth_hook.ex
defmodule LivebookWeb.AppAuthHook do
import Phoenix.Component
import Phoenix.LiveView
use LivebookWeb, :verified_routes
# For apps with password, we want to store the hashed password
# (let's call it token) in the session, as we do for the main auth.
# However, the session uses cookies, which have a ~4kb size limit.
# We could use multiple cookies, however there are also limits on
# the number of cookies, and we don't want the browser to clear
# them all at some point. Additionally, accumulating cookies for
# all apps would imply larger payloads on every regular request.
#
# Since we don't have any persistence on the server side, the other
# option is to use the browser local storage and manage it through
# JavaScript. Therefore, the whole auth is built using LiveView.
# The flows are:
#
# * unauthenticated - the auth LiveView shows a regular form and
# validates the user-provided password. Once the password is
# correct, it pushes an event to the client to store the token.
# Once the token is stored it redirects to the app page.
#
# * authenticated - on dead render the app LiveView renders just
# a loading screen. On the client side, provided it's the app
# page, we read the token from local storage (if stored) and
# send it in mount connect params via the socket. Then on the
# server we use that token to authenticate.
#
# This module defines a hook that sets the following assigns:
#
# * `:app_authenticated?` - reflects the current authentication.
# For public apps (or in case the user has full access) it is
# set to `true` on both dead and live render
#
# * `:livebook_authenticated?` - if the user has full Livebook
# access
#
# * `:app_settings` - the current app settings
#
def on_mount(:default, %{"slug" => slug}, session, socket) do
livebook_authenticated? = livebook_authenticated?(session, socket)
socket = assign(socket, livebook_authenticated?: livebook_authenticated?)
case Livebook.Apps.fetch_settings(slug) do
{:ok, %{access_type: :public} = app_settings} ->
{:cont, assign(socket, app_authenticated?: true, app_settings: app_settings)}
{:ok, %{access_type: :protected} = app_settings} ->
app_authenticated? = livebook_authenticated? or has_valid_token?(socket, app_settings)
{:cont,
assign(socket, app_authenticated?: app_authenticated?, app_settings: app_settings)}
:error ->
{:halt, redirect(socket, to: ~p"/")}
end
end
# Skip auth for non-app-specific routes
def on_mount(:default, %{}, _session, socket) do
{:cont, socket}
end
defp livebook_authenticated?(session, socket) do
uri = get_connect_info(socket, :uri)
LivebookWeb.AuthPlug.authenticated?(session, uri.port)
end
defp has_valid_token?(socket, app_settings) do
connect_params = get_connect_params(socket) || %{}
if token = connect_params["app_auth_token"] do
valid_auth_token?(token, app_settings)
else
false
end
end
@doc """
Generates auth token that can be sent to the client.
"""
@spec get_auth_token(Livebook.Notebook.AppSettings.t()) :: String.t()
def get_auth_token(app_settings) do
:crypto.hash(:sha256, app_settings.password) |> Base.encode64()
end
@doc """
Checks the given token is valid.
"""
@spec valid_auth_token?(String.t(), Livebook.Notebook.AppSettings.t()) :: boolean()
def valid_auth_token?(token, app_settings) do
Plug.Crypto.secure_compare(token, get_auth_token(app_settings))
end
@doc """
Checks if the given password is valid.
"""
@spec valid_password?(String.t(), Livebook.Notebook.AppSettings.t()) :: boolean()
def valid_password?(password, app_settings) do
Plug.Crypto.secure_compare(password, app_settings.password)
end
end