Packages
ash_authentication
5.0.0-rc.10
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.
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/ash_authentication.add_strategy.dynamic_oidc.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
# credo:disable-for-this-file Credo.Check.Design.AliasUsage
if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.AshAuthentication.AddStrategy.DynamicOidc do
use Igniter.Mix.Task
@example "mix ash_authentication.add_strategy.dynamic_oidc"
@shortdoc "Adds a data-driven OIDC strategy + OidcConnection resource"
@moduledoc """
#{@shortdoc}
Generates a `OidcConnection` resource (in your accounts namespace) for
storing per-tenant/per-customer OIDC client configuration, then wires a
`dynamic_oidc :sso` strategy into your user resource that looks up the
connection at request time.
The shape of the generated connection resource includes `base_url`,
`client_id`, `client_secret` (sensitive), `display_name`, and `icon_url`
string attributes plus a default bypass policy for AshAuthentication
interactions. You're expected to add multitenancy and any custom
write-side policies yourself.
See the `AshAuthentication.Strategy.DynamicOidc` and
`AshAuthentication.OidcConnection` moduledocs for runtime details.
## Example
```bash
#{@example}
```
## Options
* `--user`, `-u` — The user resource. Defaults to `YourApp.Accounts.User`.
* `--accounts`, `-a` — The accounts domain. Defaults to `YourApp.Accounts`.
* `--identity-field`, `-i` — The user attribute used to identify users.
Defaults to `email`.
* `--connection`, `-c` — The OidcConnection resource name. Defaults to
`<accounts>.OidcConnection`.
* `--name`, `-n` — The strategy name. Defaults to `sso`.
"""
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :ash,
example: @example,
extra_args?: false,
only: nil,
positional: [],
composes: [],
schema: [
accounts: :string,
user: :string,
identity_field: :string,
connection: :string,
name: :string
],
aliases: [a: :accounts, u: :user, i: :identity_field, c: :connection, n: :name],
defaults: [identity_field: "email", name: "sso"]
}
end
# sobelow_skip ["DOS.BinToAtom"]
def igniter(igniter) do
options = parse_options(igniter)
strategy_name = String.to_atom(options[:name])
case Igniter.Project.Module.module_exists(igniter, options[:user]) do
{true, igniter} ->
identity_resource =
Module.concat(AshAuthentication.Igniter.parent_module(options[:user]), UserIdentity)
connection_resource = connection_module(options)
igniter
|> Ash.Resource.Igniter.add_new_attribute(options[:user], options[:identity_field], """
attribute #{inspect(options[:identity_field])}, :ci_string do
allow_nil? false
public? true
end
""")
|> AshAuthentication.Igniter.ensure_identity(options[:user], options[:identity_field])
|> AshAuthentication.Igniter.ensure_user_identity_resource(
options[:user],
identity_resource
)
|> generate_connection_resource(connection_resource, options)
|> AshAuthentication.Igniter.add_oauth_register_action(
options[:user],
strategy_name,
identity_field: options[:identity_field],
identity_resource: identity_resource,
identity_change: AshAuthentication.Strategy.DynamicOidc.IdentityChange
)
|> AshAuthentication.Igniter.add_new_strategy(
options[:user],
:dynamic_oidc,
strategy_name,
"""
dynamic_oidc :#{strategy_name} do
connection_resource #{inspect(connection_resource)}
identity_resource #{inspect(identity_resource)}
redirect_uri "http://localhost:4000/auth"
end
"""
)
|> AshAuthentication.Igniter.codegen_for_strategy(strategy_name)
|> Igniter.add_notice("""
Dynamic OIDC strategy "#{strategy_name}" setup:
1. Populate #{inspect(connection_resource)} with one row per
customer / tenant. Each row needs `base_url`, `client_id`, and
`client_secret` from that customer's OIDC IdP (Okta, Entra ID,
Auth0, etc.). Optionally set `display_name` and `icon_url` for
the sign-in UI.
2. Each customer's IdP admin should register the following callback
URL in their app integration:
http://your-app.example/auth/user/#{strategy_name}/callback
3. Set Ash tenant upstream of the auth router (e.g. from subdomain
in a Phoenix plug) so the strategy scopes connection lookups
correctly.
Encrypt `client_secret` at rest in production — see `ash_cloak`.
""")
{false, igniter} ->
Igniter.add_issue(igniter, """
User module #{inspect(options[:user])} was not found.
Perhaps you have not yet installed ash_authentication?
""")
end
end
defp parse_options(igniter) do
options =
igniter.args.options
|> Keyword.put_new_lazy(:accounts, fn ->
Igniter.Project.Module.module_name(igniter, "Accounts")
end)
options
|> Keyword.put_new_lazy(:user, fn -> Module.concat(options[:accounts], User) end)
|> Keyword.update(:identity_field, :email, &String.to_atom/1)
|> Keyword.update!(:accounts, &AshAuthentication.Igniter.maybe_parse_module/1)
|> Keyword.update!(:user, &AshAuthentication.Igniter.maybe_parse_module/1)
end
defp connection_module(options) do
if connection = options[:connection] do
Igniter.Project.Module.parse(connection)
else
Module.concat(options[:accounts], OidcConnection)
end
end
defp generate_connection_resource(igniter, connection_resource, options) do
{exists?, igniter} = Igniter.Project.Module.module_exists(igniter, connection_resource)
if exists? do
Igniter.add_issue(
igniter,
"""
OidcConnection resource already exists: #{inspect(connection_resource)}.
Pass `--connection` with a different module name, or remove the existing one.
"""
)
else
extensions =
cond do
Code.ensure_loaded?(AshPostgres.DataLayer) ->
"Ash.Policy.Authorizer,postgres"
Code.ensure_loaded?(AshSqlite.DataLayer) ->
"Ash.Policy.Authorizer,sqlite"
true ->
"Ash.Policy.Authorizer"
end
igniter
|> Igniter.compose_task("ash.gen.resource", [
inspect(connection_resource),
"--default-actions",
"create,read,update,destroy",
"--extend",
extensions
])
|> Igniter.Project.Module.find_and_update_module!(connection_resource, fn zipper ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
use Ash.Resource,
extensions: [AshAuthentication.OidcConnection]
oidc_connection do
domain #{inspect(options[:accounts])}
end
""")}
end)
|> Ash.Resource.Igniter.add_bypass(
connection_resource,
quote do
AshAuthentication.Checks.AshAuthenticationInteraction
end,
quote do
authorize_if always()
end
)
end
end
end
else
defmodule Mix.Tasks.AshAuthentication.AddStrategy.DynamicOidc do
@shortdoc "Adds a data-driven OIDC strategy + OidcConnection resource"
@moduledoc @shortdoc
use Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'ash_authentication.add_strategy.dynamic_oidc' 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