Advertisement
Guest User

Untitled

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