mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-01-06 15:07:52 +08:00
106 lines
3.8 KiB
Elixir
106 lines
3.8 KiB
Elixir
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
|