Saturday, August 5, 2017

How to create a user authentication systems with email verification in phoenix framework

Share it Please
Phoenix Framework is built with the powerful and resilient programming language called Elixir Lang. It is gaining momentum by the day thanks to it’s expressive syntax which makes you productive and it’s built upon the shoulders of the Erlang vm called BEAM which makes your code performant.
Here will build a user registration with Email verification and a login systems. Phoenix framework can be used to build normal html 5 crud apps, json api and real time backends. We’ll be building a real time backend using Phoenix channels and an Elixir library Guardian jwt.
You can find installation instructions on the Elixir site and Phoenix framework .
We be using Phoenix 1.3 which is the current version.
Lets create our app called sample with this command:
$ mix phx.new sample --no-html --no-brunch
* creating sample/config/config.exs
* creating sample/config/dev.exs
* creating sample/config/prod.exs
* creating sample/config/prod.secret.exs
* creating sample/config/test.exs
* creating sample/lib/sample/application.ex
* creating sample/lib/sample/web/channels/user_socket.ex
* creating sample/lib/sample/web/views/error_helpers.ex
* creating sample/lib/sample/web/views/error_view.ex
* creating sample/lib/sample/web/endpoint.ex
* creating sample/lib/sample/web/router.ex
* creating sample/lib/sample/web/web.ex
* creating sample/mix.exs
* creating sample/README.md
* creating sample/test/support/channel_case.ex
* creating sample/test/support/conn_case.ex
* creating sample/test/test_helper.exs
* creating sample/test/sample/web/views/error_view_test.exs
* creating sample/lib/sample/web/gettext.ex
* creating sample/priv/gettext/en/LC_MESSAGES/errors.po
* creating sample/priv/gettext/errors.pot
* creating sample/lib/sample/repo.ex
* creating sample/priv/repo/seeds.exs
* creating sample/test/support/data_case.ex
* creating sample/.gitignore
Fetch and install dependencies? [Yn] n
We are almost there! The following steps are missing:
    $ cd sample
    $ mix deps.get
Then configure your database in config/dev.exs and run:
    $ mix ecto.create
Start your Phoenix app with:
    $ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
    $ iex -S mix phx.server
cd into your sample app and go to the mix.ex file and add these libraries in your deps function : {:guardian, “~> 0.14”} , {:swoosh, “~> 0.8.1”}, {:secure_random, “~> 0.5.1”}. These will provide us with Json web token for user authentication , sending email for the user to verify their emailaddress before they can fully loging into our app, and generating a email verification token.
Lets now setup swoosh to send emails, we’ll be using Mailgun api to signup for a free sandbox accout to start sending email with their api.
# In your config/config.exs file
config :sample, Sample.Mailer,
  adapter: Swoosh.Adapters.Mailgun,
  api_key: "x.x.x"
  
# In your lib/sample folder create mailer.ex
defmodule Sample.Mailer do
  use Swoosh.Mailer, otp_app: :sample
end
defmodule StockitServer.UserEmail do
    import Swoosh.Email
    
    
 # In your lib/sample folder create user_email.ex   
defmodule StockitServer.UserEmail do
    import Swoosh.Email
    def welcome(user) do
        new()
        |> to({user.name, user.email})
        |> from({"your app name", "your-appxxx@gmail.com"})
        |> subject("Your sample verification email")
        |> html_body("<p>Thanks for signing up with us</p>
            <p>Please click the link below to verify your email address</p>
            <a href=https://sxxxx.com/v1/api/auth/verify_email/#                    {user.token}>Verify address</a>"
          )
    end
end
Lets config Guardian library:
# In your config/config.ex  
 
 config :guardian, Guardian,
  allowed_algos: ["HS512"], # optional
  verify_module: Guardian.JWT,  # optional
  issuer: "Sample",
  ttl: { 30, :days },
  allowed_drift: 2000,
  verify_issuer: true, # optional
  secret_key: System.get_env("GUARDIAN_SECRET") || "xx...xxxx",
  serializer: Sample.Web.GuardianSerializer
  
  
  # In your lib/sample/web folder create guardian_serializer.ex
  
  defmodule Sample.Web.GuardianSerializer do
  @behaviour Guardian.Serializer
  alias Sample.Repo
  alias Sample.Account.User
  def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
  def for_token(_), do: { :error, "Unknown resource type" }
  def from_token("User:" <> id), do: { :ok, Repo.get(User, id) }
  def from_token(_), do: { :error, "Unknown resource type" }
