Skip to content

tomkonidas/one-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OneAuth

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.

About

A simple alternative to HTTP Basic Auth for Plug applications

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages