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
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()
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:
- Removes any existing one_time_passwords for the provided email
- Creates a new one_time_password
- Generates a new random numeric password
- Encrypts the password using Bcrypt
- Stores the hash in the database
- Calls the configured callback module's
one_time_password_requested/1
function which should handle sending the password to the user via email
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 dataassigns[:current_user]
will contain the user data only if:- A tuple
{:allow, user_id}
was returned from thelog_in_requested/1
callback during authentication - The user can be retrieved using the configured
get_user
function. This function should be configured withconfig :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.
- A tuple
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
Gets the session with the given token.
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 connectionemail
: String containing the user's email addresscode
: String containing the one-time password code
Logs out and redirects to the log in page.
It clears all session data for safety.
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 connectionopts
- Options for controlling the logout behavior::disconnect_self
- Boolean that determines whether to disconnect the current session socket (defaults totrue
)
Returns
%Plug.Conn{}
- The updated connection with cleared session data and redirect to root path
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 terminatedsession
- The current session structopts
- Options for controlling the logout behavior::disconnect_self
- Boolean that determines whether to disconnect the current session socket (defaults totrue
)
Returns
:ok
- When the operation completes successfully
Mount function for LiveViews that require authentication.
This function:
- Mounts the user session on the socket
- Assigns the current user to the socket if a tuple
{:allow, user_id}
was returned from thelog_in_requested/1
callback during authentication - Continues the mount flow if user is authenticated
- 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
Used for routes that require the user to not be authenticated.
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
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:
- Looks up the one-time password record for the given email
- Returns error if no password exists (with timing attack protection)
- Checks if password has expired based on configured expiration time
- Verifies the provided password matches the stored hash