Current section
Files
Jump to
Current section
Files
lib/email_authentication_controller.ex
defmodule KeenAuth.EmailAuthenticationController do
@moduledoc """
Handles email/password authentication as an alternative entry point to OAuth.
This controller provides the same pipeline integration as OAuth authentication,
but delegates credential verification to your application via the
`KeenAuth.EmailAuthenticationHandler` behaviour.
## Authentication Flow
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ OAuth (Assent) │ │ Email (You) │ │ │
│ External verify │ OR │ Local verify │ ──▶ │ KeenAuth │
└────────┬────────┘ └────────┬────────┘ │ Pipeline │
│ │ │ │
└───────────┬───────────┘ │ Mapper ──▶ │
│ │ Processor ──▶ │
▼ │ Storage │
{:ok, raw_user} └─────────────────┘
```
Both OAuth and Email authentication produce a `raw_user` map that then flows
through the standard KeenAuth pipeline (Mapper → Processor → Storage).
## Configuration
config :keen_auth,
email_enabled: true,
strategies: [
email: [
authentication_handler: MyApp.Auth.EmailHandler,
mapper: KeenAuth.Mapper.Default, # or custom
processor: MyApp.Auth.Processor
]
]
## Implementing the Handler
You must implement `KeenAuth.EmailAuthenticationHandler` behaviour:
defmodule MyApp.Auth.EmailHandler do
@behaviour KeenAuth.EmailAuthenticationHandler
@impl true
def authenticate(_conn, %{"email" => email, "password" => password}) do
case MyApp.Accounts.verify_credentials(email, password) do
{:ok, user} ->
# Return raw user map (will be passed to Mapper)
{:ok, %{
"sub" => user.id,
"email" => user.email,
"name" => user.name
}}
:error ->
{:error, :invalid_credentials}
end
end
@impl true
def handle_authenticated(conn, _user) do
# Called after successful auth, before redirect
conn
end
@impl true
def handle_unauthenticated(conn, _params, _error) do
conn
|> Phoenix.Controller.put_flash(:error, "Invalid credentials")
|> Phoenix.Controller.redirect(to: "/login")
end
end
## Routes
When `email_enabled: true`, the following route is added:
POST /auth/email/new # Submit email/password form
## Security Features
- Timing attack mitigation via random sleep on failed attempts
- Redirect URL validation (same as OAuth flow)
"""
alias KeenAuth.Mapper
alias KeenAuth.Plug.AuthSession
alias KeenAuth.Storage
alias KeenAuth.Processor
alias KeenAuth.EmailAuthenticationHandler
use Phoenix.Controller, formats: [:html, :json]
defmacro __using__(_opts \\ []) do
quote do
use Phoenix.Controller, formats: [:html, :json]
@behaviour unquote(__MODULE__)
def new(conn, params), do: unquote(__MODULE__).new(conn, params)
# def delete(conn, params), do: unquote(__MODULE__).delete(conn, params)
defoverridable unquote(__MODULE__)
end
end
# TODO Throttling
def new(conn, params) do
provider = :email
with {:ok, raw_user} <- EmailAuthenticationHandler.authenticate(conn, params),
response = %{user: raw_user, tokens: nil},
mapped_user = Mapper.current_mapper(conn, provider).map(provider, raw_user),
{:ok, conn, user, result} <- Processor.process(conn, provider, mapped_user, response),
{:ok, conn} <- Storage.store(conn, provider, user, result) do
EmailAuthenticationHandler.handle_authenticated(conn, user)
# Regenerate session ID to prevent session fixation
conn = AuthSession.regenerate(conn)
conn
|> redirect_back(params)
else
err ->
Process.sleep(Enum.random(100..300//10))
# Sleep for random amount to prevent timing attacks
EmailAuthenticationHandler.handle_unauthenticated(conn, params, err)
end
end
# def delete(conn, params) do
# storage = Storage.current_storage(conn)
# with user when not is_nil(user) <- storage.current_user(conn) do
# conn
# |> storage.delete()
# |> redirect_back(params)
# else
# nil ->
# redirect_back(conn, params)
# end
# end
@spec redirect_back(Plug.Conn.t(), map()) :: Plug.Conn.t()
def redirect_back(conn, params \\ %{}) do
KeenAuth.Helpers.RequestHelpers.redirect_back(conn, params)
end
end