Packages

WebSocket-first Elixir framework with auto-generated HTTP MCP APIs

Current section

Files

Jump to
dialup lib auth plug.ex
Raw

lib/auth/plug.ex

defmodule Dialup.Auth.Plug do
@moduledoc """
HTTP authentication plug for Dialup applications.
use Dialup,
auth_accounts: MyApp.Accounts,
plugs: [{Dialup.Auth.Plug, accounts: MyApp.Accounts}]
"""
@behaviour Plug
import Plug.Conn
@auth_paths ~w(/log_in /log_out /register /reset_password/request /reset_password/confirm)
@impl Plug
def init(opts) do
accounts = Keyword.fetch!(opts, :accounts)
unless Code.ensure_loaded?(accounts) and
function_exported?(accounts, :get_user_by_session_token, 1) do
raise ArgumentError,
"#{inspect(accounts)} must implement Dialup.Auth.Accounts callbacks"
end
Keyword.merge(opts, accounts: accounts)
end
@impl Plug
def call(conn, opts) do
accounts = Keyword.fetch!(opts, :accounts)
conn = fetch_cookies(conn)
case conn.method do
"POST" when conn.request_path in @auth_paths ->
handle_auth_post(conn, accounts)
_ ->
{conn, _user} = Dialup.Auth.fetch_current_user(conn, accounts)
conn
end
end
defp handle_auth_post(conn, accounts) do
conn = fetch_query_params(conn)
with :ok <- validate_csrf(conn),
{:ok, conn} <- dispatch_auth(conn, accounts) do
conn
else
{:error, :invalid_credentials} ->
conn
|> put_resp_content_type("text/html")
|> send_resp(401, "Invalid email or password")
|> halt()
{:error, reason} ->
conn
|> put_resp_content_type("text/html")
|> send_resp(400, auth_error_message(reason))
|> halt()
end
end
defp validate_csrf(conn) do
if Dialup.Auth.valid_csrf?(conn, conn.params), do: :ok, else: {:error, :invalid_csrf}
end
defp dispatch_auth(%{request_path: "/log_in"} = conn, accounts) do
email = param(conn, "email")
password = param(conn, "password")
if is_binary(email) and is_binary(password) do
case accounts.get_user_by_email_and_password(email, password) do
{:ok, user} ->
conn =
user
|> then(&Dialup.Auth.log_in_user(conn, &1, accounts))
|> redirect_to(Dialup.Auth.safe_redirect_path(param(conn, "return_to")))
|> halt()
{:ok, conn}
{:error, :invalid_credentials} ->
{:error, :invalid_credentials}
end
else
{:error, :invalid_credentials}
end
end
defp dispatch_auth(%{request_path: "/log_out"} = conn, accounts) do
conn =
conn
|> Dialup.Auth.log_out_user(accounts)
|> redirect_to("/")
|> halt()
{:ok, conn}
end
defp dispatch_auth(%{request_path: "/register"} = conn, accounts) do
if function_exported?(accounts, :register_user, 1) do
attrs = %{"email" => param(conn, "email"), "password" => param(conn, "password")}
case accounts.register_user(attrs) do
{:ok, user} ->
conn =
user
|> then(&Dialup.Auth.log_in_user(conn, &1, accounts))
|> redirect_to("/")
|> halt()
{:ok, conn}
{:error, _changeset} ->
{:error, :registration_failed}
end
else
{:error, :registration_unavailable}
end
end
defp dispatch_auth(%{request_path: "/reset_password/request"} = conn, accounts) do
email = param(conn, "email")
if is_binary(email) and
function_exported?(accounts, :get_user_by_email, 1) and
function_exported?(accounts, :deliver_user_reset_password_instructions, 2) do
case accounts.get_user_by_email(email) do
nil -> :ok
user -> accounts.deliver_user_reset_password_instructions(user, &"/reset_password/#{&1}")
end
end
conn = conn |> redirect_to("/log_in") |> halt()
{:ok, conn}
end
defp dispatch_auth(%{request_path: "/reset_password/confirm"} = conn, accounts) do
if function_exported?(accounts, :get_user_by_reset_password_token, 1) and
function_exported?(accounts, :reset_user_password, 2) do
token = param(conn, "token")
password = param(conn, "password")
if is_binary(token) and is_binary(password) do
with user when not is_nil(user) <- accounts.get_user_by_reset_password_token(token),
{:ok, user} <- accounts.reset_user_password(user, %{"password" => password}) do
conn =
user
|> then(&Dialup.Auth.log_in_user(conn, &1, accounts))
|> redirect_to("/")
|> halt()
{:ok, conn}
else
_ -> {:error, :invalid_reset_token}
end
else
{:error, :invalid_reset_token}
end
else
{:error, :reset_password_unavailable}
end
end
defp param(conn, key), do: conn.params[key] || conn.params[:"#{key}"]
defp redirect_to(conn, path) do
conn |> put_resp_header("location", path) |> send_resp(302, "")
end
defp auth_error_message(:invalid_csrf), do: "Invalid CSRF token"
defp auth_error_message(:registration_failed), do: "Could not register user"
defp auth_error_message(:registration_unavailable), do: "Registration is not available"
defp auth_error_message(:reset_password_unavailable), do: "Password reset is not available"
defp auth_error_message(:invalid_reset_token), do: "Invalid or expired reset token"
defp auth_error_message(_), do: "Authentication failed"
end