# OneAuth
[](https://github.com/tomkonidas/one-auth/actions/workflows/ci.yml)
[](https://hex.pm/packages/one_auth)
[](https://hexdocs.pm/one_auth)
A simple, database-free alternative to HTTP Basic Auth with session-based
authentication for Plug-compatible applications.
See the [Releases page](https://github.com/tomkonidas/one-auth/releases) 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?](#why-oneauth)
- [Features](#features)
- [Installation](#installation)
- [Configuration](#configuration)
- [Phoenix Setup](#phoenix-setup)
- [Login Example](#login-example)
- [Protecting Routes](#protecting-routes)
- [Accessing the Current User](#accessing-the-current-user)
- [In a Layout](#in-a-layout)
- [Logout Example](#logout-example)
- [Public API](#public-api)
- [License](#license)
## 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](https://plug.hexdocs.pm/), 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`:
```elixir
def deps do
[
{:one_auth, "~> 0.1"}
]
end
```
## Configuration
OneAuth is configured through your application's `runtime.exs`.
The minimum required configuration is:
```elixir
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](guides/configuration.md)
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:
```elixir
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:
```elixir
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:
```elixir
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](#protecting-routes)), they're sent back there.
Otherwise it falls back to
[`:login_redirect_path`](guides/configuration.md#login_redirect_path),
which defaults to `"/"`.
Then render the form itself. `OneAuth.login_path/1` gives you the
correct action URL without hardcoding it:
```elixir
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`](guides/configuration.md#login_path) (`"/login"` by
default), with the originally requested path remembered so the user is
sent back there after logging in:
```elixir
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:
```elixir
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:
```heex
<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:
```elixir
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:
```heex
<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:
```elixir
scope "/", MyAppWeb do
pipe_through :browser
delete "/logout", SessionController, :delete
end
```
Handle it in your controller:
```elixir
def delete(conn, _params) do
conn
|> OneAuth.logout()
|> redirect(to: OneAuth.login_path(conn))
end
```
Wire up a logout link wherever you need one:
```heex
<.link href={~p"/logout"} method="delete">Log out</.link>
```
## Public API
OneAuth intentionally exposes a small public API.
```elixir
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](LICENSE).