A file-backed store of user accounts for a served application.
Passwords are kept as PBKDF2-HMAC-SHA256 hashes with a random salt per account;
the file never holds a password. Usernames are unique without regard to case
and are returned in the spelling they were registered with. Each account carries
a props map the application owns.
{:ok, accounts} = Drafter.Accounts.start_link(path: "/var/lib/myapp/accounts.terms")
:ok = Drafter.Accounts.register(accounts, "alice", "correct horse")
{:ok, %{username: "alice", props: %{}}} = Drafter.Accounts.authenticate(accounts, "alice", "correct horse")authenticate/4 costs the same for an unknown name as for a wrong password, and
refuses a peer outright, without hashing, once it has failed five times in
fifteen minutes.
Rules
- a username is 1 to 32 characters from
A-Z,a-z,0-9,_and- - a password is at least 8 characters
- every change is written to the file before the call returns, by writing a sibling file and renaming it over the old one
- the file is text: one Erlang term per account, in registration order, that
:file.consult/1reads —%{username: "alice", number: 0, props: %{…}, hash: <<…>>, salt: <<…>>, iterations: 300000}— so it can be read and searched as it is. It is UTF-8: a binary that is printable text is written as one, any other as its bytes. A file written as:erlang.term_to_binary/1by an earlier version is read and written back as text, as is one an earlier version wrote as Latin-1
Summary
Functions
Check a username and password.
Replace the password, given the current one. :error when it does not match.
Returns a specification to start this module under a supervisor.
The account under username, without checking a password.
Merge props into the account's props. {:error, :unknown} for no such account.
Create an account. Returns :ok, or {:error, reason} with :taken,
:invalid_username or :weak_password.
Start the store, loading path if it exists.
Types
Functions
@spec authenticate(GenServer.server(), username(), String.t(), keyword()) :: {:ok, account()} | :error | {:error, :locked}
Check a username and password.
Returns {:ok, account}, :error for a wrong password or unknown name, or
{:error, :locked} for a peer that has failed too often.
Options
:peer- the caller's address, any term, counted by the throttle. Defaultnil, which is never locked.
@spec change_password(GenServer.server(), username(), String.t(), String.t()) :: :ok | :error | {:error, :weak_password}
Replace the password, given the current one. :error when it does not match.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec fetch(GenServer.server(), username()) :: {:ok, account()} | :error
The account under username, without checking a password.
@spec put_props(GenServer.server(), username(), map()) :: :ok | {:error, :unknown}
Merge props into the account's props. {:error, :unknown} for no such account.
@spec register(GenServer.server(), username(), String.t(), map()) :: :ok | {:error, register_error()}
Create an account. Returns :ok, or {:error, reason} with :taken,
:invalid_username or :weak_password.
@spec start_link(keyword()) :: GenServer.on_start()
Start the store, loading path if it exists.
Options
:path- the file to load and write. Required.:name- a GenServer name. Default: none.:iterations- PBKDF2 rounds for new hashes. Default300_000. An account keeps the count it was hashed with, so raising this later affects only new passwords.:throttle- options forDrafter.Accounts.Throttle.new/1. Default[].:default_props- a function from an account's number (0 for the first registered, then 1, 2, …; accounts from a file without numbers are numbered in name order when loaded) to props every account has unless its own props say otherwise. Default: none.