2022-09-28 09:54:05 +13:00
|
|
|
defmodule AshAuthentication do
|
2022-11-23 09:09:41 +13:00
|
|
|
import AshAuthentication.Dsl
|
2022-10-25 11:07:07 +13:00
|
|
|
|
2022-09-28 09:54:05 +13:00
|
|
|
@moduledoc """
|
2022-10-25 11:07:07 +13:00
|
|
|
AshAuthentication provides a turn-key authentication solution for folks using
|
|
|
|
[Ash](https://www.ash-hq.org/).
|
|
|
|
|
2022-10-25 20:32:57 +13:00
|
|
|
## Usage
|
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
This package assumes that you have [Ash](https://ash-hq.org/) installed and
|
|
|
|
configured. See the Ash documentation for details.
|
2022-10-25 20:32:57 +13:00
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
Once installed you can easily add support for authentication by configuring
|
|
|
|
the `AshAuthentication` extension on your resource:
|
2022-10-25 20:32:57 +13:00
|
|
|
|
|
|
|
```elixir
|
|
|
|
defmodule MyApp.Accounts.User do
|
|
|
|
use Ash.Resource,
|
2022-11-23 09:09:41 +13:00
|
|
|
extensions: [AshAuthentication]
|
2022-10-25 20:32:57 +13:00
|
|
|
|
|
|
|
attributes do
|
|
|
|
uuid_primary_key :id
|
|
|
|
attribute :email, :ci_string, allow_nil?: false
|
|
|
|
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
|
|
|
|
end
|
|
|
|
|
|
|
|
authentication do
|
|
|
|
api MyApp.Accounts
|
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
strategies do
|
|
|
|
password do
|
|
|
|
identity_field :email
|
|
|
|
hashed_password_field :hashed_password
|
|
|
|
end
|
|
|
|
end
|
2022-10-25 20:32:57 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
identities do
|
|
|
|
identity :unique_email, [:email]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
```
|
|
|
|
|
|
|
|
If you plan on providing authentication via the web, then you will need to
|
2022-12-08 18:39:22 +13:00
|
|
|
define a plug using `AshAuthentication.Plug` which builds a `Plug.Router` that
|
2022-10-25 20:32:57 +13:00
|
|
|
routes incoming authentication requests to the correct provider and provides
|
|
|
|
callbacks for you to manipulate the conn after success or failure.
|
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
If you're using AshAuthentication with Phoenix, then check out
|
|
|
|
[`ash_authentication_phoenix`](https://github.com/team-alembic/ash_authentication_phoenix)
|
|
|
|
which provides route helpers, a controller abstraction and LiveView components
|
|
|
|
for easy set up.
|
2022-10-25 20:32:57 +13:00
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
## Authentication Strategies
|
2022-10-25 20:32:57 +13:00
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
Currently supported strategies:
|
2022-10-25 20:32:57 +13:00
|
|
|
|
2022-12-08 18:39:22 +13:00
|
|
|
1. {{link:ash_authentication:module:AshAuthentication.Strategy.Password}}
|
2022-11-23 09:09:41 +13:00
|
|
|
- authenticate users against your local database using a unique identity
|
|
|
|
(such as username or email address) and a password.
|
2022-12-08 18:39:22 +13:00
|
|
|
2. {{link:ash_authentication:module:AshAuthentication.Strategy.OAuth2}}
|
2022-11-23 09:09:41 +13:00
|
|
|
- authenticate using local or remote [OAuth 2.0](https://oauth.net/2/)
|
|
|
|
compatible services.
|
2022-10-25 20:32:57 +13:00
|
|
|
|
2022-12-05 15:40:44 +13:00
|
|
|
## Add-ons
|
|
|
|
|
|
|
|
Add-ons are like strategies, except that they don't actually provide
|
|
|
|
authentication - they just provide features adjacent to authentication.
|
|
|
|
Current add-ons:
|
|
|
|
|
2022-12-08 18:39:22 +13:00
|
|
|
1. {{link:ash_authentication:module:AshAuthentication.AddOn.Confirmation}}
|
2022-12-05 15:40:44 +13:00
|
|
|
- allows you to force the user to confirm changes using a confirmation
|
|
|
|
token (eg. sending a confirmation email when a new user registers).
|
|
|
|
|
|
|
|
## Supervisor
|
|
|
|
|
|
|
|
Some add-ons or strategies may require processes to be started which manage
|
|
|
|
their state over the lifetime of the application (eg periodically deleting
|
|
|
|
expired token revocations). Because of this you should add
|
|
|
|
`{AshAuthentication.Supervisor, otp_app: :my_app}` to your application's
|
|
|
|
supervision tree. See [the Elixir
|
|
|
|
docs](https://hexdocs.pm/elixir/Application.html#module-the-application-callback-module)
|
|
|
|
for more information.
|
|
|
|
|
2022-11-02 18:18:20 +13:00
|
|
|
## DSL Documentation
|
2022-10-25 11:07:07 +13:00
|
|
|
|
|
|
|
### Index
|
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
#{Spark.Dsl.Extension.doc_index(dsl())}
|
2022-10-25 11:07:07 +13:00
|
|
|
|
|
|
|
### Docs
|
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
#{Spark.Dsl.Extension.doc(dsl())}
|
2022-10-25 11:07:07 +13:00
|
|
|
|
2022-09-28 09:54:05 +13:00
|
|
|
"""
|
2022-11-23 09:09:41 +13:00
|
|
|
alias Ash.{Api, Error.Query.NotFound, Query, Resource}
|
2022-10-25 11:07:07 +13:00
|
|
|
alias AshAuthentication.Info
|
|
|
|
alias Spark.Dsl.Extension
|
|
|
|
|
|
|
|
use Spark.Dsl.Extension,
|
2022-11-23 09:09:41 +13:00
|
|
|
sections: dsl(),
|
|
|
|
transformers: [
|
|
|
|
AshAuthentication.Transformer,
|
2022-12-08 11:50:50 +13:00
|
|
|
AshAuthentication.Verifier,
|
2022-11-23 09:09:41 +13:00
|
|
|
AshAuthentication.Strategy.Password.Transformer,
|
2022-12-08 11:50:50 +13:00
|
|
|
AshAuthentication.Strategy.Password.Verifier,
|
2022-11-23 09:09:41 +13:00
|
|
|
AshAuthentication.Strategy.OAuth2.Transformer,
|
2022-12-08 11:50:50 +13:00
|
|
|
AshAuthentication.Strategy.OAuth2.Verifier,
|
|
|
|
AshAuthentication.AddOn.Confirmation.Transformer,
|
|
|
|
AshAuthentication.AddOn.Confirmation.Verifier
|
2022-11-23 09:09:41 +13:00
|
|
|
]
|
2022-10-25 11:07:07 +13:00
|
|
|
|
|
|
|
require Ash.Query
|
|
|
|
|
|
|
|
@type resource_config :: %{
|
|
|
|
api: module,
|
|
|
|
providers: [module],
|
|
|
|
resource: module,
|
|
|
|
subject_name: atom
|
|
|
|
}
|
|
|
|
|
|
|
|
@type subject :: String.t()
|
2022-09-28 09:54:05 +13:00
|
|
|
|
|
|
|
@doc """
|
2022-10-25 11:07:07 +13:00
|
|
|
Find all resources which support authentication for a given OTP application.
|
2022-09-28 09:54:05 +13:00
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
Returns a list of resource modules.
|
|
|
|
|
|
|
|
## Example
|
|
|
|
|
|
|
|
iex> authenticated_resources(:ash_authentication)
|
|
|
|
[Example.User]
|
2022-09-28 09:54:05 +13:00
|
|
|
|
2022-10-25 11:07:07 +13:00
|
|
|
"""
|
2022-11-23 09:09:41 +13:00
|
|
|
@spec authenticated_resources(atom) :: [Resource.t()]
|
2022-10-25 11:07:07 +13:00
|
|
|
def authenticated_resources(otp_app) do
|
|
|
|
otp_app
|
|
|
|
|> Application.get_env(:ash_apis, [])
|
|
|
|
|> Stream.flat_map(&Api.Info.resources(&1))
|
2022-11-23 09:09:41 +13:00
|
|
|
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|
2022-10-25 11:07:07 +13:00
|
|
|
|> Enum.to_list()
|
|
|
|
end
|
2022-09-28 09:54:05 +13:00
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
@doc """
|
|
|
|
Return a subject string for user.
|
2022-11-02 18:18:20 +13:00
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
This is done by concatenating the resource's subject name with the resource's
|
|
|
|
primary key field(s) to generate a uri-like string.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
iex> build_user(id: "ce7969f9-afa5-474c-bc52-ac23a103cef6") |> user_to_subject()
|
|
|
|
"user?id=ce7969f9-afa5-474c-bc52-ac23a103cef6"
|
2022-11-02 18:18:20 +13:00
|
|
|
|
2022-09-28 09:54:05 +13:00
|
|
|
"""
|
2022-11-23 09:09:41 +13:00
|
|
|
@spec user_to_subject(Resource.record()) :: subject
|
|
|
|
def user_to_subject(record) do
|
2022-10-25 11:07:07 +13:00
|
|
|
subject_name =
|
|
|
|
record.__struct__
|
2022-11-23 09:09:41 +13:00
|
|
|
|> Info.authentication_subject_name!()
|
2022-10-25 11:07:07 +13:00
|
|
|
|
|
|
|
record.__struct__
|
|
|
|
|> Resource.Info.primary_key()
|
|
|
|
|> then(&Map.take(record, &1))
|
|
|
|
|> then(fn primary_key ->
|
|
|
|
"#{subject_name}?#{URI.encode_query(primary_key)}"
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
@doc ~S"""
|
|
|
|
Given a subject string, attempt to retrieve a user record.
|
|
|
|
|
|
|
|
iex> %{id: user_id} = build_user()
|
|
|
|
...> {:ok, %{id: ^user_id}} = subject_to_user("user?id=#{user_id}", Example.User)
|
2022-12-05 13:04:42 +13:00
|
|
|
|
|
|
|
Any options passed will be passed to the underlying `Api.read/2` callback.
|
2022-10-25 11:07:07 +13:00
|
|
|
"""
|
2022-12-05 13:04:42 +13:00
|
|
|
@spec subject_to_user(subject | URI.t(), Resource.t(), keyword) ::
|
2022-10-25 11:07:07 +13:00
|
|
|
{:ok, Resource.record()} | {:error, any}
|
2022-11-23 09:09:41 +13:00
|
|
|
|
2022-12-05 13:04:42 +13:00
|
|
|
def subject_to_user(subject, resource, options \\ [])
|
|
|
|
|
|
|
|
def subject_to_user(subject, resource, options) when is_binary(subject),
|
|
|
|
do: subject |> URI.parse() |> subject_to_user(resource, options)
|
|
|
|
|
|
|
|
def subject_to_user(%URI{path: subject_name, query: primary_key} = _subject, resource, options) do
|
2022-11-23 09:09:41 +13:00
|
|
|
with {:ok, resource_subject_name} <- Info.authentication_subject_name(resource),
|
|
|
|
^subject_name <- to_string(resource_subject_name),
|
|
|
|
{:ok, action_name} <- Info.authentication_get_by_subject_action_name(resource),
|
|
|
|
{:ok, api} <- Info.authentication_api(resource) do
|
2022-10-25 11:07:07 +13:00
|
|
|
primary_key =
|
|
|
|
primary_key
|
|
|
|
|> URI.decode_query()
|
|
|
|
|> Enum.to_list()
|
|
|
|
|
2022-11-23 09:09:41 +13:00
|
|
|
resource
|
2022-10-25 11:07:07 +13:00
|
|
|
|> Query.for_read(action_name, %{})
|
|
|
|
|> Query.filter(^primary_key)
|
2022-12-05 13:04:42 +13:00
|
|
|
|> api.read(options)
|
2022-10-25 11:07:07 +13:00
|
|
|
|> case do
|
2022-10-31 16:43:00 +13:00
|
|
|
{:ok, [user]} -> {:ok, user}
|
2022-11-23 09:09:41 +13:00
|
|
|
_ -> {:error, NotFound.exception([])}
|
2022-10-25 11:07:07 +13:00
|
|
|
end
|
|
|
|
end
|
2022-09-28 09:54:05 +13:00
|
|
|
end
|
|
|
|
end
|