Current section

Files

Jump to
one_time_pass_ecto lib one_time_pass_ecto.ex
Raw

lib/one_time_pass_ecto.ex

defmodule OneTimePassEcto do
@moduledoc """
Module to handle one-time passwords, usually for use in two factor
authentication.
## One-time password options
There are the following options for the one-time passwords:
* HMAC-based one-time passwords
* `:token_length` - the length of the one-time password
* the default is 6
* `:last` - the count when the one-time password was last used
* this count needs to be stored server-side
* `:window` - the number of future attempts allowed
* the default is 3
* Time-based one-time passwords
* `:token_length` - the length of the one-time password
* the default is 6
* `:interval_length` - the length of each timed interval
* the default is 30 (seconds)
* `:window` - the number of attempts, before and after the current one, allowed
* the default is 1 (1 interval before and 1 interval after)
See the documentation for the OneTimePassEcto.Base module for more details
about generating and verifying one-time passwords.
## Implementation details
The following notes provide details about how this module implements
the verification of one-time passwords.
It is important not to allow the one-time password to be reused within
the timeframe that it is valid.
For TOTPs, one method of preventing reuse is to compare the output of
check_totp (the `last` value) with the previous output. The output
should be greater than the previous `last` value.
In the case of HOTPs, it is important that the database is locked
from the time the `last` value is checked until the `last` value is
updated.
"""
import Ecto.{Changeset, Query}
alias OneTimePassEcto.Base
@doc """
Check the one-time password, and return {:ok, user} if the one-time
password is correct or {:error, message} if there is an error.
After this function has been called, you need to either add the user
to the session, by running `put_session(conn, :user_id, id)`, or send
an API token to the user.
See the `One-time password options` in this module's documentation
for available options to be used as the second argument to this
function.
"""
def verify(params, repo, user_schema, opts \\ [])
def verify(%{"id" => id, "hotp" => hotp}, repo, user_schema, opts) do
{:ok, result} =
repo.transaction(fn ->
get_user_with_lock(repo, user_schema, id)
|> check_hotp(hotp, opts)
|> update_otp(repo)
end)
result
end
def verify(%{"id" => id, "totp" => totp}, repo, user_schema, opts) do
repo.get(user_schema, id)
|> check_totp(totp, opts)
|> update_otp(repo)
end
defp check_hotp(user, hotp, opts) do
{user, Base.check_hotp(hotp, user.otp_secret, [last: user.otp_last] ++ opts)}
end
defp check_totp(user, totp, opts) do
{user, Base.check_totp(totp, user.otp_secret, opts)}
end
defp get_user_with_lock(repo, user_schema, id) do
from(u in user_schema, where: u.id == ^id, lock: "FOR UPDATE")
|> repo.one!
end
defp update_otp({_, false}, _), do: {:error, "invalid one-time password"}
defp update_otp({%{otp_last: otp_last} = user, last}, repo) when last > otp_last do
change(user, %{otp_last: last}) |> repo.update
end
defp update_otp(_, _), do: {:error, "invalid user-identifier"}
end