Packages

Library faciliating OpenID authentication flow throughout Phoenix application(s)

Current section

Files

Jump to
keen_auth lib email_authentication_handler.ex
Raw

lib/email_authentication_handler.ex

defmodule KeenAuth.EmailAuthenticationHandler do
@moduledoc """
Behaviour for implementing email/password authentication.
This is the entry point for email-based authentication, equivalent to what
OAuth providers (via Assent) do for OAuth flows. Your implementation verifies
credentials and returns a raw user map that flows through the KeenAuth pipeline.
## Pipeline Position
```
┌──────────────────────┐
│ EmailAuthHandler │ ◀── YOU IMPLEMENT THIS
│ (credential verify) │
└──────────┬───────────┘
│ {:ok, raw_user}
┌──────────────────────┐
│ Mapper │ Normalize to KeenAuth.User
└──────────┬───────────┘
┌──────────────────────┐
│ Processor │ Business logic, DB ops
└──────────┬───────────┘
┌──────────────────────┐
│ Storage │ Persist session
└──────────────────────┘
```
## Callbacks
- `authenticate/2` - Verify credentials, return `{:ok, user_map}` or `{:error, reason}`
- `handle_authenticated/2` - Called after successful auth (logging, side effects)
- `handle_unauthenticated/3` - Handle failed auth (render error, redirect)
## Example Implementation
defmodule MyApp.Auth.EmailHandler do
@behaviour KeenAuth.EmailAuthenticationHandler
import Plug.Conn
import Phoenix.Controller
@impl true
def authenticate(_conn, %{"email" => email, "password" => password}) do
with {:ok, user} <- MyApp.Accounts.get_user_by_email(email),
:ok <- MyApp.Accounts.verify_password(user, password) do
# Return raw user map - will be passed to Mapper
{:ok, %{
"sub" => to_string(user.id),
"email" => user.email,
"name" => user.display_name,
"preferred_username" => user.username
}}
else
_ -> {:error, :invalid_credentials}
end
end
@impl true
def handle_authenticated(conn, user) do
# Optional: audit log, analytics, etc.
Logger.info("User \#{user.email} signed in via email")
conn
end
@impl true
def handle_unauthenticated(conn, params, {:error, :invalid_credentials}) do
conn
|> put_flash(:error, "Invalid email or password")
|> redirect(to: params["redirect_to"] || "/login")
end
def handle_unauthenticated(conn, _params, _other_error) do
conn
|> put_flash(:error, "Authentication failed")
|> redirect(to: "/login")
end
end
## Configuration
config :keen_auth,
email_enabled: true,
strategies: [
email: [
authentication_handler: MyApp.Auth.EmailHandler,
mapper: KeenAuth.Mapper.Default,
processor: MyApp.Auth.Processor
]
]
"""
alias KeenAuth.Strategy
alias KeenAuth.Config
@doc """
Verify user credentials and return raw user data.
Called when a user submits the login form. Should validate the email/password
and return a map with user data that will be passed to the Mapper.
## Return Values
- `{:ok, user_map}` - Credentials valid. `user_map` should include keys like
`"sub"`, `"email"`, `"name"` that your Mapper expects.
- `{:error, reason}` - Credentials invalid. `reason` is passed to
`handle_unauthenticated/3`.
"""
@callback authenticate(conn :: Plug.Conn.t(), params :: map()) ::
{:ok, map()} | {:error, term()}
@doc """
Handle failed authentication attempts.
Called when `authenticate/2` returns an error. Should render an error message
or redirect the user back to the login page.
The controller adds a random delay before calling this to prevent timing attacks.
"""
@callback handle_unauthenticated(
conn :: Plug.Conn.t(),
params :: map(),
error :: term()
) :: Plug.Conn.t()
@doc """
Called after successful authentication, before redirect.
Use for side effects like audit logging. The return value should be the conn
(potentially modified).
"""
@callback handle_authenticated(conn :: Plug.Conn.t(), user :: KeenAuth.User.t()) ::
Plug.Conn.t()
def authenticate(conn, params) do
current_authentication_handler!(conn).authenticate(conn, params)
end
def handle_unauthenticated(conn, params, err) do
current_authentication_handler!(conn).handle_unauthenticated(conn, params, err)
end
def handle_authenticated(conn, user) do
current_authentication_handler!(conn).handle_authenticated(conn, user)
end
def current_authentication_handler!(conn) do
conn
|> KeenAuth.Plug.fetch_config()
|> get_authentication_handler!()
end
def get_authentication_handler!(config) do
Strategy.get_strategy!(config, :email)[:authentication_handler] || Config.raise_error("Authentication handler not defined")
end
end