KeenAuth provides a powerful pipeline-based authentication system for Phoenix applications.
The library implements a "super simple yet super powerful" approach with two entry points (OAuth or Email) that converge into a shared pipeline: Mapper → Processor → Storage.
Architecture Overview
┌─────────────────────────────────┐
│ ENTRY POINTS │
└─────────────────────────────────┘
│
┌───────────────────────┴───────────────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ OAuth (Assent) │ │ Email (Custom) │
│ │ │ │
│ External provider │ │ Your app verifies │
│ verifies creds │ │ email/password │
└─────────┬─────────┘ └─────────┬─────────┘
│ │
│ {:ok, raw_user} │
└───────────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ KEEN AUTH PIPELINE │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ MAPPER │
│ Normalize raw_user → User │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ PROCESSOR │
│ Business logic, DB, roles │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ STORAGE │
│ Persist session/tokens │
└─────────────────────────────────┘Entry Points
- OAuth (via Assent): External providers handle credential verification
- Email: Your app implements
KeenAuth.EmailAuthenticationHandlerto verify credentials
Both entry points produce a raw_user map that flows through the same pipeline.
Pipeline Stages
- Mapper: Normalizes user data and can enrich with external API calls
- Processor: Implements business logic, validation, and user transformations
- Storage: Manages data persistence (sessions, database, JWT, custom)
Basic Configuration
config :keen_auth,
strategies: [
azure_ad: [
strategy: Assent.Strategy.AzureAD,
mapper: KeenAuth.Mappers.AzureAD,
processor: MyApp.Auth.Processor,
config: [
tenant_id: System.get_env("AZURE_TENANT_ID"),
client_id: System.get_env("AZURE_CLIENT_ID"),
client_secret: System.get_env("AZURE_CLIENT_SECRET"),
redirect_uri: "https://myapp.com/auth/azure_ad/callback"
]
],
github: [
strategy: Assent.Strategy.Github,
mapper: KeenAuth.Mappers.Github,
processor: MyApp.Auth.Processor,
config: [
client_id: System.get_env("GITHUB_CLIENT_ID"),
client_secret: System.get_env("GITHUB_CLIENT_SECRET"),
redirect_uri: "https://myapp.com/auth/github/callback"
]
]
]Router Integration
Add authentication routes using the macro:
defmodule MyAppWeb.Router do
require KeenAuth
scope "/auth" do
pipe_through :browser
KeenAuth.authentication_routes()
end
endUsage
Check authentication status and get current user:
if KeenAuth.authenticated?(conn) do
user = KeenAuth.current_user(conn)
# User is authenticated
else
# Redirect to login
end
Summary
Functions
Assigns a user to the connection.
Checks if the current connection has an authenticated user.
Generates authentication routes for the router.
Returns the current authenticated user from the connection.
Returns a list of configured authentication providers with metadata.
Returns a list of configured provider names (atoms).
Renders provider buttons using a custom callback function.
Types
@type user() :: KeenAuth.User.t() | map() | term()
Functions
@spec assign_current_user(Plug.Conn.t(), user()) :: Plug.Conn.t()
Assigns a user to the connection.
This function is typically used internally by the authentication pipeline, but can be useful for testing or manual user assignment.
Examples
iex> conn = KeenAuth.assign_current_user(conn, user)
iex> KeenAuth.current_user(conn)
%{id: 123, email: "user@example.com"}
@spec authenticated?(Plug.Conn.t()) :: boolean()
Checks if the current connection has an authenticated user.
Returns true if a user is assigned to the connection, false otherwise.
Examples
iex> KeenAuth.authenticated?(conn_with_user)
true
iex> KeenAuth.authenticated?(conn_without_user)
false
Generates authentication routes for the router.
This macro creates the necessary routes for OAuth authentication flows:
GET /auth/:provider/new- Initiates OAuth flowGET /auth/:provider/callback- Handles OAuth callbackPOST /auth/:provider/callback- Handles OAuth callback (POST)GET /auth/:provider/delete- Signs out userGET /auth/delete- Signs out user (provider-agnostic)
Optionally includes email authentication routes if :email_enabled is configured.
Example
defmodule MyAppWeb.Router do
require KeenAuth
scope "/auth" do
pipe_through :browser
KeenAuth.authentication_routes()
end
endThis will create routes like:
/auth/azure_ad/new/auth/github/callback/auth/delete
@spec current_user(Plug.Conn.t()) :: user()
Returns the current authenticated user from the connection.
Retrieves the user that was assigned to the connection during the authentication
process, typically by KeenAuth.Plug.FetchUser.
Examples
iex> KeenAuth.current_user(conn)
%{id: 123, email: "user@example.com", name: "John Doe"}
iex> KeenAuth.current_user(unauthenticated_conn)
nil
@spec list_providers(Plug.Conn.t() | atom()) :: [map()]
Returns a list of configured authentication providers with metadata.
Useful for dynamically rendering login pages with only the providers that are actually configured.
Variants
list_providers(conn)- Use when you have a connection that went throughKeenAuth.Pluglist_providers(otp_app)- Use when you don't have a connection (e.g., login page before auth pipeline)
Options
Each provider can include optional metadata in its configuration:
:enabled- Set tofalseto hide provider from list (defaults totrue):label- Display name (defaults to provider name capitalized):icon- Icon identifier or URL:color- Brand color for styling
Example Configuration
config :my_app, :keen_auth,
strategies: [
email: [
label: "Email",
icon: "mail",
authentication_handler: MyApp.Auth.EmailHandler,
...
],
entra: [
label: "Microsoft",
icon: "microsoft",
color: "#0078d4",
strategy: Assent.Strategy.AzureAD,
...
],
github: [
label: "GitHub",
icon: "github",
color: "#333",
strategy: Assent.Strategy.Github,
...
]
]Example Usage
# With connection (after going through KeenAuth.Plug pipeline)
providers = KeenAuth.list_providers(conn)
# Without connection (e.g., login page)
providers = KeenAuth.list_providers(:my_app)
# Returns:
[
%{name: :email, label: "Email", icon: "mail", path: "/auth/email/new", color: nil},
%{name: :entra, label: "Microsoft", icon: "microsoft", path: "/auth/entra/new", color: "#0078d4"},
%{name: :github, label: "GitHub", icon: "github", path: "/auth/github/new", color: "#333"}
]
# In your template
<%= for provider <- @providers do %>
<a href={provider.path} style={"background: #{provider.color}"}>
<i class={"icon-#{provider.icon}"}></i>
<%= provider.label %>
</a>
<% end %>
@spec provider_names(Plug.Conn.t()) :: [atom()]
Returns a list of configured provider names (atoms).
Simpler alternative to list_providers/1 when you only need the names.
Example
KeenAuth.provider_names(conn)
#=> [:email, :entra, :github]
Renders provider buttons using a custom callback function.
This function takes a list of providers (from list_providers/1) and a render
callback that returns HTML for each provider. This allows the same provider list
to be rendered differently in different contexts.
Parameters
providers- List of provider maps fromlist_providers/1render_fn- Function that takes a provider map and returns an HTML string
Provider Map Fields
The callback receives a map with these fields:
:name- Provider atom (e.g.,:github,:entra):label- Display name (e.g., "GitHub", "Microsoft Entra"):icon- Icon identifier (if configured):color- Brand color (if configured):path- Authentication path (e.g., "/auth/github/new")
Examples
providers = KeenAuth.list_providers(:my_app)
# Small inline buttons for navbar
KeenAuth.render_providers(providers, fn p ->
~s(<a href="#{p.path}" class="btn btn-sm">#{p.label}</a>)
end)
# Large buttons with icons for login page
KeenAuth.render_providers(providers, fn p ->
~s'''
<a href="#{p.path}" class="login-btn" style="background: #{p.color || "#333"}">
<i class="icon-#{p.icon}"></i>
<span>#{p.label}</span>
</a>
'''
end)
# Filter OAuth providers only (exclude email)
providers
|> Enum.reject(& &1.name == :email)
|> KeenAuth.render_providers(fn p ->
~s(<button onclick="location.href='#{p.path}'">#{p.label}</button>)
end)