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