Amur
Simple OAuth for Plug applications.
Amur gives Plug applications a small, provider agnostic OAuth flow without requiring Phoenix. It handles the OAuth handshake, state/PKCE, provider specific configuration, and user normalization. All of this while leaving authentication and user data management up to your application.
- Plug-native, works with Phoenix or standalone Plug
- State & PKCE, handled automatically
- Stateless by default, temporary OAuth state is cleared from session after the callback
- 24 built-in providers
- Normalized users, the same data format across providers
- Custom providers, add providers that aren't built in
- Igniter, get started in under 60 seconds
- Built on Assent, OAuth strategies are provided by Assent
Why Amur?
Amur sits between your router and your OAuth provider:
Amur doesn't create users, manage sessions or impose any authentication systems on your application. It gives you the OAuth result and you decide what happens next.
Quick Start - Igniter (Recommended)
def deps do
[
{:igniter, "~> 0.8"}
]
end
# Defaults to GitHub
mix igniter.install amur --provider <Your Provider>
# Configure multiple providers
mix igniter.install amur --provider github,google
Options:
| Flag | Description |
|---|---|
--provider <name> |
Provider atom used in the generated config (default: github), look here for the list of providers |
--all |
Generate config for every built-in provider (cannot be combined with --provider) |
--app <name> |
Override the detected app name |
--no-config / --no-router / --no-controller |
Skip individual pieces |
Add your secrets into a .env:
GITHUB_CLIENT_ID=<ID>
GITHUB_CLIENT_SECRET=<SECRET>
That's it for basic setup of Amur.
Built-in providers
Amur ships with support for the following providers:
- Apple,
- Auth0
- Azure AD
- Basecamp
- Bitbucket
- DigitalOcean
- Discord
- GitHub
- GitLab
- Hack Club
- LINE
- Slack
- Spotify
- Strava
- Stripe
- Telegram
- Twitch
- Twitter (X)
- VK
- Zitadel
Each is a thin wrapper around the corresponding Assent strategy.
Custom providers
You can define your own provider module using the Amur.Provider behaviour:
defmodule MyApp.Auth.CustomProvider do
use Amur.Provider
@impl true
def strategy, do: Assent.Strategy.OAuth2
@impl true
def base_config do
[
base_url: "https://api.example.com",
authorization_endpoint: "/oauth/authorize",
token_endpoint: "/oauth/token",
user_endpoint: "/user"
]
end
@impl true
def normalize_user(user) do
%{uid: user["id"], email: user["email"], name: user["name"]}
end
end
Then reference it in your config:
config :amur,
providers: [
my_provider: MyApp.Auth.CustomProvider
]
Scopes
When the default scope isn't fit for your needs you can use a custom scope to get exactly whats needed. To request specific OAuth scopes, pass them in your provider config:
config :amur,
providers: [
github: [
client_id: "..",
client_secret: "..",
scopes: "user:email,read:org"
]
]
Manual Setup
This is what igniter sets up for you automatically.
1. Configure your OAuth providers
# config/runtime.exs
config :amur,
base_url: System.get_env("BASE_URL") || "http://localhost:4000",
providers: [
github: [
client_id: System.fetch_env!("GITHUB_CLIENT_ID"),
client_secret: System.fetch_env!("GITHUB_CLIENT_SECRET")
]
],
on_success: &MyAppWeb.AuthController.on_success/2,
on_failure: &MyAppWeb.AuthController.on_failure/2
| Key | Required | Description |
|---|---|---|
base_url |
no | Base URL used to build the redirect_uri (#{base_url}/auth/:provider/callback). Defaults to "". |
providers |
yes | Keyword list of provider configurations. Each key is a provider name, each value is either a keyword list of credentials or a custom provider module. |
on_success |
yes | A {module, function, args} MFA tuple or a function capture of arity 2, called with (conn, %{user: normalized_user, token: token}). |
on_failure |
no | Same format as on_success, called with (conn, reason). Defaults to a redirect to /. |
2. Mount the router
# Phoenix
scope "/auth", alias: false do
pipe_through :browser
forward "/", Amur.Router
end
The alias: false on the scope is required, without it Phoenix rewrites Amur.Router as YourAppWeb.Amur.Router.
Inside a browser pipeline, session and flash helpers are available for your callbacks.
Amur works with Plug.Router too:
# Plug
forward "/auth", to: Amur.Router
The router exposes three endpoints:
| Endpoint | Description |
|---|---|
GET /auth/:provider |
Initiates the OAuth flow |
GET /auth/:provider/callback |
Handles the provider callback |
Amur stores the OAuth handshake params (the state, PKCE verifier, ...) in
the session for the duration of the flow and clears them automatically once
the callback has been handled, no manual cleanup needed.
3. Add an auth controller
Implement the Amur.Callback behaviour so both OAuth result handlers are
present and their arguments are type checked:
defmodule MyAppWeb.AuthController do
@behaviour Amur.Callback
import Plug.Conn
import Phoenix.Controller
@impl true
def on_success(conn, %{user: user}) do
conn
|> put_flash(:info, "Logged in as #{user.email}")
|> redirect(to: "/")
|> halt()
end
@impl true
def on_failure(conn, reason) do
conn
|> put_flash(:error, "Authentication failed")
|> redirect(to: "/")
|> halt()
end
end
The normalized user map has the following shape:
%{
provider: "github", # the provider atom as a string
uid: "12345", # provider-specific user ID
email: "user@example.com",
name: "username",
avatar: "https://..."
}
The public Amur.User.t() type represents this map. The :uid, :email,
:name, and :avatar fields are optional because providers may not return
them, and provider-specific fields may also be present.
Different providers may return different fields. See each provider module's normalize_user/1 for the exact shape.
on_success/2 also receives the OAuth token in the same map. Bind it only
when you need it (for example, to call the provider's API on the user's
behalf); otherwise ignore it by pattern-matching just :user:
def on_success(conn, %{user: user, token: token}) do
conn
|> put_session(:access_token, token["access_token"])
|> redirect(to: "/")
|> halt()
end
The token map's keys depend on the provider's flow: OAuth 2.0 providers use
token["access_token"], while OAuth 1.0 (Twitter) uses token["oauth_token"]
and token["oauth_token_secret"].
License
MIT