defmodule AshAuthentication.Plug do
@moduledoc ~S"""
Generate an authentication plug.
Use in your app by creating a new module called `AuthPlug` or similar:
```elixir
defmodule MyAppWeb.AuthPlug do
use AshAuthentication.Plug, otp_app: :my_app
def handle_success(conn, _activity, user, _token) do
conn
|> store_in_session(user)
|> send_resp(200, "Welcome back #{user.name}")
end
def handle_failure(conn, _activity, reason) do
conn
|> send_resp(401, "Better luck next time")
end
end
```
### Using in Phoenix
In your Phoenix router you can add it:
```elixir
scope "/auth" do
pipe_through :browser
forward "/", MyAppWeb.AuthPlug
end
```
In order to load any authenticated users for either web or API users you can add the following to your router:
```elixir
import MyAppWeb.AuthPlug
pipeline :session_users do
pipe :load_from_session
end
pipeline :bearer_users do
pipe :load_from_bearer
end
scope "/", MyAppWeb do
pipe_through [:browser, :session_users]
live "/", PageLive, :home
end
scope "/api", MyAppWeb do
pipe_through [:api, :bearer_users]
get "/" ApiController, :index
end
```
### Using in a Plug application
```elixir
use Plug.Router
forward "/auth", to: MyAppWeb.AuthPlug
```
Note that you will need to include a bunch of other plugs in the pipeline to
do useful things like session and query param fetching.
"""
alias Ash.Resource
alias AshAuthentication.Plug.{Defaults, Helpers, Macros}
alias Plug.Conn
require Macros
@type activity :: {atom, atom}
@type token :: String.t()
@doc """
When authentication has been succesful, this callback will be called with the
conn, the successful activity, the authenticated resource and a token.
This allows you to choose what action to take as appropriate for your
application.
The default implementation calls `store_in_session/2` and returns a simple
"Access granted" message to the user. You almost definitely want to override
this behaviour.
"""
@callback handle_success(Conn.t(), activity, Resource.record() | nil, token | nil) :: Conn.t()
@doc """
When there is any failure during authentication this callback is called.
Note that this includes not just authentication failures but potentially
route-not-found errors also.
The default implementation simply returns a 401 status with the message
"Access denied". You almost definitely want to override this.
"""
@callback handle_failure(Conn.t(), activity, any) :: Conn.t()
@doc false
@spec __using__(keyword) :: Macro.t()
defmacro __using__(opts) do
otp_app =
opts
|> Keyword.fetch!(:otp_app)
|> Macro.expand_once(__CALLER__)
quote do
require Macros
Macros.validate_subject_name_uniqueness(unquote(otp_app))
@behaviour AshAuthentication.Plug
@behaviour Plug
import Plug.Conn
defmodule Router do
@moduledoc false
use AshAuthentication.Plug.Router,
otp_app: unquote(otp_app),
return_to:
__MODULE__
|> Module.split()
|> List.delete_at(-1)
|> Module.concat()
end
Macros.define_load_from_session(unquote(otp_app))
Macros.define_load_from_bearer(unquote(otp_app))
Macros.define_revoke_bearer_tokens(unquote(otp_app))
@impl true
defdelegate handle_success(conn, activity, user, token), to: Defaults
@impl true
defdelegate handle_failure(conn, activity, error), to: Defaults
defoverridable handle_success: 4, handle_failure: 3
@impl true
defdelegate init(opts), to: Router
@impl true
defdelegate call(conn, opts), to: Router
defdelegate set_actor(conn, subject_name), to: Helpers
defdelegate store_in_session(conn, user), to: Helpers
end
end
end