Packages
ash_authentication
4.8.3
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/ash_authentication.install.ex
# credo:disable-for-this-file Credo.Check.Design.AliasUsageif Code.ensure_loaded?(Igniter) do defmodule Mix.Tasks.AshAuthentication.Install do @example "mix igniter.install ash_authentication" @shortdoc "Installs AshAuthentication. Invoke with `mix igniter.install ash_authentication`" @moduledoc """ #{@shortdoc} ## Example ```bash #{@example} ``` ## Options * `--accounts` or `-a` - The domain that contains your resources. Defaults to `YourApp.Accounts`. * `--user` or `-u` - The resource that represents a user. Defaults to `<accounts>.User`. * `--token` or `-t` - The resource that represents a token. Defaults to `<accounts>.Token`. * `--auth-strategy` - The strategy or strategies to use for authentication. None by default, can be specified multiple times for more than one strategy. To add after installation, use `mix ash_authentication.add_strategy password` """ use Igniter.Mix.Task @impl Igniter.Mix.Task def info(_argv, _parent) do %Igniter.Mix.Task.Info{ group: :ash, schema: [ accounts: :string, user: :string, token: :string, yes: :boolean, auth_strategy: :csv ], composes: [ "ash_authentication.add_strategy" ], aliases: [ a: :accounts, u: :user, t: :token, y: :yes ] } end @impl Igniter.Mix.Task def igniter(igniter) do options = Keyword.put_new_lazy(igniter.args.options, :accounts, fn -> Igniter.Project.Module.module_name(igniter, "Accounts") end) options = options |> Keyword.put_new_lazy(:user, fn -> Module.concat(options[:accounts], User) end) |> Keyword.put_new_lazy(:token, fn -> Module.concat(options[:accounts], Token) end) |> parse_module_option(:accounts) |> parse_module_option(:user) |> parse_module_option(:token) accounts_domain = options[:accounts] token_resource = options[:token] user_resource = options[:user] secrets_module = Igniter.Project.Module.module_name(igniter, "Secrets") otp_app = Igniter.Project.Application.app_name(igniter) {igniter, resource_args, repo} = data_layer_args(igniter, options) igniter |> Igniter.Project.Formatter.import_dep(:ash_authentication) |> Igniter.Project.Formatter.add_formatter_plugin(Spark.Formatter) |> Igniter.Project.Config.configure("test.exs", :bcrypt_elixir, [:log_rounds], 1) |> Spark.Igniter.prepend_to_section_order( :"Ash.Resource", [:authentication, :tokens] ) |> Igniter.compose_task( "ash.gen.domain", [inspect(accounts_domain), "--ignore-if-exists"] ++ igniter.args.argv_flags ++ resource_args ) |> generate_token_resource(token_resource, igniter.args.argv_flags, resource_args) |> Igniter.Project.Application.add_new_child( {AshAuthentication.Supervisor, otp_app: otp_app}, after: fn _ -> true end ) |> setup_data_layer(repo) |> generate_user_resource( user_resource, igniter.args.argv_flags, resource_args, token_resource, secrets_module, otp_app ) |> Ash.Igniter.codegen("add_authentication_resources") |> add_strategies(options, igniter.args.argv_flags) end defp add_strategies(igniter, options, argv) do case List.wrap(options[:auth_strategy]) do [] -> Igniter.add_notice(igniter, """ Don't forget to add at least one authentication strategy! You can use the task `mix ash_authentication.add_strategy`, or view the docs at https://hexdocs.pm/ash_authentication/get-started.html """) strategies -> Enum.reduce(strategies, igniter, fn strategy, igniter -> Igniter.compose_task(igniter, "ash_authentication.add_strategy", [strategy | argv]) end) end end defp generate_user_resource( igniter, user_resource, argv, resource_args, token_resource, secrets_module, otp_app ) do dev_secret = :crypto.strong_rand_bytes(32) |> Base.encode64(padding: false) |> binary_part(0, 32) test_secret = :crypto.strong_rand_bytes(32) |> Base.encode64(padding: false) |> binary_part(0, 32) runtime_secret = {:code, quote do System.get_env("TOKEN_SIGNING_SECRET") || raise "Missing environment variable `TOKEN_SIGNING_SECRET`!" end} case Igniter.Project.Module.find_module(igniter, user_resource) do {:ok, {igniter, _, _}} -> Igniter.add_notice( igniter, "User resource already exists: #{user_resource}, skipping creation." ) {:error, igniter} -> extensions = cond do Code.ensure_loaded?(AshPostgres.DataLayer) -> "postgres" Code.ensure_loaded?(AshSqlite.DataLayer) -> "sqlite" true -> nil end resource_args = if extensions do resource_args ++ ["--extend", extensions] else resource_args end igniter |> Igniter.compose_task( "ash.gen.resource", [ inspect(user_resource), "--uuid-primary-key", "id", "--default-actions", "read" ] ++ argv ++ resource_args ) end |> Igniter.compose_task( "ash.extend", [inspect(user_resource), "AshAuthentication,Ash.Policy.Authorizer"] ) |> Ash.Resource.Igniter.add_action(user_resource, """ read :get_by_subject do description "Get a user by the subject claim in a JWT" argument :subject, :string, allow_nil?: false get? true prepare AshAuthentication.Preparations.FilterBySubject end """) |> AshAuthentication.Igniter.add_new_add_on( user_resource, nil, :log_out_everywhere, """ log_out_everywhere do apply_on_password_change? true end """ ) |> Ash.Resource.Igniter.add_bypass( user_resource, quote do AshAuthentication.Checks.AshAuthenticationInteraction end, quote do authorize_if always() end ) |> Ash.Resource.Igniter.add_policy( user_resource, quote do always() end, quote do forbid_if always() end ) |> Spark.Igniter.set_option(user_resource, [:authentication, :tokens, :enabled?], true) |> Spark.Igniter.set_option( user_resource, [:authentication, :tokens, :token_resource], token_resource ) |> Spark.Igniter.set_option( user_resource, [:authentication, :tokens, :token_resource], token_resource ) |> Spark.Igniter.set_option( user_resource, [:authentication, :tokens, :signing_secret], secrets_module ) |> Spark.Igniter.set_option( user_resource, [:authentication, :tokens, :store_all_tokens?], true ) |> Spark.Igniter.set_option( user_resource, [:authentication, :tokens, :require_token_presence_for_authentication?], true ) |> Igniter.Project.Config.configure_new( "dev.exs", otp_app, [:token_signing_secret], dev_secret ) |> Igniter.Project.Config.configure_new( "test.exs", otp_app, [:token_signing_secret], test_secret ) |> Igniter.Project.Config.configure_runtime_env( :prod, otp_app, [:token_signing_secret], runtime_secret ) |> AshAuthentication.Igniter.add_secret_from_env( secrets_module, user_resource, [:authentication, :tokens, :signing_secret], :token_signing_secret ) end defp generate_token_resource(igniter, token_resource, _argv, resource_args) do case Igniter.Project.Module.find_module(igniter, token_resource) do {:ok, {igniter, _, _}} -> Igniter.add_notice( igniter, "Token resource already exists: #{token_resource}, skipping creation." ) {:error, igniter} -> extensions = cond do Code.ensure_loaded?(AshPostgres.DataLayer) -> "postgres" Code.ensure_loaded?(AshSqlite.DataLayer) -> "sqlite" true -> nil end resource_args = if extensions do resource_args ++ ["--extend", extensions] else resource_args end igniter |> Igniter.compose_task( "ash.gen.resource", [ inspect(token_resource), "--default-actions", "read" ] ++ resource_args ) end |> Igniter.compose_task("ash.extend", [ inspect(token_resource), "AshAuthentication.TokenResource,Ash.Policy.Authorizer" ]) |> Ash.Resource.Igniter.add_new_attribute(token_resource, :jti, """ attribute :jti, :string do primary_key? true public? true allow_nil? false sensitive? true end """) |> Ash.Resource.Igniter.add_new_attribute(token_resource, :subject, """ attribute :subject, :string do allow_nil? false public? true end """) |> Ash.Resource.Igniter.add_new_attribute(token_resource, :expires_at, """ attribute :expires_at, :utc_datetime do allow_nil? false public? true end """) |> Ash.Resource.Igniter.add_new_attribute(token_resource, :purpose, """ attribute :purpose, :string do allow_nil? false public? true end """) |> Ash.Resource.Igniter.add_new_attribute(token_resource, :extra_data, """ attribute :extra_data, :map do public? true end """) |> Ash.Resource.Igniter.add_new_attribute(token_resource, :created_at, """ create_timestamp :created_at """) |> Ash.Resource.Igniter.add_new_attribute(token_resource, :updated_at, """ update_timestamp :updated_at """) # Consider moving to the extension's `install/5` callback, but we need # to only run it if the resource is being created which we can't # currently tell in that callback |> Ash.Resource.Igniter.add_action(token_resource, """ read :expired do description "Look up all expired tokens." filter expr(expires_at < now()) end """) |> Ash.Resource.Igniter.add_bypass( token_resource, quote do AshAuthentication.Checks.AshAuthenticationInteraction end, quote do description "AshAuthentication can interact with the token resource" authorize_if always() end ) |> Ash.Resource.Igniter.add_policy( token_resource, quote do always() end, quote do description "No one aside from AshAuthentication can interact with the tokens resource." forbid_if always() end ) |> Ash.Resource.Igniter.add_action(token_resource, """ read :get_token do description "Look up a token by JTI or token, and an optional purpose." get? true argument :token, :string, sensitive?: true argument :jti, :string, sensitive?: true argument :purpose, :string, sensitive?: false prepare AshAuthentication.TokenResource.GetTokenPreparation end """) |> Ash.Resource.Igniter.add_action(token_resource, """ action :revoked?, :boolean do description "Returns true if a revocation token is found for the provided token" argument :token, :string, sensitive?: true argument :jti, :string, sensitive?: true run AshAuthentication.TokenResource.IsRevoked end """) |> Ash.Resource.Igniter.add_action(token_resource, """ create :revoke_token do description "Revoke a token. Creates a revocation token corresponding to the provided token." accept [:extra_data] argument :token, :string, allow_nil?: false, sensitive?: true change AshAuthentication.TokenResource.RevokeTokenChange end """) |> Ash.Resource.Igniter.add_action(token_resource, """ create :store_token do description "Stores a token used for the provided purpose." accept [:extra_data, :purpose] argument :token, :string, allow_nil?: false, sensitive?: true change AshAuthentication.TokenResource.StoreTokenChange end """) |> Ash.Resource.Igniter.add_action(token_resource, """ destroy :expunge_expired do description "Deletes expired tokens." change filter(expr(expires_at < now())) end """) |> Ash.Resource.Igniter.add_action(token_resource, """ update :revoke_all_stored_for_subject do description "Revokes all stored tokens for a specific subject." accept [:extra_data] argument :subject, :string, allow_nil?: false, sensitive?: true change AshAuthentication.TokenResource.RevokeAllStoredForSubjectChange end """) end cond do Code.ensure_loaded?(AshPostgres.Igniter) -> def setup_data_layer(igniter, repo) do igniter |> AshPostgres.Igniter.add_postgres_extension(repo, "citext") end def data_layer_args(igniter, opts) do {igniter, repo} = AshPostgres.Igniter.select_repo(igniter, generate?: true, yes: opts[:yes]) {igniter, ["--repo", inspect(repo)], repo} end Code.ensure_loaded?(AshSqlite.Igniter) -> def setup_data_layer(igniter, _repo) do igniter end def data_layer_args(igniter, opts) do {igniter, repo} = AshSqlite.Igniter.select_repo(igniter, generate?: true, yes: opts[:yes]) {igniter, ["--repo", inspect(repo)], repo} end true -> def setup_data_layer(igniter, _), do: igniter def data_layer_args(igniter, _) do {igniter, [], nil} end end defp parse_module_option(opts, option) do Keyword.update(opts, option, nil, fn value -> if is_binary(value) do Igniter.Project.Module.parse(value) else value end end) end endelse defmodule Mix.Tasks.AshAuthentication.Install do use Mix.Task @shortdoc "Installs AshAuthentication. Invoke with `mix igniter.install ash_authentication`" @moduledoc @shortdoc def run(_argv) do Mix.shell().error(""" The task 'ash_authentication.install' requires igniter to be run. Please install igniter and try again. For more information, see: https://hexdocs.pm/igniter """) exit({:shutdown, 1}) end endend