A Macaw.Auth backend over AshAuthentication: a host's Ash user and
token resources behind the contract, so an Ash host gets Macaw's plugs,
LiveView hooks, steps and revocation push without a second user table.
config :macaw_auth, backend: Macaw.Auth.Ash
config :macaw_auth, Macaw.Auth.Ash,
user_resource: MyApp.Accounts.User,
strategies: [:password],
roles_from: :roles,
disabled_field: :disabled_at(Macaw.Auth.Ash.Config has every key.) The token resource takes
Macaw.Auth.Ash.TokenResource and Macaw.Auth.Ash.Notifier; a
directory sign-in is the Macaw.Auth.Ash.Strategy.Ldap strategy on the
user resource.
- Who someone is:
authenticate/2runs the configured AshAuthentication strategies in order, asMacaw.Auth.Nativeruns its sources: the first that knows the user decides; failures back off by macaw_auth's native throttle. - Their sessions: stored user tokens in the token resource
(
Macaw.Auth.Ash.Sessions, which says why).
The second factor is unsupported. AshAuthentication 4.x, the stable
line this backend targets, has no TOTP strategy (5.0, a release
candidate as this is written, adds one). So every user reports
second_factor: :none, no session owes the step, and
verify_second_factor/3 refuses with {:error, :unsupported}. The
conformance kit runs with its second-factor block pending, saying why,
rather than passed by a stub. A host that needs a second factor today
uses Macaw.Auth.Native.
Summary
Functions
Checks %{username: _, password: _} against the configured strategies,
in order: {:ok, %Macaw.Auth.User{}}, or {:error, :invalid_credentials | :disabled | :throttled}. Options: :remote_ip (a string or an
address tuple; the client's, for the throttle's address key), :now (a
DateTime, for the throttle's clock).
Checks the whole configuration (Macaw.Auth.Ash.Config.get/0: the
resources, the strategies and every LDAP strategy's fields, the
notifier, idle_days against the token lifetime): :ok, or
ArgumentError naming the fault. macaw_auth's application calls it at
start, so a misconfigured host refuses boot rather than failing its
first sign-in.
The idle limit fetch_session/2 expires sessions by: idle_days in
seconds, which is also the remember cookie's lifetime.
A user record as the contract's %Macaw.Auth.User{}: the username from
username_field, roles from roles_from, the source from
source_field (else "local"), disabled from disabled_field (true,
or any timestamp), and no second factor (see the moduledoc).
Unsupported: {:error, :unsupported}. AshAuthentication 4.x has no TOTP
strategy; users never owe a second factor under this backend (see the
moduledoc).
Functions
@spec authenticate(map(), keyword()) :: {:ok, Macaw.Auth.User.t()} | {:error, :invalid_credentials | :disabled | :throttled}
Checks %{username: _, password: _} against the configured strategies,
in order: {:ok, %Macaw.Auth.User{}}, or {:error, :invalid_credentials | :disabled | :throttled}. Options: :remote_ip (a string or an
address tuple; the client's, for the throttle's address key), :now (a
DateTime, for the throttle's clock).
The first strategy that knows the user decides. A password
strategy knows a user who has a row with a password hash; an LDAP
strategy, one its directory knows (Macaw.Auth.Native.Source.Ldap.knows?/2).
A refusal by a strategy that knows the user is final: a wrong password
never falls through to the next strategy, or one account's password
could be tried against another's. One that doesn't passes the attempt
on.
One refusal. A wrong password and an unknown user are both
:invalid_credentials, and take the same time: AshAuthentication's
password sign-in hashes against nothing when no user matches
(hash_provider.simulate/0, bcrypt's no_user_verify), as for a wrong
password it hashes against the user's; an empty username or password,
which never reaches the sign-in query, is hashed against nothing here.
A disabled user (disabled_field) is refused with :disabled, only
after the right password, so the refusal tells a guesser nothing.
Throttled as the native backend is, by its throttle
(Macaw.Auth.Native.Throttle, which macaw_auth's application starts
whichever backend is configured): failures back off per username
(trimmed and case-folded; an empty one shares one key) and per
:remote_ip. While either waits, the attempt is refused :throttled
without checking the password, and not counted, but a hash's time is
still spent (as for an empty field), so the response time doesn't tell
a guesser when the block starts or ends. Every other refusal counts as
a failure; a success resets the username's count, never the address's.
Emits [:macaw_auth, :ash, :authenticate] with %{count: 1} and
%{result: :ok | :error, reason: term, strategy: name | nil, user_id: id | nil}; never the username or password.
@spec check_config!() :: :ok
Checks the whole configuration (Macaw.Auth.Ash.Config.get/0: the
resources, the strategies and every LDAP strategy's fields, the
notifier, idle_days against the token lifetime): :ok, or
ArgumentError naming the fault. macaw_auth's application calls it at
start, so a misconfigured host refuses boot rather than failing its
first sign-in.
@spec session_idle_seconds(keyword()) :: pos_integer()
The idle limit fetch_session/2 expires sessions by: idle_days in
seconds, which is also the remember cookie's lifetime.
@spec to_user(struct(), Macaw.Auth.Ash.Config.t()) :: Macaw.Auth.User.t()
A user record as the contract's %Macaw.Auth.User{}: the username from
username_field, roles from roles_from, the source from
source_field (else "local"), disabled from disabled_field (true,
or any timestamp), and no second factor (see the moduledoc).
Unsupported: {:error, :unsupported}. AshAuthentication 4.x has no TOTP
strategy; users never owe a second factor under this backend (see the
moduledoc).