mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-01-27 01:08:29 +08:00
Unify env variables and change auth to use session (#195)
This commit is contained in:
parent
3a19021983
commit
57047f9c7f
15 changed files with 210 additions and 218 deletions
61
README.md
61
README.md
|
@ -2,41 +2,25 @@
|
|||
|
||||
Livebook is a web application for writing interactive and collaborative code notebooks. It features:
|
||||
|
||||
* A deployable web app built with [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view)
|
||||
where users can create, fork, and run multiple notebooks.
|
||||
* A deployable web app built with [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view) where users can create, fork, and run multiple notebooks.
|
||||
|
||||
* Each notebook is made of multiple sections: each section is made of Markdown and Elixir
|
||||
cells. Code in Elixir cells can be evaluated on demand. Mathematical formulas are also
|
||||
supported via [KaTeX](https://katex.org/).
|
||||
* Each notebook is made of multiple sections: each section is made of Markdown and Elixir cells. Code in Elixir cells can be evaluated on demand. Mathematical formulas are also supported via [KaTeX](https://katex.org/).
|
||||
|
||||
* Persistence: notebooks can be persisted to disk through the `.livemd` format, which is a
|
||||
subset of Markdown. This means your notebooks can be saved for later, easily shared, and
|
||||
they also play well with version control.
|
||||
* Persistence: notebooks can be persisted to disk through the `.livemd` format, which is a subset of Markdown. This means your notebooks can be saved for later, easily shared, and they also play well with version control.
|
||||
|
||||
* Sequential evaluation: code cells run in a specific order, guaranteeing future users of
|
||||
the same Livebook see the same output. If you re-execute a previous cell, following cells
|
||||
are marked as stale to make it clear they depend on outdated notebook state.
|
||||
* Sequential evaluation: code cells run in a specific order, guaranteeing future users of the same Livebook see the same output. If you re-execute a previous cell, following cells are marked as stale to make it clear they depend on outdated notebook state.
|
||||
|
||||
* Custom runtimes: when executing Elixir code, you can either start a fresh Elixir process,
|
||||
connect to an existing node, or run it inside an existing Elixir project, with access to
|
||||
all of its modules and dependencies. This means Livebook can be a great tool to provide
|
||||
live documentation for existing projects.
|
||||
* Custom runtimes: when executing Elixir code, you can either start a fresh Elixir process, connect to an existing node, or run it inside an existing Elixir project, with access to all of its modules and dependencies. This means Livebook can be a great tool to provide live documentation for existing projects.
|
||||
|
||||
* Explicit dependencies: if your notebook has dependencies, they are explicitly listed and
|
||||
installed with the help of the `Mix.install/2` command in Elixir v1.12+.
|
||||
* Explicit dependencies: if your notebook has dependencies, they are explicitly listed and installed with the help of the `Mix.install/2` command in Elixir v1.12+.
|
||||
|
||||
* Collaborative features allow multiple users to work on the same notebook at once.
|
||||
Collaboration works either in single-node or multi-node deployments - without a
|
||||
need for additional tooling.
|
||||
* Collaborative features allow multiple users to work on the same notebook at once. Collaboration works either in single-node or multi-node deployments - without a need for additional tooling.
|
||||
|
||||
There is a [screencast by José Valim showing some of Livebook features](https://www.youtube.com/watch?v=RKvqc-UEe34).
|
||||
Otherwise, here is a peek at the "Welcome to Livebook" introductory notebook:
|
||||
There is a [screencast by José Valim showing some of Livebook features](https://www.youtube.com/watch?v=RKvqc-UEe34). Otherwise, here is a peek at the "Welcome to Livebook" introductory notebook:
|
||||
|
||||
![Screenshot](https://user-images.githubusercontent.com/9582/113567534-166f4980-960f-11eb-98df-c0b8b81f8a27.png)
|
||||
|
||||
The current version provides only the initial step of our Livebook vision. Our plan
|
||||
is to continue focusing on visual, collaborative, and interactive features in the
|
||||
upcoming releases.
|
||||
The current version provides only the initial step of our Livebook vision. Our plan is to continue focusing on visual, collaborative, and interactive features in the upcoming releases.
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -49,21 +33,26 @@ For now, the best way to run Livebook is by cloning it and running it locally:
|
|||
|
||||
You will need [Elixir v1.11](https://elixir-lang.org/install.html) or later.
|
||||
|
||||
We will work on other distribution modes (escripts, Docker images, etc) once
|
||||
we start distributing official releases.
|
||||
We will work on other distribution modes (escripts, Docker images, etc) once we start distributing official releases.
|
||||
|
||||
## Security considerations
|
||||
### Security considerations
|
||||
|
||||
Livebook is built to document and execute code. Anyone with access to a
|
||||
Livebook instance will be able to access any file and execute any code
|
||||
in the machine Livebook is running.
|
||||
Livebook is built to document and execute code. Anyone with access to a Livebook instance will be able to access any file and execute any code in the machine Livebook is running.
|
||||
|
||||
For this reason, `Livebook` only binds to the 127.0.0.1, allowing access
|
||||
to happen only within the current machine. When running `Livebook` in the
|
||||
production environment - the recommended environment - we also generate a
|
||||
token on initialization and we only allow access to the Livebook if said
|
||||
token is supplied as part of the URL.
|
||||
For this reason, `Livebook` only binds to the 127.0.0.1, allowing access to happen only within the current machine. When running `Livebook` in the production environment - the recommended environment - we also generate a token on initialization and we only allow access to the Livebook if said token is supplied as part of the URL.
|
||||
|
||||
### Environment variables
|
||||
<!-- Environment variables -->
|
||||
|
||||
The following environment variables configure Livebook:
|
||||
|
||||
* LIVEBOOK_PASSWORD - sets a password that must be used to access Livebook. Must be at least 12 characters. Defaults to token authentication.
|
||||
|
||||
* LIVEBOOK_PORT - sets the port Livebook runs on. If you want multiple instances to run on the same domain but different ports, you also need to set `LIVEBOOK_SECRET_KEY_BASE`. Defaults to 8080.
|
||||
|
||||
* LIVEBOOK_SECRET_KEY_BASE - sets a secret key that is used to sign and encrypt the session and other payloads used by Livebook. Must be at least 64 characters long and it can be generated by commands such as: `openssl rand -base64 48`. Defaults to a random secret on every boot.
|
||||
|
||||
<!-- Environment variables -->
|
||||
## License
|
||||
|
||||
Copyright (C) 2021 Dashbit
|
||||
|
|
|
@ -3,7 +3,6 @@ import Config
|
|||
# Configures the endpoint
|
||||
config :livebook, LivebookWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "9hHHeOiAA8wrivUfuS//jQMurHxoMYUtF788BQMx2KO7mYUE8rVrGGG09djBNQq7",
|
||||
pubsub_server: Livebook.PubSub,
|
||||
live_view: [signing_salt: "livebook"]
|
||||
|
||||
|
@ -15,7 +14,8 @@ config :logger, :console,
|
|||
# Use Jason for JSON parsing in Phoenix
|
||||
config :phoenix, :json_library, Jason
|
||||
|
||||
config :livebook, :token_authentication, true
|
||||
# Sets the default authentication mode to token
|
||||
config :livebook, :authentication_mode, :token
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
|
|
|
@ -66,4 +66,5 @@ config :phoenix, :stacktrace_depth, 20
|
|||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
config :livebook, authentication_mode: :disabled
|
||||
# Disable authentication mode during dev
|
||||
config :livebook, :authentication_mode, :disabled
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import Config
|
||||
|
||||
# For production, don't forget to configure the url host
|
||||
# to something meaningful, Phoenix uses this information
|
||||
# when generating URLs.
|
||||
# Default bind and port for production
|
||||
config :livebook, LivebookWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 8080]
|
||||
|
||||
# The output is shown to the end user,
|
||||
# so limit the amount of information we show.
|
||||
config :logger, level: :info
|
||||
|
||||
config :livebook, authentication_mode: :token
|
||||
# Start log-level in notice by default to reduce output
|
||||
config :logger, level: :notice
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
|
|
|
@ -1,25 +1,17 @@
|
|||
import Config
|
||||
require Logger
|
||||
|
||||
# Configure the type of names used for distribution and the node name.
|
||||
# By default a random short name is used.
|
||||
# config :livebook, :node, {:shortnames, "livebook"}
|
||||
# config :livebook, :node, {:longnames, :"livebook@127.0.0.1"}
|
||||
config :livebook, LivebookWeb.Endpoint,
|
||||
secret_key_base:
|
||||
Livebook.Config.secret!("LIVEBOOK_SECRET_KEY_BASE") ||
|
||||
Base.encode64(:crypto.strong_rand_bytes(48))
|
||||
|
||||
if password = System.get_env("LIVEBOOK_PASSWORD") do
|
||||
config :livebook,
|
||||
authentication_mode: :password,
|
||||
password: password
|
||||
if password = Livebook.Config.password!("LIVEBOOK_PASSWORD") do
|
||||
config :livebook, authentication_mode: :password, password: password
|
||||
else
|
||||
config :livebook, token: Livebook.Utils.random_id()
|
||||
end
|
||||
|
||||
if config_env() == :prod do
|
||||
# In order to persist sessions between deployments (desirable when using password authentication mode)
|
||||
# allow to customize secret_key_base. Otherwise the secret will change every time app starts.
|
||||
secret_key_base =
|
||||
if secret = System.get_env("SECRET_KEY_BASE") do
|
||||
secret
|
||||
else
|
||||
:crypto.strong_rand_bytes(48) |> Base.encode64()
|
||||
end
|
||||
|
||||
config :livebook, LivebookWeb.Endpoint, secret_key_base: secret_key_base
|
||||
if port = Livebook.Config.port!("LIVEBOOK_PORT") do
|
||||
config :livebook, LivebookWeb.Endpoint, http: [port: port]
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ config :livebook, LivebookWeb.Endpoint,
|
|||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
# Disable authentication mode during test
|
||||
config :livebook, :authentication_mode, :disabled
|
||||
|
||||
# Use longnames when running tests in CI, so that no host resolution is required,
|
||||
|
|
|
@ -7,7 +7,6 @@ defmodule Livebook.Application do
|
|||
|
||||
def start(_type, _args) do
|
||||
ensure_distribution!()
|
||||
initialize_token()
|
||||
|
||||
children = [
|
||||
# Start the Telemetry supervisor
|
||||
|
@ -44,7 +43,7 @@ defmodule Livebook.Application do
|
|||
:ok
|
||||
|
||||
_ ->
|
||||
abort!("""
|
||||
Livebook.Config.abort!("""
|
||||
could not start epmd (Erlang Port Mapper Driver). Livebook uses epmd to \
|
||||
talk to different runtimes. You may have to start epmd explicitly by calling:
|
||||
|
||||
|
@ -61,17 +60,15 @@ defmodule Livebook.Application do
|
|||
{type, name} = get_node_type_and_name()
|
||||
|
||||
case Node.start(name, type) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, reason} -> abort!("could not start distributed node: #{inspect(reason)}")
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Livebook.Config.abort!("could not start distributed node: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp abort!(message) do
|
||||
IO.puts("\nERROR!!! [Livebook] " <> message)
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
defp get_node_type_and_name() do
|
||||
Application.get_env(:livebook, :node) || {:shortnames, random_short_name()}
|
||||
end
|
||||
|
@ -80,19 +77,9 @@ defmodule Livebook.Application do
|
|||
:"livebook_#{Livebook.Utils.random_short_id()}"
|
||||
end
|
||||
|
||||
# Generates and configures random token if token auth is enabled
|
||||
defp initialize_token() do
|
||||
token_auth? = Application.fetch_env!(:livebook, :token_authentication)
|
||||
|
||||
if token_auth? do
|
||||
token = Livebook.Utils.random_id()
|
||||
Application.put_env(:livebook, :token, token)
|
||||
end
|
||||
end
|
||||
|
||||
defp display_startup_info() do
|
||||
if Phoenix.Endpoint.server?(:livebook, LivebookWeb.Endpoint) do
|
||||
IO.ANSI.format([:blue, "Livebook running at #{access_url()}"]) |> IO.puts()
|
||||
IO.puts("[Livebook] Application running at #{access_url()}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule Livebook.Config do
|
||||
@moduledoc false
|
||||
|
||||
@type auth_mode() :: :token | :password | :disabled
|
||||
|
||||
@doc """
|
||||
Checks if the distribution mode is configured to use short names.
|
||||
"""
|
||||
|
@ -13,6 +15,14 @@ defmodule Livebook.Config do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the authentication mode.
|
||||
"""
|
||||
@spec auth_mode() :: auth_mode()
|
||||
def auth_mode() do
|
||||
Application.fetch_env!(:livebook, :authentication_mode)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return the root path for persisting notebooks.
|
||||
"""
|
||||
|
@ -20,4 +30,55 @@ defmodule Livebook.Config do
|
|||
def root_path() do
|
||||
Application.get_env(:livebook, :root_path, File.cwd!())
|
||||
end
|
||||
|
||||
## Parsing
|
||||
|
||||
@doc """
|
||||
Parses a long secret.
|
||||
"""
|
||||
def secret!(env) do
|
||||
if secret_key_base = System.get_env(env) do
|
||||
if byte_size(secret_key_base) < 64 do
|
||||
abort!(
|
||||
"cannot start Livebook because #{env} must be at least 64 characters. " <>
|
||||
"Invoke `openssl rand -base64 48` to generate an appropriately long secret."
|
||||
)
|
||||
end
|
||||
|
||||
secret_key_base
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a port.
|
||||
"""
|
||||
def port!(env) do
|
||||
if port = System.get_env(env) do
|
||||
case Integer.parse(port) do
|
||||
{port, ""} -> port
|
||||
:error -> abort!("expected #{env} to be an integer, got: #{inspect(port)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a password.
|
||||
"""
|
||||
def password!(env) do
|
||||
if password = System.get_env(env) do
|
||||
if byte_size(password) < 12 do
|
||||
abort!("cannot start Livebook because #{env} must be at least 12 characters")
|
||||
end
|
||||
|
||||
password
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Aborts booting due to a configuration error.
|
||||
"""
|
||||
def abort!(message) do
|
||||
IO.puts("\nERROR!!! [Livebook] " <> message)
|
||||
System.halt(1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,15 @@ defmodule LivebookCLI.Server do
|
|||
@moduledoc false
|
||||
|
||||
@behaviour LivebookCLI.Task
|
||||
@external_resource "README.md"
|
||||
|
||||
[_, environment_variables, _] =
|
||||
"README.md"
|
||||
|> File.read!()
|
||||
|> String.split("<!-- Environment variables -->")
|
||||
|
||||
@external_resource "README.md"
|
||||
@environment_variables String.trim(environment_variables)
|
||||
|
||||
@impl true
|
||||
def usage() do
|
||||
|
@ -10,12 +19,18 @@ defmodule LivebookCLI.Server do
|
|||
|
||||
Available options:
|
||||
|
||||
-p, --port The port to start the web application on, defaults to 8080
|
||||
--no-token Disable token authentication, enabled by default
|
||||
--sname Set a short name for the app distributed node
|
||||
--name Set a name for the app distributed node
|
||||
--no-token Disable token authentication, enabled by default
|
||||
If LIVEBOOK_PASSWORD is set, it takes precedence over token auth
|
||||
--sname Set a short name for the app distributed node
|
||||
-p, --port The port to start the web application on, defaults to 8080
|
||||
|
||||
The --help option can be given to print this notice.
|
||||
|
||||
## Environment variables
|
||||
|
||||
#{@environment_variables}
|
||||
|
||||
The --help option can be given for usage information.
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -74,8 +89,12 @@ defmodule LivebookCLI.Server do
|
|||
|
||||
defp opts_to_config([], config), do: config
|
||||
|
||||
defp opts_to_config([{:token, token_auth?} | opts], config) do
|
||||
opts_to_config(opts, [{:livebook, :token_authentication, token_auth?} | config])
|
||||
defp opts_to_config([{:token, false} | opts], config) do
|
||||
if Livebook.Config.auth_mode() == :token do
|
||||
opts_to_config(opts, [{:livebook, :authentication_mode, :disabled} | config])
|
||||
else
|
||||
opts_to_config(opts, config)
|
||||
end
|
||||
end
|
||||
|
||||
defp opts_to_config([{:port, port} | opts], config) do
|
||||
|
|
|
@ -1,36 +1,37 @@
|
|||
defmodule LivebookWeb.AuthController do
|
||||
use LivebookWeb, :controller
|
||||
|
||||
alias LivebookWeb.Helpers
|
||||
plug :require_unauthenticated_password
|
||||
|
||||
def index(conn, _assigns) do
|
||||
conn
|
||||
|> set_authenticated()
|
||||
|> ensure_authenticated()
|
||||
alias LivebookWeb.AuthPlug
|
||||
|
||||
defp require_unauthenticated_password(conn, _opts) do
|
||||
if Livebook.Config.auth_mode() != :password or AuthPlug.authenticated?(conn, :password) do
|
||||
redirect_home(conn)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp set_authenticated(conn) do
|
||||
conn
|
||||
|> assign(:authenticated, LivebookWeb.AuthPlug.authenticated?(conn))
|
||||
end
|
||||
|
||||
defp ensure_authenticated(%Plug.Conn{assigns: %{authenticated: true}} = conn) do
|
||||
conn
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
defp ensure_authenticated(conn) do
|
||||
def index(conn, _params) do
|
||||
conn
|
||||
|> put_view(LivebookWeb.ErrorView)
|
||||
|> render("401.html")
|
||||
end
|
||||
|
||||
def authenticate(conn, %{"password" => password}) do
|
||||
password = :crypto.hash(:sha256, password) |> Base.encode16()
|
||||
cookie_key = Helpers.auth_cookie_key(conn, :password)
|
||||
conn = AuthPlug.store(conn, :password, password)
|
||||
|
||||
if AuthPlug.authenticated?(conn, :password) do
|
||||
redirect_home(conn)
|
||||
else
|
||||
index(conn, %{})
|
||||
end
|
||||
end
|
||||
|
||||
defp redirect_home(conn) do
|
||||
conn
|
||||
|> put_resp_cookie(cookie_key, password, Helpers.auth_cookie_opts())
|
||||
|> redirect(to: "/")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,24 +68,4 @@ defmodule LivebookWeb.Helpers do
|
|||
|> String.split("\n")
|
||||
|> Enum.map(&Phoenix.HTML.raw/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns cookie key based on connection port and authentication type.
|
||||
|
||||
The user may run multiple Livebook instances on the same host
|
||||
on different ports, so the cookie name should be scoped under port.
|
||||
"""
|
||||
@spec auth_cookie_key(Plug.Conn.t(), :token | :password) :: String.t()
|
||||
def auth_cookie_key(conn, type) do
|
||||
"#{conn.port}#{inspect(type)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns cookie options that should be used to sign authentication cookies.
|
||||
"""
|
||||
@spec auth_cookie_opts() :: Keyword.t()
|
||||
def auth_cookie_opts() do
|
||||
# max_age is set to 30 days in seconds
|
||||
[sign: true, max_age: 2_592_000]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,8 +7,6 @@ defmodule LivebookWeb.AuthPlug do
|
|||
|
||||
@behaviour Plug
|
||||
|
||||
alias LivebookWeb.Helpers
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
|
||||
|
@ -17,79 +15,58 @@ defmodule LivebookWeb.AuthPlug do
|
|||
|
||||
@impl true
|
||||
def call(conn, _opts) do
|
||||
case auth_mode() do
|
||||
:password ->
|
||||
password_authentication(conn)
|
||||
mode = Livebook.Config.auth_mode()
|
||||
|
||||
:token ->
|
||||
token_authentication(conn)
|
||||
|
||||
:disabled ->
|
||||
conn
|
||||
if authenticated?(conn, mode) do
|
||||
conn
|
||||
else
|
||||
authenticate(conn, mode)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores in the session the secret for the given mode.
|
||||
"""
|
||||
def store(conn, mode, value) do
|
||||
put_session(conn, key(conn, mode), hash(value))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if given connection is already authenticated.
|
||||
"""
|
||||
@spec authenticated?(Plug.Conn.t()) :: boolean()
|
||||
def authenticated?(conn, mode \\ auth_mode())
|
||||
@spec authenticated?(Plug.Conn.t(), Livebook.Config.auth_mode()) :: boolean()
|
||||
def authenticated?(conn, mode)
|
||||
|
||||
def authenticated?(conn, mode) when mode in [:token, :password] do
|
||||
secret = prepare_secret(mode)
|
||||
|
||||
key = Helpers.auth_cookie_key(conn, mode)
|
||||
conn = fetch_cookies(conn, signed: [key])
|
||||
cookie = conn.cookies[key]
|
||||
|
||||
is_binary(cookie) and Plug.Crypto.secure_compare(cookie, secret)
|
||||
secret = get_session(conn, key(conn, mode))
|
||||
is_binary(secret) and Plug.Crypto.secure_compare(secret, expected(mode))
|
||||
end
|
||||
|
||||
def authenticated?(_conn, _mode) do
|
||||
true
|
||||
end
|
||||
|
||||
defp password_authentication(conn) do
|
||||
if authenticated?(conn) do
|
||||
defp authenticate(conn, :password) do
|
||||
conn
|
||||
|> redirect(to: "/authenticate")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp authenticate(conn, :token) do
|
||||
token = Map.get(conn.query_params, "token")
|
||||
|
||||
if is_binary(token) and Plug.Crypto.secure_compare(hash(token), expected(:token)) do
|
||||
# Redirect to the same path without query params
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> redirect(to: "/authenticate")
|
||||
|> store(:token, token)
|
||||
|> redirect(to: conn.request_path)
|
||||
|> halt()
|
||||
else
|
||||
raise LivebookWeb.InvalidTokenError
|
||||
end
|
||||
end
|
||||
|
||||
defp token_authentication(conn) do
|
||||
token = prepare_secret(:token)
|
||||
cookie_key = Helpers.auth_cookie_key(conn, :token)
|
||||
token_param = Map.get(conn.query_params, "token")
|
||||
|
||||
cond do
|
||||
is_binary(token_param) and Plug.Crypto.secure_compare(token_param, token) ->
|
||||
conn
|
||||
|> put_resp_cookie(cookie_key, token_param, Helpers.auth_cookie_opts())
|
||||
# Redirect to the same path without query params
|
||||
|> redirect(to: conn.request_path)
|
||||
|> halt()
|
||||
|
||||
authenticated?(conn) ->
|
||||
conn
|
||||
|
||||
true ->
|
||||
raise LivebookWeb.InvalidTokenError
|
||||
end
|
||||
end
|
||||
|
||||
defp auth_mode() do
|
||||
Application.fetch_env!(:livebook, :authentication_mode)
|
||||
end
|
||||
|
||||
defp prepare_secret(mode) do
|
||||
secret = Application.fetch_env!(:livebook, mode)
|
||||
|
||||
case mode do
|
||||
:token -> secret
|
||||
:password -> :crypto.hash(:sha256, secret) |> Base.encode16()
|
||||
end
|
||||
end
|
||||
defp key(conn, mode), do: "#{conn.port}:#{mode}"
|
||||
defp expected(mode), do: hash(Application.fetch_env!(:livebook, mode))
|
||||
defp hash(value), do: :crypto.hash(:sha256, value)
|
||||
end
|
||||
|
|
|
@ -16,8 +16,7 @@ defmodule LivebookWeb.Router do
|
|||
end
|
||||
|
||||
scope "/", LivebookWeb do
|
||||
pipe_through :browser
|
||||
pipe_through :auth
|
||||
pipe_through [:browser, :auth]
|
||||
|
||||
live "/", HomeLive, :page
|
||||
live "/home/sessions/:session_id/close", HomeLive, :close_session
|
||||
|
|
9
mix.exs
9
mix.exs
|
@ -11,8 +11,7 @@ defmodule Livebook.MixProject do
|
|||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
escript: escript(),
|
||||
preferred_cli_env: preferred_cli_env()
|
||||
escript: escript()
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -58,10 +57,4 @@ defmodule Livebook.MixProject do
|
|||
app: nil
|
||||
]
|
||||
end
|
||||
|
||||
defp preferred_cli_env() do
|
||||
[
|
||||
build: :prod
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -59,33 +59,31 @@ defmodule LivebookWeb.AuthPlugTest do
|
|||
end
|
||||
|
||||
@tag token: "grumpycat"
|
||||
test "persists authentication across requests using cookies", %{conn: conn} do
|
||||
test "persists authentication across requests", %{conn: conn} do
|
||||
conn = get(conn, "/?token=grumpycat")
|
||||
assert get_session(conn, "80:token")
|
||||
|
||||
assert Map.has_key?(conn.resp_cookies, "80:token")
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> Plug.Test.recycle_cookies(conn)
|
||||
|> get("/")
|
||||
|
||||
conn = get(conn, "/")
|
||||
assert conn.status == 200
|
||||
assert conn.resp_body =~ "New notebook"
|
||||
end
|
||||
end
|
||||
|
||||
describe "password authentication" do
|
||||
@tag password: "grumpycat"
|
||||
test "redirects to '/authenticate' if not already authenticated", %{conn: conn} do
|
||||
conn = get(conn, "/")
|
||||
test "redirects to '/' if no authentication is required", %{conn: conn} do
|
||||
conn = get(conn, "/authenticate")
|
||||
assert redirected_to(conn) == "/"
|
||||
end
|
||||
|
||||
@tag password: "grumpycat"
|
||||
test "redirects to '/authenticate' if not authenticated", %{conn: conn} do
|
||||
conn = get(conn, "/")
|
||||
assert redirected_to(conn) == "/authenticate"
|
||||
end
|
||||
|
||||
@tag password: "grumpycat"
|
||||
test "redirects to '/' on valid authentication", %{conn: conn} do
|
||||
conn = post(conn, Routes.auth_path(conn, :authenticate), password: "grumpycat")
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
|
||||
conn = get(conn, "/")
|
||||
|
@ -95,24 +93,23 @@ defmodule LivebookWeb.AuthPlugTest do
|
|||
@tag password: "grumpycat"
|
||||
test "redirects back to '/authenticate' on invalid password", %{conn: conn} do
|
||||
conn = post(conn, Routes.auth_path(conn, :authenticate), password: "invalid password")
|
||||
assert html_response(conn, 200) =~ "Authentication required"
|
||||
|
||||
conn = get(conn, "/")
|
||||
assert redirected_to(conn) == "/authenticate"
|
||||
end
|
||||
|
||||
@tag password: "grumpycat"
|
||||
test "persists authentication across requests using cookies", %{conn: conn} do
|
||||
test "persists authentication across requests", %{conn: conn} do
|
||||
conn = post(conn, Routes.auth_path(conn, :authenticate), password: "grumpycat")
|
||||
assert get_session(conn, "80:password")
|
||||
|
||||
assert Map.has_key?(conn.resp_cookies, "80:password")
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> Plug.Test.recycle_cookies(conn)
|
||||
|> get("/")
|
||||
|
||||
conn = get(conn, "/")
|
||||
assert conn.status == 200
|
||||
assert conn.resp_body =~ "New notebook"
|
||||
|
||||
conn = get(conn, "/authenticate")
|
||||
assert redirected_to(conn) == "/"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue