authority v0.3.0 Authority.Authentication behaviour

A behaviour for looking up users and authenticating credentials.

Implementation

Simply use Authority.Authentication in your module:

defmodule MyApp.Accounts do
  use Authority.Authentication
end

This will define a MyApp.Accounts.authenticate/2 function, which will execute the following callback functions in this order:

  • before_identify/1
  • identify/1
  • before_validate/2
  • validate/3
  • after_validate/2

If any of the callbacks return an {:error, term} response, the failed/2 callback will execute. The __using__/1 macro provides default, empty implementations of all the callbacks except validate/3, which you must implement yourself.

All the default callback implementations can be overriden.

Execution Flow

If you call authenticate/2 with an {email, password} tuple, the callbacks will proceed as follows:

  MyApp.Accounts.authenticate({"test@example.com", "password"})

  # Will execute...
  before_identify("test@example.com")

  # This should return a user matching the email address
  identify("test@example.com")

  # If you need to do any checks on the user prior to validating
  # the password, do them here
  before_validate(user, :any)

  # Verify that the given password is valid for the given user
  validate("password", user, :any)

  # Do any cleanup needed after a successful validation
  after_validate(user, :any)

The :any argument is the purpose of the authentication. It allows you to require different credentials for different purposes. It can be passed as the second argument to authenticate/2:

  # Sets the purpose to :recovery, e.g. account recovery
  MyApp.Accounts.authenticate({"test@example.com", "password"}, :recovery)

Example

defmodule MyApp.Accounts do
  use Authority.Authentication

  # OPTIONAL
  @impl Authority.Authentication
  def before_identify(identifier) do
    # Do anything you need to do to the identifier
    # before it is used to identify the user.
    # 
    # Return {:ok, identifier}
  end

  # REQUIRED
  @impl Authority.Authentication
  def identify(identifier) do
    # Identify the user
  end

  # OPTIONAL
  @impl Authority.Authentication
  def before_validate(user, purpose) do
    # Define any additional checks that should determine
    # if authentication is permitted.
    #
    # For example, check if the user is active or if the
    # account is locked.
  end

  # REQUIRED
  @impl Authority.Authentication
  def validate(credential, user, purpose) do
    # check if the credential is valid for the user
  end

  # OPTIONAL
  @impl Authority.Authentication
  def after_validate(user, purpose) do
    # Clean up after a successful authentication
  end

  # OPTIONAL
  @impl Authority.Authentication
  def failed(user, error) do
    # Do anything that needs to be done on failure, such as
    # locking the user account on too many failed attempts
  end
end

Link to this section Summary

Types

A credential for a user

An authentication error

An identifier for a user, such as a username or email address

The purpose for the authentication request. This can be used as the basis to approve or deny the request. Defaults to :any

A memorized secret known to the user and the system, such as a password or token value

An authenticated user. Can be any type that represents a user in your system

Callbacks

This function will be called after validate/3, if validation was successful. Use it to clean up after a successful authentication

Verifies the credential and returns the associated user, if the credential is valid. Must assume that the purpose of the request is :any

Verifies the credential and returns the associated user, if the credential is valid. The purpose is configurable

This function will be called before identify/1. Use it to modify or refresh the id component of the credential before it is used to lookup the user

This function will be called before validate/3. Use it to verify any additional information beyond the user’s secret: such as whether their account is active or locked

This function will be called if the authentication attempt fails for any reason. Use it to apply security locks or anything else that needs to be done on failure

Identifies and returns the user belonging to the given id

Validates whether the given secret is valid for the identified user and purpose

Link to this section Types

Link to this type credential()
credential() :: {id(), secret()} | secret()

A credential for a user.

Credentials can either be tuples containing an id/secret pair, or a single value which serves both purposes, like an API token.

ID/Secret Tuples

{"test@email.com", "password"}

When a tuple, the first element is considered the id, and will be passed to the following callbacks:

  • before_identify/1
  • identify/1

The second element will be considered the secret, and will be passed as the first argument to validate/3.

Single-Value Credentials

%MyApp.Accounts.Token{token: "..."}

When a non-tuple value is used as a credential, (like a token) the value will be used for both purposes, both to identify the user and as a shared secret for validating the request.

The value will be passed as either the id and the secret to all relevant callbacks.

  • before_identify/1
  • identify/1
  • validate/3
Link to this type error()
error() :: {:error, term()}

An authentication error.

Link to this type id()
id() :: any()

An identifier for a user, such as a username or email address.

Link to this type purpose()
purpose() :: atom()

The purpose for the authentication request. This can be used as the basis to approve or deny the request. Defaults to :any.

Link to this type secret()
secret() :: any()

A memorized secret known to the user and the system, such as a password or token value.

Link to this type user()
user() :: any()

An authenticated user. Can be any type that represents a user in your system.

Link to this section Callbacks

Link to this callback after_validate(user, purpose)
after_validate(user(), purpose()) :: :ok | error()

This function will be called after validate/3, if validation was successful. Use it to clean up after a successful authentication.

Example

def after_validate(user, purpose) do
  # Set failed login attempts for this user back to 0, since the user
  # successfully logged in
end
Link to this callback authenticate(credential)
authenticate(credential()) :: {:ok, user()} | error()

Verifies the credential and returns the associated user, if the credential is valid. Must assume that the purpose of the request is :any.

Link to this callback authenticate(credential, purpose)
authenticate(credential(), purpose()) :: {:ok, user()} | error()

Verifies the credential and returns the associated user, if the credential is valid. The purpose is configurable.

Link to this callback before_identify(id)
before_identify(id()) :: {:ok, id()} | error()

This function will be called before identify/1. Use it to modify or refresh the id component of the credential before it is used to lookup the user.

Example

def before_identify(%Token{token: token}) do
  # Load the token from the database instead of relying on the
  # struct that was passed in, to ensure accuracy
end
Link to this callback before_validate(user, purpose)
before_validate(user(), purpose()) :: :ok | error()

This function will be called before validate/3. Use it to verify any additional information beyond the user’s secret: such as whether their account is active or locked.

Example

def before_validate(user, purpose) do
  if user.active, do: :ok, else: {:error, :inactive}
end
Link to this callback failed(user, error)
failed(user(), error()) :: :ok | error()

This function will be called if the authentication attempt fails for any reason. Use it to apply security locks or anything else that needs to be done on failure.

Example

def failed(user, error) do
  # Apply a lock to the user's account if they have failed to
  # log in too many times
end
Link to this callback identify(id)
identify(id()) :: {:ok, user()} | error()

Identifies and returns the user belonging to the given id.

Example

def identify(email) do
  # Look up the user by email
end
Link to this callback validate(secret, user, purpose)
validate(secret(), user(), purpose()) :: :ok | error()

Validates whether the given secret is valid for the identified user and purpose.

Example

def validate(password, user, purpose) do
  if hash(password) == user.encrypted_password do
    :ok
  else
    {:error, :invalid_password}
  end
end