DatadeftAuth (datadeft_auth v0.6.1)
Copy MarkdownFramework-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
@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: :fakeuses the crate-shipped in-memory store/outbox (tests and local development);mode: :awsuses DynamoDB, SES and Secrets Manager through the deployment-role credential chain and requires theawskey.servicecarriesterms_version/privacy_versionplus optional lifetimes,enforce_country,account_display,default_localeandlocales.emailcarrieslink_base_url(the token is appended),subject,textand optionalhtml; 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 takesactive_document(and optionallyprevious_document), the auth secret document JSON strings delivered once over the private pipe. AWS mode takesactive_ref/previous_ref(name_or_arnwithstageorversion_id) so key material never enters BEAM; inline documents are allowed only together with a localendpoint_url.aws(AWS mode only) carriesregion,table,from_email, optionalreply_to, and the test-onlyendpoint_url/fake_outboxpair for DynamoDB Local runs.pow(optional) enables the proof-of-work admission gate:difficulty(leading zero hex characters, 1..64; document your production floor) andproof_budget_per_solve(admissions one solve buys over the proof's lifetime, shared across nodes), plus optionalchallenge_max_age_secs(default 120) andproof_ttl_secs(default 10800). Without this key the pow operations return a configuration error.
@type max_pending() :: 1..1024
Validated bound of the :max_pending 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.
@type options() :: [option()]
Optional transport settings; duplicate and unknown keys are rejected.
@type request_timeout() :: 1..60000
Validated bound of the :request_timeout option, in milliseconds.
@type restart_delay() :: 1..5000
Validated bound of the :restart_delay option, in milliseconds.
@type status_result() :: {:ok, :transport_ready} | {:error, DatadeftAuth.Error.t()}
Transport health only, not authentication or AWS readiness.
Functions
@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.
@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.
@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.
@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.
@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.
@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.
@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.
@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.
@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).
@spec start_link(options()) :: GenServer.on_start()
Starts the transport owner. Readiness is asynchronous and checked with status/1.
@spec status(GenServer.server()) :: status_result()
Checks transport health; a refused initialization returns :configuration until the child is restarted with corrected config.
@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.