Packages
phoenix_kit
1.5.1
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
Current section
Files
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
## 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.Utils.Routes
import Ecto.Query
@magic_link_context "magic_link"
@doc """
Generates a magic link for the given email address.
Returns `{:ok, user, token}` if the user exists, or `{:error, :user_not_found}`
if no user is found with that email.
## 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}
"""
def generate_magic_link(email) when is_binary(email) do
email = String.trim(email) |> String.downcase()
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
end
@doc """
Verifies a magic link token and returns the associated user.
The token is automatically deleted after successful verification (single-use).
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)
{:ok, 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_id == ^user.id 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_id == ^user.id 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
# Get configured repo module
defp repo do
Config.get(:repo, nil)
end
end