Advertisement
Guest User

Untitled

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