end
Phoenix framework directory structure now favours creating context to access your data model. So lets create Accounts context with apis to access our User model.
#In your lib/sample create account/account.ex
defmodule Sample.Account do
  @moduledoc """
  The boundary for the Account system.
  """
  import Ecto.Query, warn: false
  alias Sample.Repo
  alias Sample.Account.User
  @doc """
  Returns the list of users.
  ## Examples
      iex> list_users()
      [%User{}, ...]
  """
  def list_users do
    Repo.all(from u in "account_users",
      order_by: u.inserted_at,
      select: %{name: u.name, username: u.username, email_verified: u.email_verified, token: u.token, email: u.email, id: u.id})
  end
  @doc """
  Gets a single user.
  Raises `Ecto.NoResultsError` if the User does not exist.
  ## Examples
      iex> get_user!(123)
      %User{}
      iex> get_user!(456)
      ** (Ecto.NoResultsError)
  """
  def get_user!(id), do: Repo.one(from u in "account_users",
                                      where: u.id == ^String.to_integer(id),
                                      select: %{name: u.name, username: u.username, email_verified: u.email_verified, token: u.token, email: u.email, id: u.id})
  @doc """
  Creates a user.
  ## Examples
      iex> create_user(%{field: value})
      {:ok, %User{}}
      iex> create_user(%{field: bad_value})
      {:error, %Ecto.Changeset{}}
  """
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end
  @doc """
  Updates a user.
  ## Examples
      iex> update_user(user, %{field: new_value})
      {:ok, %User{}}
      iex> update_user(user, %{field: bad_value})
      {:error, %Ecto.Changeset{}}
  """
  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end
  @doc """
  Deletes a User.
  ## Examples
      iex> delete_user(user)
      {:ok, %User{}}
      iex> delete_user(user)
      {:error, %Ecto.Changeset{}}
  """
  def delete_user(%User{} = user) do
    Repo.delete(user)
  end
  @doc """
  Returns an `%Ecto.Changeset{}` for tracking user changes.
  ## Examples
      iex> change_user(user)
      %Ecto.Changeset{source: %User{}}
  """
  def change_user(%User{} = user) do
    User.changeset(user, %{})
  end
  def get_by(username) do
    Repo.get_by(User, username: username)
  end
  def get_by_token(token) do
    Repo.get_by(User, token: token)
  end
  def verify_email(user) do
    user
    |> User.verify_changeset()
    |> Repo.update()
  end
  def get!(id) do
    Repo.get!(User, id)
  end
end
#In your lib/sample create  account/user.ex for our user schema
defmodule Sample.Account.User do
    use Ecto.Schema
    import  Ecto.Changeset
    alias Sample.Account.User
    schema "account_users" do
        field :email, :string
        field :name, :string
        field :username, :string
        field :password, :string, virtual: true
        field :password_hash, :string
        field :email_verified, :boolean
        field :token, :string
        timestamps()
    end
    def changeset(%User{} = user, params) do
        user
        |> cast(params, [:name, :email, :username])
        |> validate_required([:name, :username, :email])
        |> validate_length(:username, min: 5, max: 20)
        |> validate_format(:email, ~r/@/)
        |> unique_constraint(:email)
        |> put_not_verified()
        |> put_token()
    end
    defp put_not_verified(changeset) do
        case changeset do
            %Ecto.Changeset{valid?: true} ->
                put_change(changeset, :email_verified, false)
            _ ->
                changeset
        end
    end
    defp put_token(changeset) do
        case changeset do
            %Ecto.Changeset{valid?: true} ->
                put_change(changeset, :token, SecureRandom.urlsafe_base64())
            _ ->
                changeset
        end
    end
    def registration_changeset(model, params \\ %{}) do
        model
        |> changeset(params)
        |> cast(params, [:password])
        |> validate_length(:password, min: 4, max: 200)
        |> put_pass_hash()
    end
    defp put_pass_hash(changeset) do
        case changeset do
            %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
                put_change(changeset, :password_hash, Hasher.salted_password_hash(password))
            _ ->
                changeset
        end
    end
    def verify_changeset(%User{} = user, params \\ %{}) do
        user
        |> cast(params, [])
        |> put_verify_email()
    end
    defp put_verify_email(changeset) do
        case changeset do
            %Ecto.Changeset{valid?: true} ->
                put_change(changeset, :email_verified, true)
            _ ->
                changeset
        end
    end
