Guest User

Untitled

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