CI Hex.pm Documentation

A simple, database-free alternative to HTTP Basic Auth with session-based authentication for Plug-compatible applications.

See the Releases page for a changelog of notable changes between versions.

[!WARNING] OneAuth is under active development. APIs may change before the first stable release.

Table of Contents

Why OneAuth?

Some applications only need to protect access behind a single account — a personal dashboard, an internal tool, an admin interface. Pulling in a full user-authentication system for that is overkill, and relying on the browser's built-in HTTP Basic Auth dialog is a poor experience with no real logout flow.

OneAuth fills that gap: a normal, session-backed login flow, configured with a single username and password, and no database required.

Good fits include:

  • Personal dashboards
  • Internal tools
  • Admin interfaces
  • Small private applications

OneAuth is not intended for applications requiring:

  • Multiple users
  • User registration
  • Password recovery
  • OAuth providers
  • Roles and permissions

OneAuth is built on Plug, so it isn't tied to a specific web framework. It works with any Plug-compatible application, including Phoenix and other frameworks built on top of Plug. The examples below use Phoenix since it's the most common target.

Features

  • Database-free — configuration lives in runtime.exs, no schema or migrations
  • Session-based — a real login/logout flow instead of a browser Basic Auth dialog
  • Single account — built for the "one user" use case, not multi-tenant auth
  • Framework-agnostic — works with any Plug-based application
  • Small surface area — a handful of plugs and functions, easy to audit

Installation

Add :one_auth to your list of dependencies in mix.exs:

def deps do
  [
    {:one_auth, "~> 0.1"}
  ]
end

Configuration

OneAuth is configured through your application's runtime.exs.

The minimum required configuration is:

config :one_auth,
  username: System.fetch_env!("ONE_AUTH_USERNAME"),
  password: System.fetch_env!("ONE_AUTH_PASSWORD"),
  signing_secret: System.fetch_env!("ONE_AUTH_SIGNING_SECRET")

Additional options are available for customizing session behavior and routes. See the Configuration guide for the full list.

Phoenix Setup

Add OneAuth.Plug.LoadSession to your :browser pipeline, after :fetch_session, so the current session is loaded on every request. Add a separate :require_auth pipeline for routes that need to be protected:

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  # ...
  plug OneAuth.Plug.LoadSession
end

pipeline :require_auth do
  plug OneAuth.Plug.RequireAuth
end

Login Example

Add routes for rendering the login form and submitting credentials, scoped to your :browser pipeline:

scope "/", MyAppWeb do
  pipe_through :browser

  get "/login", SessionController, :new
  post "/login", SessionController, :create
end

Handle both actions in a controller. OneAuth.login/3 verifies the submitted credentials and starts the session:

defmodule MyAppWeb.SessionController do
  use MyAppWeb, :controller

  def new(conn, _params) do
    render(conn, :new)
  end

  def create(conn, %{"username" => username, "password" => password}) do
    case OneAuth.login(conn, username, password) do
      {:ok, conn} ->
        redirect(conn, to: OneAuth.login_redirect_path(conn))

      :error ->
        conn
        |> put_flash(:error, "Invalid username or password")
        |> redirect(to: OneAuth.login_path(conn))
    end
  end
end

OneAuth.login_redirect_path/1 is where a successful login sends the user. If they were redirected to /login from a protected page (see Protecting Routes), they're sent back there. Otherwise it falls back to :login_redirect_path, which defaults to "/".

Then render the form itself. OneAuth.login_path/1 gives you the correct action URL without hardcoding it:

defmodule MyAppWeb.SessionHTML do
  use MyAppWeb, :html

  def new(assigns) do
    ~H"""
    <h1>Log in to your account</h1>

    <.simple_form for={%{}} action={OneAuth.login_path(@conn)}>
      <input
        id="username"
        type="text"
        name="username"
        placeholder="Username"
        autocomplete="username"
        aria-label="Username"
        required
      />

      <input
        id="password"
        type="password"
        name="password"
        placeholder="Password"
        autocomplete="current-password"
        aria-label="Password"
        required
      />

      <:actions>
        <.button phx-disable-with="Logging in...">Log in</.button>
      </:actions>
    </.simple_form>
    """
  end
end

Protecting Routes

Pipe any scope you want to protect through :require_auth in addition to :browser. Unauthenticated requests are redirected to the :login_path ("/login" by default), with the originally requested path remembered so the user is sent back there after logging in:

scope "/admin", MyAppWeb do
  pipe_through [:browser, :require_auth]

  get "/", AdminController, :index
end

Everything under /admin in this example now requires a logged-in session — no per-action checks needed.

Accessing the Current User

Once a session is loaded, OneAuth.current_user/1 returns the logged-in username (or nil if there isn't one). This works anywhere you have access to conn, including in controllers and templates:

def index(conn, _params) do
  current_user = OneAuth.current_user(conn)
  render(conn, :index, current_user: current_user)
end

Then reference the assign in your template:

<p>Signed in as {@current_user}</p>

In a Layout

A single controller assign only covers one action. For something like a root layout, where you want a greeting or login/logout link on every page, assign current_user once in your pipeline instead:

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  # ...
  plug OneAuth.Plug.LoadSession
  plug :assign_current_user
end

def assign_current_user(conn, _opts) do
  assign(conn, :current_user, OneAuth.current_user(conn))
end

@current_user is now available in every template rendered through that pipeline, including your root layout:

<header>
  <%= if @current_user do %>
    <span>Hi, {@current_user}</span>
    <.link href={~p"/logout"} method="delete">Log out</.link>
  <% else %>
    <.link href={~p"/login"}>Log in</.link>
  <% end %>
</header>

Logout Example

Add a logout route:

scope "/", MyAppWeb do
  pipe_through :browser

  delete "/logout", SessionController, :delete
end

Handle it in your controller:

def delete(conn, _params) do
  conn
  |> OneAuth.logout()
  |> redirect(to: OneAuth.login_path(conn))
end

Wire up a logout link wherever you need one:

<.link href={~p"/logout"} method="delete">Log out</.link>

Public API

OneAuth intentionally exposes a small public API.

OneAuth.login(conn, username, password)
OneAuth.login_path(conn)
OneAuth.login_redirect_path(conn)
OneAuth.current_user(conn)
OneAuth.logout(conn)

Most applications should only need these functions together with OneAuth.Plug.LoadSession and OneAuth.Plug.RequireAuth.

License

OneAuth is released under the MIT License.