Drafter.Accounts (drafter v0.4.0)

Copy Markdown View Source

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/1 reads — %{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/1 by an earlier version is read and written back as text, as is one an earlier version wrote as Latin-1

Summary

Functions

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

account()

@type account() :: %{username: username(), props: map()}

register_error()

@type register_error() :: :taken | :invalid_username | :weak_password

username()

@type username() :: String.t()

Functions

authenticate(server, username, password, opts \\ [])

@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. Default nil, which is never locked.

change_password(server, username, current, new_password)

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

fetch(server, username)

@spec fetch(GenServer.server(), username()) :: {:ok, account()} | :error

The account under username, without checking a password.

put_props(server, username, props)

@spec put_props(GenServer.server(), username(), map()) :: :ok | {:error, :unknown}

Merge props into the account's props. {:error, :unknown} for no such account.

register(server, username, password, props \\ %{})

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

start_link(opts)

@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. Default 300_000. An account keeps the count it was hashed with, so raising this later affects only new passwords.
  • :throttle - options for Drafter.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.