Packages
ash_authentication
4.6.1
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.AliasUsage
if 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
case Igniter.Project.Module.find_module(igniter, user_resource) do
{:ok, {igniter, _, _}} ->
Igniter.add_warning(
igniter,
"User resource already exists: #{user_resource}, skipping creation."
)
{:error, igniter} ->
extensions = "AshAuthentication,Ash.Policy.Authorizer"
extensions =
cond do
Code.ensure_loaded?(AshPostgres.DataLayer) ->
"postgres,#{extensions}"
Code.ensure_loaded?(AshSqlite.DataLayer) ->
"sqlite,#{extensions}"
true ->
extensions
end
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}
igniter
|> Igniter.compose_task(
"ash.gen.resource",
[
inspect(user_resource),
"--uuid-primary-key",
"id",
"--default-actions",
"read",
"--extend",
extensions
] ++ argv ++ resource_args
)
|> 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
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_warning(
igniter,
"Token resource already exists: #{token_resource}, skipping creation."
)
{:error, igniter} ->
extensions = "AshAuthentication.TokenResource,Ash.Policy.Authorizer"
extensions =
cond do
Code.ensure_loaded?(AshPostgres.DataLayer) ->
"postgres,#{extensions}"
Code.ensure_loaded?(AshSqlite.DataLayer) ->
"sqlite,#{extensions}"
true ->
extensions
end
igniter
|> Igniter.compose_task(
"ash.gen.resource",
[
inspect(token_resource),
"--default-actions",
"read",
"--extend",
extensions,
"--attribute",
"jti:string:primary_key:public:required:sensitive",
"--attribute",
"subject:string:required:public",
"--attribute",
"expires_at:utc_datetime:required:public",
"--attribute",
"purpose:string:required:public",
"--attribute",
"extra_data:map:public"
] ++ resource_args
)
|> 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
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
end
else
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
end
end