Macaw.Auth.Ash (macaw_auth_ash v0.3.1)

Copy Markdown View Source

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/2 runs the configured AshAuthentication strategies in order, as Macaw.Auth.Native runs 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

authenticate(credentials, opts \\ [])

@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.

check_config!()

@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.

session_idle_seconds(opts)

@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.

to_user(record, config \\ Config.get())

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).

verify_second_factor(session_id, code, opts)

Unsupported: {:error, :unsupported}. AshAuthentication 4.x has no TOTP strategy; users never owe a second factor under this backend (see the moduledoc).