Guardian Generate Token for Account Verification

Hey guys, still exploring Guardian Right now am trying to implement the concept where when a user registers, an email verification is send to the user’s email as part of the authorization flow. But i have no idea how i an use Guardian for that though i believe its possible. Any kind of help will surely be appreciated.

Thank you all :blush:

1 Like

The most simple way is to generate a guardian token and make that part of the url parameters, though that might be long, I’d opt not for guardian at all for this and instead generate a unique database entry in a table that maps to what actually needs to be done, then the activation link can be much shorter and more readable. :slight_smile:

1 Like

(note: this is not using guardian, but perhaps can help you find a way of doing it)
The way I’ve implemented it was using a change_set for the signup, that adds a token to the record:

user model

def signup(struct, params \\ %{}) do
    struct
    |> cast(params, [:email, :password, :password_confirmation, :username])
    |> validate_required([:email, :password, :password_confirmation, :username])
    |> unique_constraint(:email)
    |> unique_constraint(:username)
    |> validate_length(:username, min: 3)
    |> validate_length(:username, max: 20)
    |> validate_length(:password, min: 6)
    |> validate_format(:email, ~r/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i)
    |> password_match
    |> set_required
  end

defp set_required(signup) do
    if signup.valid? do
      signup
      |> gen_hash_password
      |> put_change(:verification_token, random_string(64))
    else
      signup
    end
  end

def random_string(length) do
    :crypto.strong_rand_bytes(length) |> Base.url_encode64
  end

defp gen_hash_password(signup) do
    hash = Comeonin.Bcrypt.hashpwsalt(get_change(signup, :password))
    signup |> put_change(:password_hash, hash)
  end

And then just have a link to an endpoint that confirms it:

registration controller

def verify(conn, %{"vt" => token}) do
    case AetherWars.Registration.verify(token, AetherWars.Repo) do
      {:ok, %User{}} ->
        conn
        |> put_flash(:info, "Your account was verified, you can now login!")
        |> redirect(to: "/login")
      {:error, message} ->
        conn
        |> put_flash(:error, message)
        |> redirect(to: "/login")
    end
  end

registration model

def verify(token, repo) do
    query = from u in User, where: u.verification_token == ^token
    user = repo.one(query)
    case user do
      %User{verified: false} ->
        user = change user, verified: true
        repo.update(user)
      %User{verified: true} ->
        {:error, "You've already confirmed your email address, you can login!"}
      nil ->
        {:error, "Arfff - It doesn't seem there's such a token in Alastria!"}
    end
  end
2 Likes

Here’s another solution we use Guardian, but the token generation is done via Phoenix.

Context:

##
  # User activation API
  ##

  @doc """
  Generates activation token and sends activation email to the given user. 
  """
  @spec initiate_activation(%User{}) :: struct()
  defdelegate initiate_activation(user), to: ActivationService, as: :process_email_confirmation

  @doc """
  Finds and validates user for the given token, marks user's email as confirmed.

  Returns {:ok, message} or {:error, reason} tuple.
  """
  @spec finalize_activation(String.t) :: {:ok, String.t} | {:error, String.t}
  defdelegate finalize_activation(token), to: ActivationService, as: :check_confirmation_token

Here’s the (somewhat simplified) service module:

defmodule App.Accounts.Service.Activation do
  @moduledoc false

  alias App.{Mailer, Repo, Accounts.User}
  alias AppWeb.Endpoint
  alias App.Email.User, as: UserEmail
  alias App.Email.Admin, as: AdminEmail
  alias Ecto.{Changeset, Multi}
  alias Phoenix.Token

  require Logger

  import Ecto
  import Ecto.Query

  @token_max_age :app_name |> Application.get_env(:auth) |> Keyword.get(:normal_token_max_age)

  @doc false
  def process_email_confirmation(%User{} = user) do
    token = generate_token(user)
    result =
      user
      |> persist_token_multi(token)
      |> Repo.transaction()

    case result do
      {:ok, _params} -> send_confirmation_email(user, token)
      {:error, failed_operation, failed_value, changes_so_far} ->
        params = %{failed_operation: failed_operation, failed_value: failed_value, changes_so_far: changes_so_far}
        process_error(user, params)
    end
  end

  @doc false
  def check_confirmation_token(token) when is_binary(token) do
    with(
      {:ok, user_id} <- Token.verify(Endpoint, "user", token, [max_age: @token_max_age]),
      user <- Repo.get(User, user_id),
      1 <- user |> assoc(:user_tokens) |> where([token: ^token, type: "email_confirmation"]) |> Repo.aggregate(:count, :id)
    ) do
      case Repo.transaction(persist_confirmation(user)) do
        {:ok, _params} -> {:ok, "Email address confirmed"}
        {:error, failed_operation, failed_value, changes_so_far} ->
          params = %{failed_operation: failed_operation, failed_value: failed_value, changes_so_far: changes_so_far}
          process_error(user, params)
          {:error, "We are having technitcal problems, please retry confirmation"}
      end
    else
      _ -> {:error, "Invalid token"}
    end
  end

  defp send_confirmation_email(user, token) do
    user
    |> UserEmail.registration_email(token)
    |> Mailer.deliver_later!()
  end

  defp persist_token_multi(user, token) do
    Multi.new
    |> Multi.delete_all(:delete_old_tokens, get_old_tokens_query(user))
    |> Multi.insert(:insert_token, build_assoc(user, :user_tokens, %{token: token, type: "email_confirmation"}))
  end

  defp persist_confirmation(user) do
    Multi.new
    |> Multi.delete_all(:delete_old_tokens, get_old_tokens_query(user))
    |> Multi.update(:update_user, Changeset.change(user, [email_confirmed: true]))
  end

  defp get_old_tokens_query(user) do
    user
    |> assoc(:user_tokens)
    |> where([t], [type: "email_confirmation"])
  end

  defp generate_token(user) do
    Token.sign(Endpoint, "user", user.id)
  end

  defp process_error(user, params) do
    Logger.error("Unable to complete email confirmation, extra data:")
    Logger.error(inspect(user))
    Logger.error(inspect(params))
  end  
end

You can call Accounts.initiate_activation(user) after creating the user and in the confirmation controller action something like that:

case Accounts.finalize_activation(token) do
      {:ok, _} ->
        conn
        |> put_flash(:info, "Email confirmed")
        |> redirect(to: some_path(conn, :index))
      {:error, message} ->
        conn
        |> put_flash(:error, message)
        |> redirect(to: some_path(conn, :index))
    end
3 Likes

Okay this is a good idea too. thanks

I do love this approach to, looks much simpler and straight forward

thank you all for your feedbacks really appreciate