Unify env variables and change auth to use session (#195)

This commit is contained in:
José Valim 2021-04-15 15:50:29 +02:00 committed by GitHub
parent 3a19021983
commit 57047f9c7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 210 additions and 218 deletions

View file

@ -2,41 +2,25 @@
Livebook is a web application for writing interactive and collaborative code notebooks. It features: 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) * A deployable web app built with [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view) where users can create, fork, and run multiple notebooks.
where users can create, fork, and run multiple notebooks.
* Each notebook is made of multiple sections: each section is made of Markdown and Elixir * 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/).
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 * 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.
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 * 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.
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, * 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.
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 * 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+.
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. * 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.
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). 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:
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) ![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 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.
is to continue focusing on visual, collaborative, and interactive features in the
upcoming releases.
## Usage ## 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. 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 will work on other distribution modes (escripts, Docker images, etc) once we start distributing official releases.
we start distributing official releases.
## Security considerations ### Security considerations
Livebook is built to document and execute code. Anyone with access to a 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 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 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.
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 ## License
Copyright (C) 2021 Dashbit Copyright (C) 2021 Dashbit

View file

@ -3,7 +3,6 @@ import Config
# Configures the endpoint # Configures the endpoint
config :livebook, LivebookWeb.Endpoint, config :livebook, LivebookWeb.Endpoint,
url: [host: "localhost"], url: [host: "localhost"],
secret_key_base: "9hHHeOiAA8wrivUfuS//jQMurHxoMYUtF788BQMx2KO7mYUE8rVrGGG09djBNQq7",
pubsub_server: Livebook.PubSub, pubsub_server: Livebook.PubSub,
live_view: [signing_salt: "livebook"] live_view: [signing_salt: "livebook"]
@ -15,7 +14,8 @@ config :logger, :console,
# Use Jason for JSON parsing in Phoenix # Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason 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 # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.

View file

@ -66,4 +66,5 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation # Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime
config :livebook, authentication_mode: :disabled # Disable authentication mode during dev
config :livebook, :authentication_mode, :disabled

View file

@ -1,15 +1,10 @@
import Config import Config
# For production, don't forget to configure the url host # Default bind and port for production
# to something meaningful, Phoenix uses this information
# when generating URLs.
config :livebook, LivebookWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 8080] config :livebook, LivebookWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 8080]
# The output is shown to the end user, # Start log-level in notice by default to reduce output
# so limit the amount of information we show. config :logger, level: :notice
config :logger, level: :info
config :livebook, authentication_mode: :token
# ## SSL Support # ## SSL Support
# #

View file

@ -1,25 +1,17 @@
import Config import Config
require Logger
# Configure the type of names used for distribution and the node name. config :livebook, LivebookWeb.Endpoint,
# By default a random short name is used. secret_key_base:
# config :livebook, :node, {:shortnames, "livebook"} Livebook.Config.secret!("LIVEBOOK_SECRET_KEY_BASE") ||
# config :livebook, :node, {:longnames, :"livebook@127.0.0.1"} Base.encode64(:crypto.strong_rand_bytes(48))
if password = System.get_env("LIVEBOOK_PASSWORD") do if password = Livebook.Config.password!("LIVEBOOK_PASSWORD") do
config :livebook, config :livebook, authentication_mode: :password, password: password
authentication_mode: :password, else
password: password config :livebook, token: Livebook.Utils.random_id()
end end
if config_env() == :prod do if port = Livebook.Config.port!("LIVEBOOK_PORT") do
# In order to persist sessions between deployments (desirable when using password authentication mode) config :livebook, LivebookWeb.Endpoint, http: [port: port]
# 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
end end

View file

@ -9,6 +9,7 @@ config :livebook, LivebookWeb.Endpoint,
# Print only warnings and errors during test # Print only warnings and errors during test
config :logger, level: :warn config :logger, level: :warn
# Disable authentication mode during test
config :livebook, :authentication_mode, :disabled config :livebook, :authentication_mode, :disabled
# Use longnames when running tests in CI, so that no host resolution is required, # Use longnames when running tests in CI, so that no host resolution is required,

View file

