Guest User

Untitled

a guest
Feb 13th, 2018
340
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.49 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.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.
Add Comment
Please, Sign In to add comment