daily pastebin goal
45%
SHARE
TWEET

Untitled

a guest Feb 13th, 2018 172 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. # Elixir + Phoenix Framework + Guardian + JWT + Comeonin
  2.  
  3. ### Preparing environment
  4. We need to generate secret key for development environment.
  5. ```bash
  6. mix phoenix.gen.secret
  7. # ednkXywWll1d2svDEpbA39R5kfkc9l96j0+u7A8MgKM+pbwbeDsuYB8MP2WUW1hf
  8. ```
  9.  
  10. Let's generate User model and controller.
  11.  
  12. ```bash
  13. mix ecto.migrations user_add_is_admin
  14. ```
  15. ```elixir
  16. defmodule MyAppName.Repo.Migrations.UserAddIsAdmin do
  17.   use Ecto.Migration
  18.   def change do
  19.     alter table(:users) do
  20.       add :is_admin, :boolean
  21.       add :phone, :string
  22.     end
  23.   end
  24. end
  25. ```
  26.  
  27. ```bash
  28.   mix ecto.migrate
  29. ```
  30.  
  31. 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.
  32.  
  33. ```elixir
  34. defmodule MyAppName.GuardianSerializer do
  35.   @behaviour Guardian.Serializer
  36.  
  37.   alias MyAppName.Repo
  38.   alias MyAppName.User
  39.  
  40.   def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
  41.   def for_token(_), do: { :error, "Unknown resource type" }
  42.  
  43.   def from_token("User:" <> id), do: { :ok, Repo.get(User, id) }
  44.   def from_token(_), do: { :error, "Unknown resource type" }
  45. end
  46. ```
  47.  
  48. After that we need to add Guardian configuration. Add `guardian` base configuration to your `config/config.exs`
  49.  
  50. ```elixir
  51. config :guardian, Guardian,
  52.   allowed_algos: ["HS512"], # optional
  53.   verify_module: Guardian.JWT,  # optional
  54.   issuer: "MyAppName",
  55.   ttl: { 30, :days },
  56.   allowed_drift: 2000,
  57.   verify_issuer: true, # optional
  58.   secret_key: "ednkXywWll1d2svDEpbA39R5kfkc9l96j0+u7A8MgKM+pbwbeDsuYB8MP2WUW1hf", # Insert previously generated secret key!
  59.   serializer: MyAppName.GuardianSerializer
  60. ```
  61.  
  62. Add `guardian` dependency to your `mix.exs`
  63. ```elixir
  64. defp deps do
  65.   [
  66.     # ...
  67.     {:guardian, "~> 0.14"},
  68.     # ...
  69.   ]
  70. end
  71. ```
  72.  
  73. Fetch and compile dependencies
  74.  
  75. ```bash
  76. mix do deps.get, compile  
  77. ```
  78.  
  79. #### Guardian is ready!
  80.  
  81. ### Model authentication part
  82.  
  83. #### User tweaks
  84.  
  85. Now we need to add users path to our API routes.
  86. ```elixir
  87. defmodule MyAppName.Router do
  88.   # ...
  89.   scope "/api/v1", MyAppName do
  90.     pipe_through :api
  91.  
  92.     resources "/users", UserController, except: [:new, :edit]
  93.   end
  94.   # ...
  95. end
  96. ```
  97.  
  98. Next step is to add validations to `web/models/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.
  99.  
  100. ```elixir
  101. defmodule MyAppName.User do
  102.   # ...
  103.   schema "users" do
  104.     field :email, :string
  105.     field :name, :string
  106.     field :phone, :string
  107.     field :password, :string, virtual: true # We need to add this row
  108.     field :password_hash, :string
  109.     field :is_admin, :boolean, default: false
  110.  
  111.     timestamps()
  112.   end
  113.   # ...
  114. end
  115. ```
  116.  
  117. #### Validations and password hashing
  118.  
  119. Add `comeonin` dependency to your `mix.exs`
  120. ```elixir
  121. #...
  122. def application do
  123.   [applications: [:comeonin]] # Add comeonin to OTP application
  124. end
  125. # ...
  126. defp deps do
  127.   [
  128.     # ...
  129.     {:comeonin, "~> 3.0"} # Add comeonin to dependencies
  130.     # ...
  131.   ]
  132. end
  133. ```
  134.  
  135. Now we need to edit `web/models/user.ex`, add validations for `[:email, password]` and integrate password hash generation. Also we need separate changeset functions for internal usage and API registration.
  136.  
  137. ```elixir
  138. defmodule MyAppName.User do
  139.   #...
  140.   def changeset(struct, params \\ %{}) do
  141.     struct
  142.     |> cast(params, [:email, :name, :phone, :password, :is_admin])
  143.     |> validate_required([:email, :name, :password])
  144.     |> validate_changeset
  145.   end
  146.  
  147.   def registration_changeset(struct, params \\ %{}) do
  148.     struct
  149.     |> cast(params, [:email, :name, :phone, :password])
  150.     |> validate_required([:email, :name, :phone, :password])
  151.     |> validate_changeset
  152.   end
  153.  
  154.   defp validate_changeset(struct) do
  155.     struct
  156.     |> validate_length(:email, min: 5, max: 255)
  157.     |> validate_format(:email, ~r/@/)
  158.     |> unique_constraint(:email)
  159.     |> validate_length(:password, min: 8)
  160.     |> validate_format(:password, ~r/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*/, [message: "Must include at least one lowercase letter, one uppercase letter, and one digit"])
  161.     |> generate_password_hash
  162.   end
  163.  
  164.   defp generate_password_hash(changeset) do
  165.     case changeset do
  166.       %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
  167.         put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(password))
  168.       _ ->
  169.         changeset
  170.     end
  171.   end
  172.   #...
  173. end
  174. ```
  175.  
  176. ### API authentication with Guardian
  177.  
  178. Let's add headers check in our `web/router.ex` for further authentication flow.
  179.  
  180. ```elixir
  181. defmodule MyAppName.Router do
  182.   # ...
  183.   pipeline :api do
  184.     plug :accepts, ["json"]
  185.     plug Guardian.Plug.VerifyHeader
  186.     plug Guardian.Plug.LoadResource
  187.   end
  188.  
  189.   pipeline :authenticated do
  190.     plug Guardian.Plug.EnsureAuthenticated
  191.   end
  192.   # ...
  193.   scope "/api/v1", MyAppName do
  194.     pipe_through :api
  195.  
  196.     pipe_through :authenticated # restrict unauthenticated access for routes below
  197.     resources "/users", UserController, except: [:new, :edit]
  198.   end
  199.   # ...
  200. end
  201. ```
  202.  
  203. ### Registration
  204.  
  205. 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.
  206.  
  207. Let's create RegistrationController. We need to create new file `web/controllers/registration_controller.ex`. Also we need specific `registration_changeset` that we declared before inside of `web/models/user.ex`
  208.  
  209. ```elixir
  210. defmodule MyAppName.RegistrationController do
  211.   use MyAppName.Web, :controller
  212.  
  213.   alias MyAppName.User
  214.  
  215.   def sign_up(conn, %{"user" => user_params}) do
  216.     changeset = User.registration_changeset(%User{}, user_params)
  217.  
  218.     case Repo.insert(changeset) do
  219.       {:ok, user} ->
  220.         conn
  221.         |> put_status(:created)
  222.         |> put_resp_header("location", user_path(conn, :show, user))
  223.         |> render("success.json", user: user)
  224.       {:error, changeset} ->
  225.         conn
  226.         |> put_status(:unprocessable_entity)
  227.         |> render(MyAppName.ChangesetView, "error.json", changeset: changeset)
  228.     end
  229.   end
  230. end
  231. ```
  232.  
  233. Also we need RegistrationView. So, we need to create one more file named `web/views/registration_view.ex`.
  234.  
  235. ```
  236. defmodule MyAppName.RegistrationView do
  237.   use MyAppName.Web, :view
  238.  
  239.   def render("success.json", %{user: user}) do
  240.     %{
  241.       status: :ok,
  242.       message: """
  243.         Now you can sign in using your email and password at /api/sign_in. You will receive JWT token.
  244.         Please put this token into Authorization header for all authorized requests.
  245.       """
  246.     }
  247.   end
  248. end
  249. ```
  250.  
  251. After that we need to add /api/sign_up route. Just add it inside of API scope.
  252.  
  253. ```
  254. defmodule MyAppName.Router do
  255.   # ...
  256.   scope "/api", MyAppName do
  257.     pipe_through :api
  258.  
  259.     post "/sign_up", RegistrationController, :sign_up
  260.     # ...
  261.   end
  262.   # ...
  263. end
  264. ```
  265.  
  266. 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/sign_up with this JSON body.
  267.  
  268. ```JSON
  269. {
  270.     "user": {}
  271. }
  272. ```
  273.  
  274. We should receive this response
  275.  
  276. ```JSON
  277. {
  278.   "errors": {
  279.     "phone": [
  280.       "can't be blank"
  281.     ],
  282.     "password": [
  283.       "can't be blank"
  284.     ],
  285.     "name": [
  286.       "can't be blank"
  287.     ],
  288.     "email": [
  289.       "can't be blank"
  290.     ]
  291.   }
  292. }
  293. ```
  294.  
  295. It's good point, but we need to create new user. That's why we need to POST correct payload.
  296.  
  297. ```JSON
  298. {
  299.     "user": {
  300.         "email": "hello@world.com",
  301.         "name": "John Doe",
  302.         "phone": "033-64-22",
  303.         "password": "MySuperPa55"
  304.     }
  305. }
  306. ```
  307.  
  308. We must get this response.
  309.  
  310. ```JSON
  311. {
  312.   "status": "ok",
  313.   "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"
  314. }
  315. ```
  316.  
  317. ### Session management
  318.  
  319. Wow! We've created new user! Now we have user with password hash in our DB. We need to add password checker function in `web/models/user.ex`.
  320.  
  321. ```elixir
  322. defmodule MyAppName.User do
  323.   # ...
  324.   def find_and_confirm_password(email, password) do
  325.     case Repo.get_by(User, email: email) do
  326.       nil ->
  327.         {:error, :not_found}
  328.       user ->
  329.         if Comeonin.Bcrypt.checkpw(password, user.password_hash) do
  330.           {:ok, user}
  331.         else
  332.           {:error, :unauthorized}
  333.         end
  334.     end
  335.   end
  336.   # ...
  337. end
  338. ```
  339.  
  340. It's time to use our credentials for sign in action. We need to add `SessionController` with `sign_in` and `sign_out` actions, so create `web/controllers/session_controller.ex`.
  341.  
  342. ```
  343. defmodule MyAppName.SessionController do
  344.   use MyAppName.Web, :controller
  345.  
  346.   alias MyAppName.User
  347.  
  348.   def sign_in(conn, %{"session" => %{"email" => email, "password" => password}}) do  
  349.     case User.find_and_confirm_password(email, password) do
  350.       {:ok, user} ->
  351.          {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :api)
  352.  
  353.          conn
  354.          |> render "sign_in.json", user: user, jwt: jwt
  355.       {:error, _reason} ->
  356.         conn
  357.         |> put_status(401)
  358.         |> render "error.json", message: "Could not login"
  359.     end
  360.   end  
  361. end
  362. ```
  363.  
  364. Good! Next step is to add SessionView in `web/views/session_view.ex`.
  365.  
  366. ```
  367. defmodule MyAppName.SessionView do
  368.   use MyAppName.Web, :view
  369.  
  370.   def render("sign_in.json", %{user: user, jwt: jwt}) do
  371.     %{
  372.       status: :ok,
  373.       data: %{
  374.         token: jwt,
  375.         email: user.email
  376.       },
  377.       message: "You are successfully logged in! Add this token to authorization header to make authorized requests."
  378.     }
  379.   end
  380. end
  381. ```
  382.  
  383. Add some routes to handle sign_in action in web/router.ex.
  384.  
  385. ```
  386. defmodule MyAppName.Router do
  387.   use MyAppName.Web, :router
  388.   #...
  389.   scope "/api/v1", CianExporter.API.V1 do
  390.     pipe_through :api
  391.  
  392.     post "/sign_up", RegistrationController, :sign_up
  393.     post "/sign_in", SessionController, :sign_in # Add this line
  394.  
  395.     pipe_through :authenticated
  396.     resources "/users", UserController, except: [:new, :edit]
  397.   end
  398.   # ...
  399. end
  400. ```
  401.  
  402. Ok. Let's check this stuff. POST `/api/sign_in` with this params.
  403.  
  404. ```JSON
  405. {
  406.     "session": {
  407.         "email": "hello@world.com",
  408.         "password": "MySuperPa55"
  409.     }
  410. }
  411. ```
  412.  
  413. We should receive this response
  414.  
  415. ```JSON
  416. {
  417.   "status": "ok",
  418.   "message": "You are successfully logged in! Add this token to authorization header to make authorized requests.",
  419.   "data": {
  420.     "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJVc2VyOjEiLCJleHAiOjE0OTgwMzc0OTEsImlhdCI6MTQ5NTQ0NTQ5MSwiaXNzIjoiQ2lhbkV4cG9ydGVyIiwianRpIjoiZDNiOGYyYzEtZDU3ZS00NTBlLTg4NzctYmY2MjBiNWIxMmI1IiwicGVtIjp7fSwic3ViIjoiVXNlcjoxIiwidHlwIjoiYXBpIn0.HcJ99Tl_K1UBsiVptPa5YX65jK5qF_L-4rB8HtxisJ2ODVrFbt_TH16kJOWRvJyJIoG2EtQz4dXj7tZgAzJeJw",
  421.     "email": "hello@world.com"
  422.   }
  423. }
  424. ```
  425.  
  426. Now. You can take this token and add it to `Authorization: Bearer #{token}` header.
RAW Paste Data
Top