end
# Let create our migration from your command line
$ mix ecto.gen.migration create_user
cd into your priv/repo/migrations folder. You’ll see the already created migration file.
defmodule Sample.Repo.Migrations.CreateAccountUser do
  use Ecto.Migration
  def change do
    create table(:account_users) do
        add :name, :string
        add :username, :string
        add :email, :string
        add :token, :string
        add :email_verified, :boolean
        add :password_hash, :string
        timestamps()
    end
    create unique_index(:account_users, [:email, :username])
  end
end
Now all that’s left is to migrate up our database
$ mix ecto.migrate
Now let’s create our authentication module. cd into lib/sample/web folder and create aan auth.ex file
defmodule Sample.Web.Auth do
    import Hasher
    alias Sample.Account
    def login_by_username_and_pass(username, given_pass) do
        user = Account.get_by(username)
        if user do
            cond do
            user.email_verified && check_password_hash(given_pass, user.password_hash) ->
                {:ok, user}
            user.email_verified && check_password_hash(given_pass, user.password_hash) == false ->
                {:err, :unauthorized}
            not user.email_verified ->
                {:err, :notverified}
            end
        else
            {:err, :notfound}
        end
    
    end
  
  end
Let’s now create our route to verify our email address
defmodule Sample.Web.Router do
use Sample.Web, :router
pipeline :api do
  plug :accepts, ["json", "json-api"]
  plug JaSerializer.Deserializer
end
pipeline :api_auth do  
  plug :accepts, ["json", "json-api"]
  plug Guardian.Plug.VerifyHeader, realm: "Bearer"
  plug Guardian.Plug.LoadResource
  plug JaSerializer.Deserializer
end 
  scope "/", Sample.Web do
  pipe_through :api # Use the default browser stack
  get "/", PageController, :index
end
scope "/v1/api/auth", Sample.Web do
  pipe_through :api
   get "/verify_email/:token", AccessTokenController, :verify_email 
end
end
Lets create our AccessTokenController.
#In your lib/sample/web/controllers/access_token_controller.ex
defmodule Sample.Web.AccessTokenController do
    use Sample.Web, :controller
    alias Sample.Account
    alias Sample.Account.User
     action_fallback Sample.Web.FallbackController
    def verify_email(conn, %{"token" => token}) do
        user = Account.get_by_token(token)
        if user do
           with {:ok, %User{} = user} <- Account.verify_email(user) do
               
                conn
                |> put_status(200)
                |> render("verify_email.json", user: user)
              
            end 
        else 
            conn
            |> put_status(:not_found)
            |> json(%{error: "invalid token"})
        end
    end
end
Lets create our access token view.
defmodule Sample.Web.AccessTokenView do
    use Sample.Web, :view
    def render("verify_email.json", %{user: user}) do
        %{
            verified: user.email_verified
        }   
    end
end
Lets create an access token channel to handle our user registration and login.
#In lib/sample/web/channels create access_token_channel.ex
defmodule Sample.Web.AccessTokenChannel do
  use Sample.Web, :channel
  alias Sample.Account
  alias Sample.UserEmail
  alias Sample.Mailer
  def join("access:lobby", _payload, socket) do
      {:ok, socket}
  end
  def handle_in("create:user", %{"user" => user_params}, socket) do
    case Account.create_user(user_params) do
      {:ok, user} ->
        UserEmail.welcome(user)|>Mailer.deliver
        resp = %{data: %{id: user.id,
                    name: user.name,
                    username: user.username,
                    email: user.email,
                    token: user.token,
                    verified: user.email_verified }
                   }
        push socket, "create:user", resp
        {:reply, :ok, socket}
      {:error, _changeset} ->
        {:reply, {:error, %{errors: "Could not create user"}}, socket}
    end
  end
  
def handle_in("create:login", %{"user" => %{"username" => username, "password" =>  password}}, socket)  do
      case Sample.Web.Auth.login_by_username_and_pass(username, password)  do
        {:ok, user} ->
          {:ok, jwt, _claims} = Guardian.encode_and_sign(user, :access)
          resp = %{data: %{access_token: jwt}}
          push socket, "create", resp
          {:noreply, socket}
        {:err, :notverified} ->
          {:reply, {:error, %{errors: "please verify your email address"}}, socket}
        {:err, :unauthorized} ->
          {:reply, {:error, %{errors: "user password incorrect"}}, socket}
        {:err, :notfound} ->
          {:reply, {:error, %{errors: "user not found"}}, socket}
      end
  end
end
#In your lib/sample/web/channels/user_socket.ex file add to the list of channels
channel "access:*", Sample.Web.AccessTokenChannel
This concludes our user signup and login system for a real time backend server.

No comments:

Post a Comment

Powered By Aleme Gabriel