mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-20 05:13:10 +12:00
a939dde9b9
This is missing a bunch of features that you probably want to use (eg confirmation, password resets), but it's a pretty good place to put a stake in the sand and say it works.
176 lines
4.7 KiB
Elixir
176 lines
4.7 KiB
Elixir
defmodule AshAuthentication do
|
|
import AshAuthentication.Utils, only: [to_sentence: 2]
|
|
|
|
@authentication %Spark.Dsl.Section{
|
|
name: :authentication,
|
|
describe: "Configure authentication for this resource",
|
|
schema: [
|
|
subject_name: [
|
|
type: :atom,
|
|
doc: """
|
|
The subject name is used in generating token claims and in generating authentication routes.
|
|
"""
|
|
],
|
|
api: [
|
|
type: {:behaviour, Ash.Api},
|
|
doc: """
|
|
The name of the Ash API to use to access this resource when registering/authenticating.
|
|
"""
|
|
],
|
|
get_by_subject_action_name: [
|
|
type: :atom,
|
|
doc: """
|
|
The name of the read action used to retrieve the access when calling `AshAuthentication.subject_to_resource/2`.
|
|
""",
|
|
default: :get_by_subject
|
|
]
|
|
]
|
|
}
|
|
@tokens %Spark.Dsl.Section{
|
|
name: :tokens,
|
|
describe: "Configure JWT settings for this resource",
|
|
schema: [
|
|
enabled?: [
|
|
type: :boolean,
|
|
doc: """
|
|
Should JWTs be generated by this resource?
|
|
"""
|
|
],
|
|
signing_algorithm: [
|
|
type: :string,
|
|
doc: """
|
|
The algorithm to use for token signing.
|
|
|
|
Available signing algorithms are; #{to_sentence(Joken.Signer.algorithms(), final: "and")}.
|
|
"""
|
|
],
|
|
token_lifetime: [
|
|
type: :pos_integer,
|
|
doc: """
|
|
How long a token should be valid, in hours.
|
|
"""
|
|
],
|
|
revocation_resource: [
|
|
type: {:behaviour, Ash.Resource},
|
|
doc: """
|
|
If token generation is enabled for this resource, we need a place to store revocation information.
|
|
This option is the name of an Ash Resource which has the `AshAuthentication.TokenRevocation` extension present.
|
|
"""
|
|
]
|
|
]
|
|
}
|
|
|
|
@moduledoc """
|
|
AshAuthentication
|
|
|
|
AshAuthentication provides a turn-key authentication solution for folks using
|
|
[Ash](https://www.ash-hq.org/).
|
|
|
|
|
|
## Authentication DSL
|
|
|
|
### Index
|
|
|
|
#{Spark.Dsl.Extension.doc_index([@authentication])}
|
|
|
|
### Docs
|
|
|
|
#{Spark.Dsl.Extension.doc([@authentication])}
|
|
|
|
## Token DSL
|
|
|
|
### Index
|
|
|
|
#{Spark.Dsl.Extension.doc_index([@tokens])}
|
|
|
|
### Docs
|
|
|
|
#{Spark.Dsl.Extension.doc([@tokens])}
|
|
|
|
"""
|
|
alias Ash.{Api, Query, Resource}
|
|
alias AshAuthentication.Info
|
|
alias Spark.Dsl.Extension
|
|
|
|
use Spark.Dsl.Extension,
|
|
sections: [@authentication, @tokens],
|
|
transformers: [AshAuthentication.Transformer]
|
|
|
|
require Ash.Query
|
|
|
|
@type resource_config :: %{
|
|
api: module,
|
|
providers: [module],
|
|
resource: module,
|
|
subject_name: atom
|
|
}
|
|
|
|
@type subject :: String.t()
|
|
|
|
@doc """
|
|
Find all resources which support authentication for a given OTP application.
|
|
|
|
Returns a map where the key is the authentication provider, and the values are
|
|
lists of api/resource pairs.
|
|
|
|
This is primarily useful for introspection, but also allows us to simplify
|
|
token lookup.
|
|
"""
|
|
@spec authenticated_resources(atom) :: [resource_config]
|
|
def authenticated_resources(otp_app) do
|
|
otp_app
|
|
|> Application.get_env(:ash_apis, [])
|
|
|> Stream.flat_map(&Api.Info.resources(&1))
|
|
|> Stream.map(&{&1, Extension.get_persisted(&1, :authentication)})
|
|
|> Stream.reject(&(elem(&1, 1) == nil))
|
|
|> Stream.map(fn {resource, config} ->
|
|
Map.put(config, :resource, resource)
|
|
end)
|
|
|> Enum.to_list()
|
|
end
|
|
|
|
@doc """
|
|
Return a subject string for an AshAuthentication resource.
|
|
"""
|
|
@spec resource_to_subject(Resource.record()) :: subject
|
|
def resource_to_subject(record) do
|
|
subject_name =
|
|
record.__struct__
|
|
|> AshAuthentication.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 """
|
|
Given a subject string, attempt to retrieve a resource.
|
|
"""
|
|
@spec subject_to_resource(subject | URI.t(), resource_config) ::
|
|
{:ok, Resource.record()} | {:error, any}
|
|
def subject_to_resource(subject, config) when is_binary(subject),
|
|
do: subject |> URI.parse() |> subject_to_resource(config)
|
|
|
|
def subject_to_resource(%URI{path: subject_name, query: primary_key} = _subject, config)
|
|
when is_map(config) do
|
|
with ^subject_name <- to_string(config.subject_name),
|
|
{:ok, action_name} <- Info.authentication_get_by_subject_action_name(config.resource) do
|
|
primary_key =
|
|
primary_key
|
|
|> URI.decode_query()
|
|
|> Enum.to_list()
|
|
|
|
config.resource
|
|
|> Query.for_read(action_name, %{})
|
|
|> Query.filter(^primary_key)
|
|
|> config.api.read()
|
|
|> case do
|
|
{:ok, [actor]} -> {:ok, actor}
|
|
_ -> {:error, "Invalid subject"}
|
|
end
|
|
end
|
|
end
|
|
end
|