Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # Elixir + Phoenix Framework 1.3 + Guardian + JWT(Refresh, Revoke, Recover) + Comeonin
- ### User model bootstrap
- Let's generate User model and controller.
- ```bash
- mix ecto.create
- mix phoenix.gen.json Accounts User users email:string password_hash:string
- ```
- Now we need to add users path to our API routes.
- ```elixir
- defmodule MyAppName.Router do
- # ...
- scope "/api/v1", MyAppNameWeb do
- pipe_through :api
- resources "/users", UserController, except: [:new, :edit]
- end
- # ...
- end
- ```
- Also we need to do some fixes in migration file.
- If you need `uuid` instead of `id` we need to add `:binary_id` field and disable native `primary_key`.
- If you have columns with unique values you also need to call `unique_index` method.
- Also we need to add `default` and not `null` instructions.
- ```elixir
- defmodule MyAppName.Repo.Migrations.CreateMyAppName.Accounts.User do
- use Ecto.Migration
- def change do
- create table(:accounts_users, primary_key: false) do
- add :id, :binary_id, primary_key: true
- add :email, :string, null: false
- add :name, :string, null: false
- add :phone, :string, null: true
- add :password_hash, :string, null: false
- add :is_admin, :boolean, null: false, default: false
- timestamps()
- end
- create unique_index(:accounts_users, [:email])
- end
- end
- ```
- Let's migrate DB.
- ```bash
- mix ecto.migrate
- ```
- ### Preparing environment
- We need to generate secret key for development environment.
- ```bash
- mix phoenix.gen.secret
- # ednkXywWll1d2svDEpbA39R5kfkc9l96j0+u7A8MgKM+pbwbeDsuYB8MP2WUW1hf
- ```
- Guardian requires serializer for JWT token generation, so we need to create it `lib/my_app_name/token_serializer.ex`. You need to restart your server, after adding files to `lib` folder.
- ```elixir
- defmodule MyAppName.GuardianSerializer do
- @behaviour Guardian.Serializer
- alias MyAppName.Repo
- alias MyAppName.Accounts.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
- ```
- After that we need to add Guardian configuration. Add `guardian` base configuration to your `config/config.exs`
- ```elixir
- config :guardian, Guardian,
- allowed_algos: ["HS512"], # optional
- verify_module: Guardian.JWT, # optional
- issuer: "MyAppName",
- ttl: { 30, :days },
- allowed_drift: 2000,
- verify_issuer: true, # optional
- secret_key: "ednkXywWll1d2svDEpbA39R5kfkc9l96j0+u7A8MgKM+pbwbeDsuYB8MP2WUW1hf", # Insert previously generated secret key!
- serializer: MyAppName.GuardianSerializer
- ```
- Add `guardian` dependency to your `mix.exs`
- ```elixir
- defp deps do
- [
- # ...
- {:guardian, "~> 0.14"},
- # ...
- ]
- end
- ```
- Fetch and compile dependencies
- ```bash
- mix do deps.get, compile
- ```
- #### Guardian is ready!
- ### Model authentication part
- #### User tweaks
- Next step is to add validations to `lib/my_app_name/accounts/user.ex`. Virtual `:password` field will exist in Ecto structure, but not in the database, so we are able to provide password to the model’s changesets and, therefore, validate that field.
- ```elixir
- defmodule MyAppName.Accounts.User do
- # ...
- @primary_key {:id, :binary_id, autogenerate: true}
- schema "accounts_users" do
- field :email, :string
- field :name, :string
- field :phone, :string
- field :password, :string, virtual: true # We need to add this row
- field :password_confirmation, :string, virtual: true # Confirmation for password field
- field :password_hash, :string
- field :is_admin, :boolean, default: false
- timestamps()
- end
- # ...
- end
- ```
- #### Validations and password hashing
- Add `comeonin` dependency to your `mix.exs`
- ```elixir
- #...
- def application do
- [applications: [:comeonin]] # Add comeonin to OTP application
- end
- # ...
- defp deps do
- [
- # ...
- {:comeonin, "~> 3.0"} # Add comeonin to dependencies
- # ...
- ]
- end
- ```
- Now we need to edit `lib/my_app_name/accounts/user.ex`, add validations for `[:email, password]` and integrate password hash generation. Also we need separate changeset functions for internal usage and API registration.
- ```elixir
- defmodule MyAppName.Accounts.User do
- #...
- def changeset(%User{} = user, attrs) do
- user
- |> cast(attrs, [:email, :name, :phone, :password, :is_admin])
- |> validate_required([:email, :name, :password])
- |> validate_changeset
- end
- def registration_changeset(%User{} = user, attrs) do
- user
- |> cast(attrs, [:email, :name, :phone, :password, :password_confirmation])
- |> validate_required([:email, :name, :phone, :password, :password_confirmation])
- |> validate_confirmation(:password)
- |> validate_changeset
- end
- defp validate_changeset(user) do
- user
- |> validate_length(:email, min: 5, max: 255)
- |> validate_format(:email, ~r/@/)
- |> unique_constraint(:email)
- |> validate_length(:password, min: 8)
- |> validate_format(:password, ~r/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*/, [message: "Must include at least one lowercase letter, one uppercase letter, and one digit"])
- |> generate_password_hash
- end
- defp generate_password_hash(changeset) do
- case changeset do
- %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
- put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(password))
- _ ->
- changeset
- end
- end
- #...
- end
- ```
- ### API authentication with Guardian
- Let's add headers check in our `lib/my_app_name/web/router.ex` for further authentication flow.
- ```elixir
- defmodule MyAppName.Router do
- # ...
- pipeline :api do
- plug :accepts, ["json"]
- plug Guardian.Plug.VerifyHeader
- plug Guardian.Plug.LoadResource
- end
- pipeline :authenticated do
- plug Guardian.Plug.EnsureAuthenticated
- end
- # ...
- scope "/api/v1", MyAppName.Web do
- pipe_through :api
- pipe_through :authenticated # restrict unauthenticated access for routes below
- resources "/users", UserController, except: [:new, :edit]
- end
- # ...
- end
- ```
- ### Registration
- Now we can't get access to /users route without Bearer JWT Token in header. That's why we need to add `RegistrationController` and `SessionController`. It's a good time to make commit before further changes.
- Hey we need to add some more logic registration. Let's add `register_user` method in `lib/my_app_name/accounts/accounts.ex`
- ```elixir
- defmodule MyAppName.Accounts do
- @moduledoc """
- The boundary for the Accounts system.
- """
- import Ecto.Query, warn: false
- alias MyAppName.Repo
- alias MyAppName.Accounts.User
- # ...
- @doc """
- Creates a user using registration attributes.
- """
- def register_user(attrs \\ %{}) do
- %User{}
- |> User.registration_changeset(attrs)
- |> Repo.insert()
- end
- # ...
- end
- ```
- Let's create `RegistrationController`. We need to create new file `lib/my_app_name/web/controllers/registration_controller.ex`. Also we need specific `registration_changeset` that we declared before inside of `lib/my_app_name/accounts/user.ex`
- ```elixir
- defmodule MyAppName.Web.RegistrationController do
- use MyAppName.Web, :controller
- alias MyAppName.Accounts
- alias MyAppName.Accounts.User
- action_fallback MyAppName.Web.FallbackController
- def sign_up(conn, %{"user" => user_params}) do
- with {:ok, %User{} = user} <- Accounts.register_user(user_params) do
- conn
- |> put_status(:created)
- |> put_resp_header("location", user_path(conn, :show, user))
- |> render("success.json", user: user)
- end
- end
- end
- ```
- Also we need `RegistrationView`. So, we need to create one more file named `lib/my_app_name/web/views/registration_view.ex`.
- ```elixir
- defmodule MyAppName.Web.RegistrationView do
- use MyAppName.Web, :view
- def render("success.json", %{user: _user}) do
- %{
- status: :ok,
- message: """
- Now you can sign in using your email and password at /api/v1/sign_in. You will receive JWT token.
- Please put this token into Authorization header for all authorized requests.
- """
- }
- end
- end
- ```
- After that we need to add /api/v1/sign_up route. Just add it inside of API scope.
- ```elixir
- defmodule MyAppName.Router do
- # ...
- scope "/api/v1", MyAppName.Web do
- pipe_through :api
- post "/sign_up", RegistrationController, :sign_up
- # ...
- end
- # ...
- end
- ```
- It's time to check our registration controller. If you don't know how to write request tests. You can use Postman app. Let's POST /api/v1/sign_up with this JSON body.
- ```json
- {
- "user": {}
- }
- ```
- We should receive this response
- ```json
- {
- "errors": {
- "phone": [
- "can't be blank"
- ],
- "password": [
- "can't be blank"
- ],
- "name": [
- "can't be blank"
- ],
- "email": [
- "can't be blank"
- ]
- }
- }
- ```
- It's good point, but we need to create new user. That's why we need to POST correct payload.
- ```json
- {
- "user": {
- "email": "hello@world.com",
- "name": "John Doe",
- "phone": "033-64-22",
- "password": "MySuperPa55"
- }
- }
- ```
- We must get this response.
- ```json
- {
- "status": "ok",
- "message": " Now you can sign in using your email and password at /api/v1/sign_in. You will receive JWT token.\n Please put this token into Authorization header for all authorized requests.\n"
- }
- ```
- ### Session management
- Wow! We've created new user! Now we have user with password hash in our DB. We need to add password checker function in `lib/my_app_name/accounts/user.ex`.
- ```elixir
- defmodule MyAppName.Accounts.User do
- # ...
- def find_and_confirm_password(email, password) do
- case Repo.get_by(User, email: email) do
- nil ->
- {:error, :login_not_found}
- user ->
- if Comeonin.Bcrypt.checkpw(password, user.password_hash) do
- {:ok, user}
- else
- {:error, :login_failed}
- end
- end
- end
- # ...
- end
- ```
- Before we add `SessionController`, we need to handle `:not_found` and `:unauthorized` errors. So, let's create `FallbackAPIController` module in `lib/my_app_name/web/controllers/fallback_api_controller.ex`
- ```elixir
- defmodule MyAppName.Web.FallbackAPIController do
- use MyAppName.Web, :controller
- def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
- conn
- |> put_status(:unprocessable_entity)
- |> render(MyAppName.Web.ChangesetView, "error.json", changeset: changeset)
- end
- def call(conn, {:error, :login_failed}), do: login_failed(conn)
- def call(conn, {:error, :login_not_found}), do: login_failed(conn)
- defp login_failed(conn) do
- conn
- |> put_status(401)
- |> render(MyAppName.Web.ErrorView, "error.json", status: :unauthorized, message: "Authentication failed!")
- end
- end
- ```
- Also we need to add "error.json" to `MyAppName.Web.ErrorView` module in `lib/my_app_name/web/views/error_view.ex`. This part is required for correct JSON errors handling.
- ```elixir
- defmodule MyAppName.Web.ErrorView do
- use MyAppName.Web, :view
- # ...
- def render("error.json", %{status: status, message: message}) do
- %{status: status, message: message}
- end
- # ...
- end
- ```
- It's time to use our credentials for sign in action. We need to add `SessionController` with `sign_in` actions, so just create `lib/my_app_name/web/controllers/session_controller.ex`.
- ```elixir
- defmodule MyAppName.Web.SessionController do
- use MyAppName.Web, :controller
- alias MyAppName.Accounts.User
- action_fallback MyAppName.Web.FallbackAPIController
- def sign_in(conn, %{"session" => %{"email" => email, "password" => pass}}) do
- with {:ok, user} <- User.find_and_confirm_password(email, pass),
- {:ok, jwt, _full_claims} <- Guardian.encode_and_sign(user, :api),
- do: render(conn, "sign_in.json", user: user, jwt: jwt)
- end
- end
- ```
- Good! Next step is to add `SessionView` in `lib/my_app_name/web/views/session_view.ex`.
- ```elixir
- defmodule MyAppName.Web.SessionView do
- use MyAppName.Web, :view
- def render("sign_in.json", %{user: user, jwt: jwt}) do
- %{
- status: :ok,
- data: %{
- token: jwt,
- email: user.email
- },
- message: "You are successfully logged in! Add this token to authorization header to make authorized requests."
- }
- end
- end
- ```
- Add some routes to handle sign_in action in `lib/my_app_name/web/router.ex`.
- ```elixir
- defmodule MyAppName.Router do
- use MyAppName.Web, :router
- #...
- scope "/api/v1", MyAppName.Web do
- pipe_through :api
- post "/sign_up", RegistrationController, :sign_up
- post "/sign_in", SessionController, :sign_in # Add this line
- pipe_through :authenticated
- resources "/users", UserController, except: [:new, :edit]
- end
- # ...
- end
- ```
- Ok. Let's check this stuff. POST `/api/v1/sign_in` with this params.
- ```json
- {
- "session": {
- "email": "hello@world.com",
- "password": "MySuperPa55"
- }
- }
- ```
- We should receive this response
- ```json
- {
- "status": "ok",
- "message": "You are successfully logged in! Add this token to authorization header to make authorized requests.",
- "data": {
- "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJVc2VyOjEiLCJleHAiOjE0OTgwMzc0OTEsImlhdCI6MTQ5NTQ0NTQ5MSwiaXNzIjoiQ2lhbkV4cG9ydGVyIiwianRpIjoiZDNiOGYyYzEtZDU3ZS00NTBlLTg4NzctYmY2MjBiNWIxMmI1IiwicGVtIjp7fSwic3ViIjoiVXNlcjoxIiwidHlwIjoiYXBpIn0.HcJ99Tl_K1UBsiVptPa5YX65jK5qF_L-4rB8HtxisJ2ODVrFbt_TH16kJOWRvJyJIoG2EtQz4dXj7tZgAzJeJw",
- "email": "hello@world.com"
- }
- }
- ```
- Now. You can take this token and add it to `Authorization: #{token}` header.
Add Comment
Please, Sign In to add comment