Packages

phoenix_kit

1.7.115
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit users magic_link.ex
Raw

lib/phoenix_kit/users/magic_link.ex

defmodule PhoenixKit.Users.MagicLink do
@moduledoc """
Magic Link authentication system for PhoenixKit.
Magic Link provides passwordless authentication where users receive a secure
link via email that allows them to log in without entering a password.
## Features
- **Passwordless Authentication**: Users log in with just their email
- **Secure Token System**: Uses existing UserToken infrastructure
- **Time-Limited Links**: Magic links expire after a configurable period
- **Optional Integration**: Works alongside existing password authentication
- **Email Verification**: Links are sent to the user's email address
- **Auto-Confirmation**: Unconfirmed users are automatically confirmed upon magic link use
## Usage
# Generate and send magic link
case PhoenixKit.Users.MagicLink.generate_magic_link(email) do
{:ok, user, token} ->
# Send email with magic link
PhoenixKit.Mailer.send_magic_link_email(user, token)
{:ok, user}
{:error, :user_not_found} ->
# Handle unknown email
{:error, :invalid_email}
end
# Verify magic link token
case PhoenixKit.Users.MagicLink.verify_magic_link(token) do
{:ok, user} ->
# Log user in
{:ok, user}
{:error, :invalid_token} ->
# Handle invalid/expired token
{:error, :expired_link}
end
## Security Considerations
- Magic link tokens are single-use (automatically deleted after use)
- Short expiry time (default: 15 minutes) to minimize exposure
- Tokens are hashed before storage in database
- Email address verification ensures link goes to correct recipient
- Integration with existing user session management
## Configuration
Magic link expiry can be configured in your application:
# config/config.exs
config :phoenix_kit,
magic_link_for_login_expiry_minutes: 15
"""
alias PhoenixKit.Config
alias PhoenixKit.Users.Auth
alias PhoenixKit.Users.Auth.{User, UserToken}
alias PhoenixKit.Users.RateLimiter
alias PhoenixKit.Utils.Routes
import Ecto.Query
@magic_link_context "magic_link"
@doc """
Generates a magic link for the given email address.
This function includes rate limiting protection to prevent token enumeration attacks.
After exceeding the rate limit (default: 3 requests per 5 minutes), subsequent
requests will be rejected with `{:error, :rate_limit_exceeded}`.
Returns `{:ok, user, token}` if the user exists and rate limit is not exceeded,
`{:error, :user_not_found}` if no user is found with that email, or
`{:error, :rate_limit_exceeded}` if the rate limit has been exceeded.
## Examples
iex> PhoenixKit.Users.MagicLink.generate_magic_link("user@example.com")
{:ok, %User{}, "magic_link_token_here"}
iex> PhoenixKit.Users.MagicLink.generate_magic_link("nonexistent@example.com")
{:error, :user_not_found}
iex> PhoenixKit.Users.MagicLink.generate_magic_link("user@example.com")
{:error, :rate_limit_exceeded}
"""
def generate_magic_link(email) when is_binary(email) do
email = String.trim(email) |> String.downcase()
# Check rate limit before attempting to generate magic link
case RateLimiter.check_magic_link_rate_limit(email) do
:ok ->
case Auth.get_user_by_email(email) do
%User{} = user ->
# Revoke any existing magic link tokens for this user
revoke_magic_links(user)
# Generate new magic link token
{token, user_token} = UserToken.build_email_token(user, @magic_link_context)
case repo().insert(user_token) do
{:ok, _} ->
{:ok, user, token}
{:error, changeset} ->
{:error, changeset}
end
nil ->
# Perform a fake token generation to prevent timing attacks
# This takes similar time as real token generation
_fake_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
{:error, :user_not_found}
end
{:error, :rate_limit_exceeded} ->
{:error, :rate_limit_exceeded}
end
end
@doc """
Verifies a magic link token and returns the associated user.
The token is automatically deleted after successful verification (single-use).
If the user's email is not yet confirmed, this function will automatically
confirm the user, since clicking the magic link proves email ownership.
Returns `{:ok, user}` if the token is valid, or `{:error, :invalid_token}`
if the token is invalid, expired, or already used.
## Examples
iex> PhoenixKit.Users.MagicLink.verify_magic_link("valid_token")
{:ok, %User{}}
iex> PhoenixKit.Users.MagicLink.verify_magic_link("invalid_token")
{:error, :invalid_token}
"""
def verify_magic_link(token) when is_binary(token) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(:sha256, decoded_token)
expiry_minutes = get_expiry_minutes()
# Create query specifically for magic links with minute-based expiry
query =
from token in UserToken,
join: user in assoc(token, :user),
where:
token.token == ^hashed_token and
token.context == ^@magic_link_context and
token.inserted_at > ago(^expiry_minutes, "minute") and
token.sent_to == user.email,
select: {user, token}
case repo().one(query) do
{user, user_token} ->
# Delete the token to make it single-use
repo().delete(user_token)
# Auto-confirm user on magic link authentication
confirm_user_if_needed(user)
nil ->
{:error, :invalid_token}
end
:error ->
{:error, :invalid_token}
end
end
@doc """
Revokes all active magic link tokens for a user.
This is useful when:
- Generating a new magic link (only one should be active)
- User logs in via other means (invalidate pending magic links)
- Security concerns require invalidating all passwordless access
## Examples
iex> PhoenixKit.Users.MagicLink.revoke_magic_links(user)
:ok
"""
def revoke_magic_links(%User{} = user) do
query =
from t in UserToken,
where: t.user_uuid == ^user.uuid and t.context == ^@magic_link_context
repo().delete_all(query)
:ok
end
@doc """
Checks if magic link authentication is enabled for the application.
This can be used in controllers and views to conditionally show magic link UI.
## Examples
iex> PhoenixKit.Users.MagicLink.enabled?()
true
"""
def enabled? do
# Magic link is always available as it uses existing token infrastructure
true
end
@doc """
Gets the number of active magic link tokens for a user.
Useful for debugging or administrative interfaces.
## Examples
iex> PhoenixKit.Users.MagicLink.active_magic_links_count(user)
1
"""
def active_magic_links_count(%User{} = user) do
expiry_minutes = get_expiry_minutes()
query =
from t in UserToken,
where:
t.user_uuid == ^user.uuid and
t.context == ^@magic_link_context and
t.inserted_at > ago(^expiry_minutes, "minute")
repo().aggregate(query, :count)
end
@doc """
Generates a magic link URL for the given token.
This is a convenience function to construct the full URL that should be
included in magic link emails.
## Examples
iex> PhoenixKit.Users.MagicLink.magic_link_url("token123")
"http://localhost:4000{prefix}/users/magic-link/token123"
# Where {prefix} is the configured PhoenixKit URL prefix
"""
def magic_link_url(token) when is_binary(token) do
Routes.url("/users/magic-link/#{token}")
end
@doc """
Cleans up expired magic link tokens.
This function can be called periodically (e.g., via a scheduled job) to
remove expired tokens from the database.
Returns the number of tokens deleted.
## Examples
iex> PhoenixKit.Users.MagicLink.cleanup_expired_tokens()
5 # 5 expired tokens were deleted
"""
def cleanup_expired_tokens do
expiry_minutes = get_expiry_minutes()
query =
from t in UserToken,
where:
t.context == ^@magic_link_context and
t.inserted_at <= ago(^expiry_minutes, "minute")
{deleted_count, _} = repo().delete_all(query)
deleted_count
end
# Get configured expiry time in minutes
defp get_expiry_minutes do
Config.get(:magic_link_for_login_expiry_minutes, 15)
end
# Auto-confirm user if not yet confirmed
# If user can click the magic link, they have proven email ownership
defp confirm_user_if_needed(%User{confirmed_at: nil} = user) do
case Auth.admin_confirm_user(user) do
{:ok, confirmed_user} ->
PhoenixKit.Activity.log(%{
action: "user.email_confirmed",
module: "users",
mode: "auto",
actor_uuid: confirmed_user.uuid,
resource_type: "user",
resource_uuid: confirmed_user.uuid,
metadata: %{"method" => "magic_link", "actor_role" => "user"}
})
{:ok, confirmed_user}
{:error, _changeset} ->
{:ok, user}
end
end
defp confirm_user_if_needed(%User{} = user), do: {:ok, user}
# Get configured repo module
defp repo do
Config.get(:repo, nil)
end
end