ash_authentication/lib/ash_authentication.ex

180 lines
5.3 KiB
Elixir
Raw Normal View History

2022-09-28 09:54:05 +13:00
defmodule AshAuthentication do
import AshAuthentication.Dsl
2022-09-28 09:54:05 +13:00
@moduledoc """
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
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
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,
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
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
define a plug using
[`AshAuthentication.Plug`](https://team-alembic.github.io/ash_authentication/AshAuthentication.Plug.html)
which builds a [`Plug.Router`](https://hexdocs.pm/plug/Plug.Router.html) 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.
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
## Authentication Strategies
2022-10-25 20:32:57 +13:00
Currently supported strategies:
2022-10-25 20:32:57 +13:00
1. [`AshAuthentication.Strategy.Password`](https://team-alembic.github.io/ash_authentication/AshAuthentication.Strategy.Password.html)
- authenticate users against your local database using a unique identity
(such as username or email address) and a password.
2. [`AshAuthentication.Strategy.OAuth2`](https://team-alembic.github.io/ash_authentication/AshAuthentication.Strategy.OAuth2.html)
- authenticate using local or remote [OAuth 2.0](https://oauth.net/2/)
compatible services.
2022-10-25 20:32:57 +13:00
## DSL Documentation
### Index
#{Spark.Dsl.Extension.doc_index(dsl())}
### Docs
#{Spark.Dsl.Extension.doc(dsl())}
2022-09-28 09:54:05 +13:00
"""
alias Ash.{Api, Error.Query.NotFound, Query, Resource}
alias AshAuthentication.Info
alias Spark.Dsl.Extension
use Spark.Dsl.Extension,
sections: dsl(),
transformers: [
AshAuthentication.Transformer,
AshAuthentication.Strategy.Password.Transformer,
AshAuthentication.Strategy.OAuth2.Transformer,
AshAuthentication.AddOn.Confirmation.Transformer
]
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 """
Find all resources which support authentication for a given OTP application.
2022-09-28 09:54:05 +13:00
Returns a list of resource modules.
## Example
iex> authenticated_resources(:ash_authentication)
[Example.User]
2022-09-28 09:54:05 +13:00
"""
@spec authenticated_resources(atom) :: [Resource.t()]
def authenticated_resources(otp_app) do
otp_app
|> Application.get_env(:ash_apis, [])
|> Stream.flat_map(&Api.Info.resources(&1))
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|> Enum.to_list()
end
2022-09-28 09:54:05 +13:00
@doc """
Return a subject string for user.
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-09-28 09:54:05 +13:00
"""
@spec user_to_subject(Resource.record()) :: subject
def user_to_subject(record) do
subject_name =
record.__struct__
|> Info.authentication_subject_name!()
record.__struct__
|> Resource.Info.primary_key()
|> then(&Map.take(record, &1))
|> then(fn primary_key ->
"#{subject_name}?#{URI.encode_query(primary_key)}"
end)
end
@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)
"""
@spec subject_to_user(subject | URI.t(), Resource.t()) ::
{:ok, Resource.record()} | {:error, any}
def subject_to_user(subject, resource) when is_binary(subject),
do: subject |> URI.parse() |> subject_to_user(resource)
def subject_to_user(%URI{path: subject_name, query: primary_key} = _subject, resource) do
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
primary_key =
primary_key
|> URI.decode_query()
|> Enum.to_list()
resource
|> Query.for_read(action_name, %{})
|> Query.filter(^primary_key)
|> api.read()
|> case do
{:ok, [user]} -> {:ok, user}
_ -> {:error, NotFound.exception([])}
end
end
2022-09-28 09:54:05 +13:00
end
end