DatadeftAuth (datadeft_auth v0.6.1)

Copy Markdown

Framework-independent authentication using a supervised, separate Rust process.

The transport plus the magic-link/session and PoW APIs are implemented, backed by either the crate-shipped in-memory store (mode: :fake) or DynamoDB/SES with Secrets Manager key loading (mode: :aws). See the README for source installation, tested platforms and application deployment checks. A healthy transport does not mean authentication is configured or available.

Add {DatadeftAuth, name: MyAuth} to your application's supervision tree. The name doubles as the child id, so give each supervised instance a distinct name; unregistered (nil-name) instances share the default id and need hand-written child specs to coexist under one supervisor. Native work never runs inside BEAM. No request is automatically replayed after a timeout or worker failure.

HTTP routes belong to the consuming application. The reference layout uses /auth/magic-link/*, /auth/pow/* and POST /auth/logout; /dev/* is local tooling only. In particular, GET /auth/magic-link/confirm serves a page, while POST /auth/magic-link/consume calls confirm/4. See the README for the full mapping; this module neither mounts routes nor sets HTTP cookies.

Summary

Types

Authentication configuration sent to the executable on every (re)connect. Without one the transport is health-only and every auth call returns a configuration error. The executable validates the content fail-closed

Validated bound of the :max_pending option.

Transport options. A nil name, executable or config selects the default, same as omitting the key. Executable paths and the config are trusted deployment configuration.

Optional transport settings; duplicate and unknown keys are rejected.

Validated bound of the :request_timeout option, in milliseconds.

Validated bound of the :restart_delay option, in milliseconds.

Transport health only, not authentication or AWS readiness.

Functions

Confirms a landing: consumes the link and creates the session atomically. In the reference HTTP layout this is POST /auth/magic-link/consume, never the GET-only /auth/magic-link/confirm page. country is the trusted-edge signal, never client input. On :rejected or :bad_request clear the confirm cookie.

Fake-mode test helper: the newest outbox email's landing URL for an address. Refused outside mode: :fake; never call it in production code.

Reads the flow state bound to a confirm cookie (redirect-landing mode). On usable: false clear the confirm cookie.

Scanner-safe landing for an emailed token: validates without consuming and returns the flow secrets to store client-side. The answer is uniform for every unusable link state; render the same page either way.

Revokes the session server-side. Clear the session cookie only after {:ok, %{revoked: true}} or revoked: false (already invalid); on :unavailable keep it and retry. Protect the calling route against CSRF.

Admits one gated request by its proof cookie, spending one unit of the solve's shared lifetime budget. admitted: false (an unusable proof or an exhausted budget) means answer "solve a new challenge"; a failing budget store is {:error, %Error{code: :unavailable}} and must refuse the request — never admit on failure.

Mints a proof-of-work challenge for the application's POST /auth/pow/create route. Serialize the struct as JSON for the shipping browser client verbatim. Requires the pow configuration key.

Verifies a posted solution (the client's {chg, sol, non, tim, tag} JSON as a map). On admitted: true set the returned proof_cookie as the dd_pow cookie (HttpOnly, Secure, the configured proof TTL). On admitted: false answer with one generic "solve a new challenge" response whatever check failed.

Requests a magic-link email. The public response to the requester must stay the same generic acceptance whatever the returned reason label says; the label (for example "magic_link_request_sent") is for internal logs and metrics only and never carries identifiers.

Starts the transport owner. Readiness is asynchronous and checked with status/1.

Checks transport health; a refused initialization returns :configuration until the child is restarted with corrected config.

Validates a session cookie (read-only) and re-issues it when due. Send a returned refreshed_cookie with the normal session cookie settings on a no-store response. Refresh is best-effort: an unavailable mint key or entropy failure does not invalidate an already verified session; its existing idle/absolute expiry still applies. valid: false means sign in again; clear the cookie.

Types

config()

@type config() :: %{
  :mode => :fake | :aws,
  :service => map(),
  :email => map(),
  :secrets => map(),
  optional(:aws) => map(),
  optional(:pow) => map()
}

Authentication configuration sent to the executable on every (re)connect. Without one the transport is health-only and every auth call returns a configuration error. The executable validates the content fail-closed:

  • mode: :fake uses the crate-shipped in-memory store/outbox (tests and local development); mode: :aws uses DynamoDB, SES and Secrets Manager through the deployment-role credential chain and requires the aws key.
  • service carries terms_version/privacy_version plus optional lifetimes, enforce_country, account_display, default_locale and locales.
  • email carries link_base_url (the token is appended), subject, text and optional html; bodies must contain {magic_link_url} and the subject must not (Subject headers are routinely logged in transit, and the URL carries the bearer token). The reference landing URL ends in /auth/magic-link/landing?token=; redirect mode uses /auth/magic-link/open?token=. The destination stays application-configured.
  • secrets: fake mode takes active_document (and optionally previous_document), the auth secret document JSON strings delivered once over the private pipe. AWS mode takes active_ref/previous_ref (name_or_arn with stage or version_id) so key material never enters BEAM; inline documents are allowed only together with a local endpoint_url.
  • aws (AWS mode only) carries region, table, from_email, optional reply_to, and the test-only endpoint_url/fake_outbox pair for DynamoDB Local runs.
  • pow (optional) enables the proof-of-work admission gate: difficulty (leading zero hex characters, 1..64; document your production floor) and proof_budget_per_solve (admissions one solve buys over the proof's lifetime, shared across nodes), plus optional challenge_max_age_secs (default 120) and proof_ttl_secs (default 10800). Without this key the pow operations return a configuration error.

max_pending()

@type max_pending() :: 1..1024

Validated bound of the :max_pending option.

option()

@type option() ::
  {:name, atom()}
  | {:executable, String.t() | nil}
  | {:config, config() | nil}
  | {:request_timeout, request_timeout()}
  | {:max_pending, max_pending()}
  | {:restart_delay, restart_delay()}

Transport options. A nil name, executable or config selects the default, same as omitting the key. Executable paths and the config are trusted deployment configuration.

options()

@type options() :: [option()]

Optional transport settings; duplicate and unknown keys are rejected.

request_timeout()

@type request_timeout() :: 1..60000

Validated bound of the :request_timeout option, in milliseconds.

restart_delay()

@type restart_delay() :: 1..5000

Validated bound of the :restart_delay option, in milliseconds.

status_result()

@type status_result() :: {:ok, :transport_ready} | {:error, DatadeftAuth.Error.t()}

Transport health only, not authentication or AWS readiness.

Functions

confirm(server, confirm_cookie, confirmation, opts \\ [])

@spec confirm(GenServer.server(), String.t(), String.t(), keyword()) ::
  {:ok, DatadeftAuth.Session.t()} | {:error, DatadeftAuth.Error.t()}

Confirms a landing: consumes the link and creates the session atomically. In the reference HTTP layout this is POST /auth/magic-link/consume, never the GET-only /auth/magic-link/confirm page. country is the trusted-edge signal, never client input. On :rejected or :bad_request clear the confirm cookie.

On :unavailable, :timeout, :protocol or worker failure the outcome is indeterminate by contract, not failed: the reply may have been lost after the session committed and the one-time link was consumed. The library never replays the commit and keeps no durable commit plan. Offer the user a fresh sign-in link; one repeated confirm with the same inputs is harmless but answers :rejected when the first attempt had committed. A session committed behind a lost reply holds no issued cookie, idles out on its own and is revocable server-side.

fake_last_magic_link(server, email)

@spec fake_last_magic_link(GenServer.server(), String.t()) ::
  {:ok, DatadeftAuth.DevMagicLink.t()} | {:error, DatadeftAuth.Error.t()}

Fake-mode test helper: the newest outbox email's landing URL for an address. Refused outside mode: :fake; never call it in production code.

flow_state(server, confirm_cookie)

@spec flow_state(GenServer.server(), String.t()) ::
  {:ok, DatadeftAuth.FlowState.t()} | {:error, DatadeftAuth.Error.t()}

Reads the flow state bound to a confirm cookie (redirect-landing mode). On usable: false clear the confirm cookie.

landing(server, token)

@spec landing(GenServer.server(), String.t()) ::
  {:ok, DatadeftAuth.Landing.t()} | {:error, DatadeftAuth.Error.t()}

Scanner-safe landing for an emailed token: validates without consuming and returns the flow secrets to store client-side. The answer is uniform for every unusable link state; render the same page either way.

logout(server, session_cookie, opts \\ [])

@spec logout(GenServer.server(), String.t(), keyword()) ::
  {:ok, %{revoked: boolean()}} | {:error, DatadeftAuth.Error.t()}

Revokes the session server-side. Clear the session cookie only after {:ok, %{revoked: true}} or revoked: false (already invalid); on :unavailable keep it and retry. Protect the calling route against CSRF.

Pass the same country trusted-edge signal you pass to validate_session/3: logout validates the session before revoking it, so a missing or mismatched signal on a country-pinned session answers revoked: false while the server-side session stays live.

pow_admit(server, proof_cookie)

@spec pow_admit(GenServer.server(), String.t()) ::
  {:ok, %{admitted: boolean()}} | {:error, DatadeftAuth.Error.t()}

Admits one gated request by its proof cookie, spending one unit of the solve's shared lifetime budget. admitted: false (an unusable proof or an exhausted budget) means answer "solve a new challenge"; a failing budget store is {:error, %Error{code: :unavailable}} and must refuse the request — never admit on failure.

pow_challenge(server)

@spec pow_challenge(GenServer.server()) ::
  {:ok, DatadeftAuth.PowChallenge.t()} | {:error, DatadeftAuth.Error.t()}

Mints a proof-of-work challenge for the application's POST /auth/pow/create route. Serialize the struct as JSON for the shipping browser client verbatim. Requires the pow configuration key.

pow_verify(server, solution)

@spec pow_verify(GenServer.server(), map()) ::
  {:ok, DatadeftAuth.PowVerification.t()} | {:error, DatadeftAuth.Error.t()}

Verifies a posted solution (the client's {chg, sol, non, tim, tag} JSON as a map). On admitted: true set the returned proof_cookie as the dd_pow cookie (HttpOnly, Secure, the configured proof TTL). On admitted: false answer with one generic "solve a new challenge" response whatever check failed.

request_link(server, email, opts \\ [])

@spec request_link(GenServer.server(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, DatadeftAuth.Error.t()}

Requests a magic-link email. The public response to the requester must stay the same generic acceptance whatever the returned reason label says; the label (for example "magic_link_request_sent") is for internal logs and metrics only and never carries identifiers.

Options: terms_accepted, privacy_accepted (both default false; consent is the application's statement, not a default) and locale (a hint resolved against the configured allowlist).

start_link(opts \\ [])

@spec start_link(options()) :: GenServer.on_start()

Starts the transport owner. Readiness is asynchronous and checked with status/1.

status(server)

@spec status(GenServer.server()) :: status_result()

Checks transport health; a refused initialization returns :configuration until the child is restarted with corrected config.

validate_session(server, session_cookie, opts \\ [])

@spec validate_session(GenServer.server(), String.t(), keyword()) ::
  {:ok, DatadeftAuth.Validation.t()} | {:error, DatadeftAuth.Error.t()}

Validates a session cookie (read-only) and re-issues it when due. Send a returned refreshed_cookie with the normal session cookie settings on a no-store response. Refresh is best-effort: an unavailable mint key or entropy failure does not invalidate an already verified session; its existing idle/absolute expiry still applies. valid: false means sign in again; clear the cookie.