MagicAuth (magic_auth v0.2.0)

MagicAuth is an authentication library for Phoenix that provides effortless configuration and flexibility for your project.

Key Features:

  • Passwordless Authentication: Secure login process through one-time passwords sent via email
  • Enhanced Security: Protect your application from brute force attacks with built-in rate limiting and account lockout mechanisms
  • Customizable Interface: Fully customizable UI components to match your design
  • Effortless Configuration: Quick and simple integration with your Phoenix project
  • Schema Agnostic: Implement authentication without requiring a user schema - ideal for everything from MVPs to complex applications

To get started, see the installation documentation in MagicAuth.MixProject.

Summary

Functions

Returns a list of child processes that should be supervised.

Creates and sends a one-time password for a given email.

Authenticates the user session by looking into the session and remember me token.

Gets the session with the given token.

Logs the session in.

Logs out and redirects to the log in page.

Logs out all sessions for a given user and redirects to the log in page.

Logs out all sessions for a given user and session.

Mount function for LiveViews that require authentication.

Used for routes that require the user to not be authenticated.

Plug function that verifies if the user is authenticated.

Verifies a one-time password for a given email.

Functions

children()

Returns a list of child processes that should be supervised.

Includes token buckets needed for rate limiting:

  • OneTimePasswordRequestTokenBucket: Limits one-time password requests
  • LoginAttemptTokenBucket: Limits login attempts

Example

In your application.ex (this configuration is automatically added by the mix magic_auth.install task):

children = children ++ MagicAuth.children()

create_one_time_password(attrs)

Creates and sends a one-time password for a given email.

The one-time password is stored in the database to allow users to log in from a device that doesn't have access to the email where the code was sent. For example, the user can receive the code on their phone and use it to log in on their computer.

When called, this function creates a new one_time_password record and generates a one-time password that will be used to authenticate it. The password is then passed to the configured callback module one_time_password_requested/1 which should handle sending it to the user via email.

One-time password generation is rate limited using a token bucket system that allows a maximum of 1 generation request per minute for each email address. This prevents abuse of the email delivery service.

Parameters

  • attrs - A map containing :email

Returns

  • {:ok, code, one_time_password} - Returns the created one_time_password on success
  • {:error, changeset} - Returns the changeset with errors if validation fails
  • {:error, failed_value} - Returns the failed value if the transaction fails
  • {:error, :rate_limited, countdown} - Returns the countdown if the rate limit is exceeded

Examples

iex> MagicAuth.create_one_time_password(%{"email" => "user@example.com"})
{:ok, code, %MagicAuth.OneTimePassword{}}

The one time password length can be configured in config/config.exs:

config :magic_auth,
  one_time_password_length: 6 # default value

This function:

  1. Removes any existing one_time_passwords for the provided email
  2. Creates a new one_time_password
  3. Generates a new random numeric password
  4. Encrypts the password using Bcrypt
  5. Stores the hash in the database
  6. Calls the configured callback module's one_time_password_requested/1 function which should handle sending the password to the user via email

fetch_magic_auth_session(conn, opts)

Authenticates the user session by looking into the session and remember me token.

This function is designed to be used as a plug in your Phoenix router's pipeline. It fetches the user's session from either the session storage or the remember me cookie, and assigns the session and user (if available) to the connection.

When a user is successfully authenticated:

  • assigns[:current_session] will contain the session data
  • assigns[:current_user] will contain the user data only if:
    1. A tuple {:allow, user_id} was returned from the log_in_requested/1 callback during authentication
    2. The user can be retrieved using the configured get_user function. This function should be configured with config :magic_auth, get_user: &MyApp.Accounts.get_user_by_id/1. An error will be raised if the tuple is returned from the callback and the user schema is not configured.

If the callback returned just :allow without a user_id, or if the user schema is not configured, current_user will be nil.

Examples

# In your router.ex
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_root_layout, {MyAppWeb.LayoutView, :root}
  plug :protect_from_forgery
  plug :put_secure_browser_headers
  plug :fetch_magic_auth_session
end

get_session_by_token(token)

Gets the session with the given token.

live_socket_id(token)

log_in(conn, email, code)

Logs the session in.

It renews the session ID and clears the whole session to avoid fixation attacks.

