MagicAuth (magic_auth v0.1.1)

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.

Deletes all sessions associated with a given email.

Deletes all sessions associated with a given token.

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 the user out.

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

create_session!(email)

delete_all_sessions_by_email(email)

Deletes all sessions associated with a given email.

This function should be called when a user is deleted or has their email changed, to ensure that all their active sessions are terminated.

Parameters

  • email - The email of the user whose sessions should be deleted

Examples

iex> MagicAuth.delete_all_sessions_by_email("user@example.com")
{0, nil} # where n is the number of deleted sessions

delete_all_sessions_by_token(token)

Deletes all sessions associated with a given token.

fetch_magic_auth_session(conn, opts)

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

get_session_by_token(token)

Gets the session with the given 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 the user out.

It clears all session data for safety. See renew_session.

on_mount(atom, params, session, socket)

Mount function for LiveViews that require authentication.

This function:

  1. Mounts the user session on the socket
  2. Continues the mount flow if user is authenticated
  3. 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