@ -7,7 +7,6 @@ defmodule Livebook.Application do
def start(_type, _args) do def start(_type, _args) do
ensure_distribution!() ensure_distribution!()
initialize_token()
children = [ children = [
# Start the Telemetry supervisor # Start the Telemetry supervisor
@ -44,7 +43,7 @@ defmodule Livebook.Application do
:ok :ok
_ -> _ ->
abort!(""" Livebook.Config.abort!("""
could not start epmd (Erlang Port Mapper Driver). Livebook uses epmd to \ 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: 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() {type, name} = get_node_type_and_name()
case Node.start(name, type) do case Node.start(name, type) do
{:ok, _} -> :ok {:ok, _} ->
{:error, reason} -> abort!("could not start distributed node: #{inspect(reason)}") :ok
{:error, reason} ->
Livebook.Config.abort!("could not start distributed node: #{inspect(reason)}")
end end
end end
end end
defp abort!(message) do
IO.puts("\nERROR!!! [Livebook] " <> message)
System.halt(1)
end
defp get_node_type_and_name() do defp get_node_type_and_name() do
Application.get_env(:livebook, :node) || {:shortnames, random_short_name()} Application.get_env(:livebook, :node) || {:shortnames, random_short_name()}
end end
@ -80,19 +77,9 @@ defmodule Livebook.Application do
:"livebook_#{Livebook.Utils.random_short_id()}" :"livebook_#{Livebook.Utils.random_short_id()}"
end 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 defp display_startup_info() do
if Phoenix.Endpoint.server?(:livebook, LivebookWeb.Endpoint) 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
end end

View file

@ -1,6 +1,8 @@
defmodule Livebook.Config do defmodule Livebook.Config do
@moduledoc false @moduledoc false
@type auth_mode() :: :token | :password | :disabled
@doc """ @doc """
Checks if the distribution mode is configured to use short names. Checks if the distribution mode is configured to use short names.
""" """
@ -13,6 +15,14 @@ defmodule Livebook.Config do
end end
end end
@doc """
Returns the authentication mode.
"""
@spec auth_mode() :: auth_mode()
def auth_mode() do
Application.fetch_env!(:livebook, :authentication_mode)
end
@doc """ @doc """
Return the root path for persisting notebooks. Return the root path for persisting notebooks.
""" """
@ -20,4 +30,55 @@ defmodule Livebook.Config do
def root_path() do def root_path() do
Application.get_env(:livebook, :root_path, File.cwd!()) Application.get_env(:livebook, :root_path, File.cwd!())
end 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 end

View file

@ -2,6 +2,15 @@ defmodule LivebookCLI.Server do
@moduledoc false @moduledoc false
@behaviour LivebookCLI.Task @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 @impl true
def usage() do def usage() do
@ -10,12 +19,18 @@ defmodule LivebookCLI.Server do
Available options: 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 --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 end
@ -74,8 +89,12 @@ defmodule LivebookCLI.Server do
defp opts_to_config([], config), do: config defp opts_to_config([], config), do: config
defp opts_to_config([{:token, token_auth?} | opts], config) do defp opts_to_config([{:token, false} | opts], config) do
opts_to_config(opts, [{:livebook, :token_authentication, token_auth?} | config]) if Livebook.Config.auth_mode() == :token do
opts_to_config(opts, [{:livebook, :authentication_mode, :disabled} | config])
else
opts_to_config(opts, config)
end
end end
defp opts_to_config([{:port, port} | opts], config) do defp opts_to_config([{:port, port} | opts], config) do

View file

@ -1,36 +1,37 @@
defmodule LivebookWeb.AuthController do defmodule LivebookWeb.AuthController do
use LivebookWeb, :controller use LivebookWeb, :controller
alias LivebookWeb.Helpers plug :require_unauthenticated_password
def index(conn, _assigns) do alias LivebookWeb.AuthPlug
conn
|> set_authenticated() defp require_unauthenticated_password(conn, _opts) do
|> ensure_authenticated() if Livebook.Config.auth_mode() != :password or AuthPlug.authenticated?(conn, :password) do
redirect_home(conn)
else
conn
end
end end
defp set_authenticated(conn) do def index(conn, _params) 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
conn conn
|> put_view(LivebookWeb.ErrorView) |> put_view(LivebookWeb.ErrorView)
|> render("401.html") |> render("401.html")
end end
def authenticate(conn, %{"password" => password}) do def authenticate(conn, %{"password" => password}) do
password = :crypto.hash(:sha256, password) |> Base.encode16() conn = AuthPlug.store(conn, :password, password)
cookie_key = Helpers.auth_cookie_key(conn, :password)
if AuthPlug.authenticated?(conn, :password) do
redirect_home(conn)
else
index(conn, %{})
end
end
defp redirect_home(conn) do
conn conn
|> put_resp_cookie(cookie_key, password, Helpers.auth_cookie_opts())
|> redirect(to: "/") |> redirect(to: "/")
|> halt()
end end
end end

View file

@ -68,24 +68,4 @@ defmodule LivebookWeb.Helpers do
|> String.split("\n") |> String.split("\n")
|> Enum.map(&Phoenix.HTML.raw/1) |> Enum.map(&Phoenix.HTML.raw/1)
end 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 end

View file

@ -7,8 +7,6 @@ defmodule LivebookWeb.AuthPlug do
@behaviour Plug @behaviour Plug
alias LivebookWeb.Helpers
import Plug.Conn import Plug.Conn
import Phoenix.Controller import Phoenix.Controller
@ -17,79 +15,58 @@ defmodule LivebookWeb.AuthPlug do
@impl true @impl true
def call(conn, _opts) do def call(conn, _opts) do
case auth_mode() do mode = Livebook.Config.auth_mode()
:password ->
password_authentication(conn)
:token -> if authenticated?(conn, mode) do
token_authentication(conn) conn
else
:disabled -> authenticate(conn, mode)
conn
end end
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 """ @doc """
Checks if given connection is already authenticated. Checks if given connection is already authenticated.
""" """
@spec authenticated?(Plug.Conn.t()) :: boolean() @spec authenticated?(Plug.Conn.t(), Livebook.Config.auth_mode()) :: boolean()
def authenticated?(conn, mode \\ auth_mode()) def authenticated?(conn, mode)
def authenticated?(conn, mode) when mode in [:token, :password] do def authenticated?(conn, mode) when mode in [:token, :password] do
secret = prepare_secret(mode) secret = get_session(conn, key(conn, mode))
is_binary(secret) and Plug.Crypto.secure_compare(secret, expected(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)
end end
def authenticated?(_conn, _mode) do def authenticated?(_conn, _mode) do
true true
end end
defp password_authentication(conn) do defp authenticate(conn, :password) do
if authenticated?(conn) 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 conn
else |> store(:token, token)
conn |> redirect(to: conn.request_path)
|> redirect(to: "/authenticate")
|> halt() |> halt()
else
raise LivebookWeb.InvalidTokenError
end end
end end
defp token_authentication(conn) do defp key(conn, mode), do: "#{conn.port}:#{mode}"
token = prepare_secret(:token) defp expected(mode), do: hash(Application.fetch_env!(:livebook, mode))
cookie_key = Helpers.auth_cookie_key(conn, :token) defp hash(value), do: :crypto.hash(:sha256, value)
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
end end

View file

@ -16,8 +16,7 @@ defmodule LivebookWeb.Router do
end end
scope "/", LivebookWeb do scope "/", LivebookWeb do
pipe_through :browser pipe_through [:browser, :auth]
pipe_through :auth
live "/", HomeLive, :page live "/", HomeLive, :page
live "/home/sessions/:session_id/close", HomeLive, :close_session live "/home/sessions/:session_id/close", HomeLive, :close_session

View file

@ -11,8 +11,7 @@ defmodule Livebook.MixProject do
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
aliases: aliases(), aliases: aliases(),
deps: deps(), deps: deps(),
escript: escript(), escript: escript()
preferred_cli_env: preferred_cli_env()
] ]
end end
@ -58,10 +57,4 @@ defmodule Livebook.MixProject do
app: nil app: nil
] ]
end end
defp preferred_cli_env() do
[
build: :prod
]
end
end end

View file

@ -59,33 +59,31 @@ defmodule LivebookWeb.AuthPlugTest do
end end
@tag token: "grumpycat" @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") conn = get(conn, "/?token=grumpycat")
assert get_session(conn, "80:token")
assert Map.has_key?(conn.resp_cookies, "80:token") conn = get(conn, "/")
conn =
build_conn()
|> Plug.Test.recycle_cookies(conn)
|> get("/")
assert conn.status == 200 assert conn.status == 200
assert conn.resp_body =~ "New notebook" assert conn.resp_body =~ "New notebook"
end end
end end
describe "password authentication" do describe "password authentication" do
@tag password: "grumpycat" test "redirects to '/' if no authentication is required", %{conn: conn} do
test "redirects to '/authenticate' if not already authenticated", %{conn: conn} do conn = get(conn, "/authenticate")
conn = get(conn, "/") 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" assert redirected_to(conn) == "/authenticate"
end end
@tag password: "grumpycat" @tag password: "grumpycat"
test "redirects to '/' on valid authentication", %{conn: conn} do test "redirects to '/' on valid authentication", %{conn: conn} do
conn = post(conn, Routes.auth_path(conn, :authenticate), password: "grumpycat") conn = post(conn, Routes.auth_path(conn, :authenticate), password: "grumpycat")
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
conn = get(conn, "/") conn = get(conn, "/")
@ -95,24 +93,23 @@ defmodule LivebookWeb.AuthPlugTest do
@tag password: "grumpycat" @tag password: "grumpycat"
test "redirects back to '/authenticate' on invalid password", %{conn: conn} do test "redirects back to '/authenticate' on invalid password", %{conn: conn} do
conn = post(conn, Routes.auth_path(conn, :authenticate), password: "invalid password") conn = post(conn, Routes.auth_path(conn, :authenticate), password: "invalid password")
assert html_response(conn, 200) =~ "Authentication required"
conn = get(conn, "/") conn = get(conn, "/")
assert redirected_to(conn) == "/authenticate" assert redirected_to(conn) == "/authenticate"
end end
@tag password: "grumpycat" @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") 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 = get(conn, "/")
conn =
build_conn()
|> Plug.Test.recycle_cookies(conn)
|> get("/")
assert conn.status == 200 assert conn.status == 200
assert conn.resp_body =~ "New notebook" assert conn.resp_body =~ "New notebook"
conn = get(conn, "/authenticate")
assert redirected_to(conn) == "/"
end end
end end
end end