Login attempts are rate limited using a token bucket that allows a maximum of 10 attempts every 10 minutes per email address.

It also sets a :live_socket_id key in the session, so LiveView sessions are identified and automatically disconnected on log out.

On login success:

  • Renews the session to prevent fixation attacks
  • Sets the token in session and cookie (if remember me is enabled)
  • Redirects to original page requested or to / (default route)

On error:

  • If too many attempts: Redirects to /sessions/log_in with rate limit error message
  • If invalid code: Redirects to /sessions/password
  • If expired code: Redirects to /sessions/password
  • If access denied: Redirects to /sessions/log_in with access denied error message

Denying access

To deny access, implement the log_in_requested/1 callback in your callback module returning :deny. For example:

def log_in_requested(email) do
  case Accounts.get_user_by_email(email) do
    %User{active: false} -> :deny  # Denies access for inactive users
    _ -> :allow
  end
end

For more information on denying access, see the comments for the log_in_requested/1 function in the generated MagicAuth module in your application's codebase.

Parameters

  • conn: The Plug.Conn connection
  • email: String containing the user's email address
  • code: String containing the one-time password code

log_out(conn)

Logs out and redirects to the log in page.

It clears all session data for safety.

log_out_all(conn, opts \\ [])

Logs out all sessions for a given user and redirects to the log in page.

It deletes all sessions associated with the user from the database and broadcasts a disconnect message to all live views connected with those sessions.

Except for special cases, you should not use this function directly. If the user requests to log out of all sessions, you should redirect to the logout page.

Parameters

  • conn - The Plug.Conn connection
  • opts - Options for controlling the logout behavior:
    • :disconnect_self - Boolean that determines whether to disconnect the current session socket (defaults to true)

Returns

  • %Plug.Conn{} - The updated connection with cleared session data and redirect to root path

log_out_all(user, session, opts)

Logs out all sessions for a given user and session.

This is a lower-level function that handles the actual session deletion and socket disconnection logic. It's used by log_out_all/2 which provides the connection-based interface.

Parameters

  • user - The user struct whose sessions should be terminated
  • session - The current session struct
  • opts - Options for controlling the logout behavior:
    • :disconnect_self - Boolean that determines whether to disconnect the current session socket (defaults to true)

Returns

  • :ok - When the operation completes successfully

on_mount(atom, params, session, socket)

Mount function for LiveViews that require authentication.

This function:

  1. Mounts the user session on the socket
  2. Assigns the current user to the socket if a tuple {:allow, user_id} was returned from the log_in_requested/1 callback during authentication
  3. Continues the mount flow if user is authenticated
  4. Halts and redirects to login page if user is not authenticated

Examples of usage

In LiveView modules:

defmodule MyAppWeb.DashboardLive do
  use MyAppWeb, :live_view

  on_mount {MagicAuth, :require_authenticated}

  def mount(_params, _session, socket) do
    {:ok, socket}
  end
end

In router.ex:

live_session :admin,
  on_mount: [{MagicAuth, :require_authenticated}] do
  live "/dashboard", DashboardLive
end

redirect_if_authenticated(conn, opts)

Used for routes that require the user to not be authenticated.

require_authenticated(conn, opts)

Plug function that verifies if the user is authenticated.

If the user is not authenticated:

  • Stores the current URL in the session for later redirect
  • Redirects to the login page (defaults to: /session/log_in)
  • Shows unauthorized error message
  • Halts request processing

If the user is authenticated:

  • Allows the request to continue normally with the session information
  • The current_session is available in conn.assigns[:current_session]

Examples of usage

In router.ex routes:

scope "/", MyAppWeb do
  pipe_through [:browser, :require_authenticated]

  get "/dashboard", DashboardController, :index
  live "/profile", ProfileLive
end

verify_password(email, password)

Verifies a one-time password for a given email.

Takes an email and password as input and validates the one-time password.

Returns:

  • {:ok, one_time_password} if the password is valid
  • {:error, :invalid_code} if the password is invalid or no password exists for email
  • {:error, :code_expired} if the password has expired

The function:

  1. Looks up the one-time password record for the given email
  2. Returns error if no password exists (with timing attack protection)
  3. Checks if password has expired based on configured expiration time
  4. Verifies the provided password matches the stored hash