VBT.Accounts (vbt v0.1.0) View Source

Helper functions for account management.

This module provides helper functions for typical account management functions, such as registration, authentication, and password change.

To use these functions, you need create required database tables and Ecto schemas, and provide accounts configuration.

Database and Ecto

Two database tables are required: one which holds the accounts, and another for managing one-time account tokens.

The account tables must contain a login field and a password hash field. Both fields should be of type strings, and set to null: false. A unique constraint should exist on the login field.

There are no rules for the names of the accounts table and these required fields. For example, the table can be named users, and the login field can be named email. Finally, this table can contain arbitrary additional data (e.g. first and last name)

The tokens table can bear arbitrary name, but the list of fields and their names is more restrictive. You can create this table with the following migration:

create table(:tokens, primary_key: false) do
  add :id, :uuid, primary_key: true
  add :hash, :binary, null: false
  add :type, :string, null: false
  add :used_at, :utc_datetime
  add :expires_at, :utc_datetime, null: false
  add :account_id, references(:accounts, type: :uuid), null: true
end

Note that account_id must be made nullable. The reason is that we're inserting tokens even if the account is not existing, which prevents enumeration attacks.

The Ecto schemas should mirror the database structure of these tables. Most importantly, the accounts schema should specify has_many :tokens, while the tokens schema should specify a corresponding belong_to association.

Configuration

With tables, and schemas in place, you need to define accounts configuration. It is advised to do this by defining a private function accounts_config/0 in the context module where you're implementing accounts operations:

defmodule MyProject.SomeContext do
  # ...

  defp accounts_config() do
    %{
      repo: MyProject.Repo,
      schemas: %{
        account: MyProject.Schemas.Account,
        token: MyProject.Schemas.Token
      },
      login_field: :email,
      password_hash_field: :password_hash,
      min_password_length: 6
    }
  end
end

Usage

To implement account operations, define corresponding functions in the same context module. For example:

defmodule MyProject.SomeContext do
  def create_account(params) do
    %Account{}
    # additional client fields
    |> cast(account_params, ~w(first_name last_name)a)
    |> validate_required(~w(first_name last_name)a)
    # invocation of the generic function
    |> Accounts.create(account_params.email, account_params.password, accounts_config())
  end

  # ...
end

See documentation of individual functions, as well as VBT.Accounts.Token for details.

Tokens cleanup

By default, token entries are not removed from the database. To periodically remove them, you need to start the cleanup process. See VBT.Accounts.Token.Cleanup for details.

Link to this section Summary

Functions

Authenticates the given account, returning the account record from the database.

Changes the account password in the database.

Creates a new account.

Resets the password for the given login and token.

Changes the account password in the database without checking the current password.

Creates a one-time password reset token for the given user.

Link to this section Types

Specs

config() :: %{
  repo: module(),
  schemas: %{account: module(), token: module()},
  login_field: atom(),
  password_hash_field: atom(),
  min_password_length: pos_integer()
}

Specs

Link to this section Functions

Link to this function

authenticate(login, password, config)

View Source

Specs

authenticate(String.t(), String.t(), config()) ::
  {:ok, Ecto.Schema.t()} | {:error, :invalid}

Authenticates the given account, returning the account record from the database.

Link to this function

change_password(account, current_password, new_password, config)

View Source

Specs

change_password(Ecto.Schema.t(), String.t(), String.t(), config()) ::
  {:ok, Ecto.Schema.t()} | {:error, :invalid | Ecto.Changeset.t()}

Changes the account password in the database.

The password will be changed only if the correct current password is supplied.

Link to this function

create(data, login, password, config)

View Source

Specs

create(data(), String.t(), String.t(), config()) ::
  {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}

Creates a new account.

Notice that his function accepts either an Ecto schema or a changeset. In case you need to populate some additional fields (e.g. first/last name), you can create a changeset with corresponding changes, and with desired validations. However, this changeset shouldn't contain login/password changes and validations. Those will be included internally by this function.

If all validations succeed, the account data will be inserted into the database.

This function validates the uniqueness of the login. To do that, the function expects that a corresponding unique constraint is defined in the database.

Link to this function

reset_password(token, new_password, config)

View Source

Specs

reset_password(VBT.Accounts.Token.encoded(), String.t(), config()) ::
  {:ok, Ecto.Schema.t()} | {:error, :invalid | Ecto.Changeset.t()}

Resets the password for the given login and token.

The password is changed only if the token is valid. The token is valid if:

  • it has been created with start_password_reset/3
  • it corresponds to an existing user
  • it hasn't expired
  • it is a password reset token
  • it hasn't been used
Link to this function

set_password(account, new_password, config)

View Source

Specs

set_password(Ecto.Schema.t(), String.t(), config()) ::
  {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}

Changes the account password in the database without checking the current password.

Warning

Be careful when using this function because you could end up creating security issues, such as allowing attackers to change the password of another user. In almost all cases it's prefered to use change_password/4 or start_password_reset/3. Use this function only when the requirements explicitly state that the user should be able to change their password without providing the current one.

Link to this function

start_password_reset(login, max_age, config)

View Source

Specs

Creates a one-time password reset token for the given user.

The token will be valid for the max_age seconds.

If at some later point you want to verify if a token represents a valid and unused password reset token, you can invoke Token.get_account/3, passing "password_reset" as the expected type.

This function always succeeds. If the account for the given login doesn't exist, the token will still be generated. However, this token can't be actually used. This approach is chosen to prevent user enumeration attack.