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
endConfiguration
config :keen_auth,
email_enabled: true,
strategies: [
email: [
authentication_handler: MyApp.Auth.EmailHandler,
mapper: KeenAuth.Mapper.Default,
processor: MyApp.Auth.Processor
]
]
Summary
Callbacks
Verify user credentials and return raw user data.
Called after successful authentication, before redirect.
Handle failed authentication attempts.
Callbacks
@callback authenticate(conn :: Plug.Conn.t(), params :: map()) :: {:ok, map()} | {:error, term()}
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_mapshould include keys like"sub","email","name"that your Mapper expects.{:error, reason}- Credentials invalid.reasonis passed tohandle_unauthenticated/3.
@callback handle_authenticated(conn :: Plug.Conn.t(), user :: KeenAuth.User.t()) :: Plug.Conn.t()
Called after successful authentication, before redirect.
Use for side effects like audit logging. The return value should be the conn (potentially modified).
@callback handle_unauthenticated( conn :: Plug.Conn.t(), params :: map(), error :: term() ) :: Plug.Conn.t()
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.