Error creating session

session
gardian

#1

** (exit) an exception was raised:
** (CaseClauseError) no case clause matching: true
(imcon) lib/imcon_web/controllers/api/v1/session_controller.ex:7: ImconWeb.SessionController.create/2
(imcon) lib/imcon_web/controllers/api/v1/session_controller.ex:1: ImconWeb.SessionController.action/2
(imcon) lib/imcon_web/controllers/api/v1/session_controller.ex:1: ImconWeb.SessionController.phoenix_controller_pipeline/2
(imcon) lib/imcon_web/endpoint.ex:1: ImconWeb.Endpoint.instrument/4
(phoenix) lib/phoenix/router.ex:275: Phoenix.Router. call /1
(imcon) lib/imcon_web/endpoint.ex:1: ImconWeb.Endpoint.plug_builder_call/2
(imcon) lib/plug/debugger.ex:122: ImconWeb.Endpoint.“call (overridable 3)”/2
(imcon) lib/imcon_web/endpoint.ex:1: ImconWeb.Endpoint.call/2
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:34: Phoenix.Endpoint.Cowboy2Handler.init/2
(cowboy) /opt/imcon/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy) /opt/imcon/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3
(cowboy) /opt/imcon/deps/cowboy/src/cowboy_stream_h.erl:274: :cowboy_stream_h.request_process/3
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

    defmodule ImconWeb.SessionController do
      use ImconWeb, :controller

      plug :scrub_params, "session" when action in [:create]

      def create(conn, %{"session" => %{"email" => email, "password" => password}}) do
        case Imcon.Auth.authenticate(email, password) do
          {:ok, user} ->
            {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token)

            conn
            |> put_status(:created)
            |> render("show.json", jwt: jwt, user: user)

          :error ->
            conn
            |> put_status(:unprocessable_entity)
            |> render("error.json")
        end
      end

      def delete(conn, _) do
        {:ok, claims} = Guardian.Plug.current_claims(conn)

        conn
        |> Guardian.Plug.current_token
        |> Guardian.revoke(claims)

        conn
        |> render("delete.json")
      end
      
      def unauthenticated(conn, _params) do
        conn
        |> put_status(:forbidden)
        |> render(ImconWeb.SessionView, "forbidden.json", error: "Not Authenticated")
      end

    end

Fragment from file auth.ex

```
  def authenticate(email, encrypted_password) do
    query = Ecto.Query.from(u in User, where: u.email == ^email)
    |> limit(1)
    Repo.one(query)
    |> check_password(encrypted_password)
  end

  defp check_password(user, password) do
    case user do
      nil -> Bcrypt.dummy_checkpw()
      _ -> Bcrypt.checkpw(password, user.encrypted_password)
    end
  end
```

Partially taken from here. https://github.com/PavelZX/cms


Ecto.Association.NotLoaded
#2

Imcon.Auth.authenticate/2 returns true, but case in controller handles only {:ok, user} or :error.


#3

Thoughtless copy / paste does not lead to anything good.
Returned back, as it was in the original in Phoenix-Trelo. Again an error with associations.


#4

In general, we will continue the blunders among the three pines …

Redid a little everything related to the Guardian. I am using this interesting project as an example. https://github.com/helium/console

defmodule ImconWeb.Router do
  use ImconWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json", "xml"]
    plug :fetch_session
    plug ImconWeb.AuthApiPipeline
  end

  scope "/api", ImconWeb do
    pipe_through :api

    scope "/v1" do
      post "/registrations", RegistrationController, :create

      post "/sessions", SessionController, :create
      delete "/sessions", SessionController, :delete

      get "/current_user", CurrentUserController, :show

      resources "/board", BoardController, only: [:index, :create] do
        resources "/card", CardController, only: [:show]
      end
    end
  end

  scope "/", ImconWeb do
    pipe_through :browser # Use the default browser stack

    get "/*path", PageController, :index
  end
end

This module turned out a little different.

defmodule ImconWeb.AuthApiPipeline do
  use Guardian.Plug.Pipeline, otp_app: :imcon,
    module: ImconWeb.Guardian,
    error_handler: ImconWeb.AuthErrorHandler

  plug Guardian.Plug.VerifyHeader
  plug Guardian.Plug.LoadResource

end

#5

Now the Elixir does not swear too much, not counting minor warnings, but all salt can be in them.

[info] GET /sign_in
[debug] Processing with ImconWeb.PageController.index/2
  Parameters: %{"path" => ["sign_in"]}
  Pipelines: [:browser]
[info] Sent 200 in 752µs
[info] POST /api/v1/sessions
[info] Sent 401 in 601µs

In the browser, it says about the session creation error.

Failed to load resource: the server responded with a status of 401 (Unauthorized)

I already cited the session controller file above.

This file also had to edit the function that does not work with the new Guardian.

defmodule ImconWeb.UserSocket do
  use Phoenix.Socket

  alias ImconWeb.{BoardChannel, UserChannel}
  alias Imcon.Auth.User
  
  # Channels
  channel "board:*", BoardChannel
  channel "user:*", UserChannel

  def connect(%{"token" => token}, socket) do
    case Guardian.decode_and_verify(token, socket) do
      {:ok, claims} ->
        case from_token(claims["sub"]) do
          {:ok, user} ->
            {:ok, assign(socket, :current_user, user)}
          {:error, _reason} ->
            :error
        end
      {:error, _reason} ->
        :error
    end
  end

  def connect(_params, _socket), do: :error

  def id(socket), do: "user_socket:#{socket.assigns.current_user.id}"

  def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
  
  def for_token(_), do: { :error, "Неизвестный тип ресурса" }

  def from_token("User:" <> id), do: { :ok, Imcon.Repo.get(User, String.to_integer(id)) }

  def from_token(_), do: { :error, "Неизвестный тип ресурса" }

end

Here, nothing has changed, but maybe you need to change something?

defmodule ImconWeb.CurrentUserController do
  use ImconWeb, :controller

  plug Guardian.Plug.EnsureAuthenticated, handler: ImconWeb.SessionController

  def show(conn, _) do
    user = Guardian.Plug.current_resource(conn)

    conn
    |> put_status(:ok)
    |> render("show.json", user: user)
  end
end

#6

I fought a bit with the front end. Changed the names of entities, so they are more consistent with what was intended. Again returned to the fight with Phoenix.

[error] #PID<0.669.0> running ImconWeb.Endpoint (connection #PID<0.659.0>, stream id 5) terminated
Server: caix.ru:4001 (http)
Request: POST /api/session
** (exit) an exception was raised:
** (FunctionClauseError) no function clause matching in Imcon.Auth.Api.generate_token_from_user/3
(imcon) lib/imcon/auth/auth_api.ex:57: Imcon.Auth.Api.generate_token_from_user(%Imcon.Auth.User{…

  import Joken

  alias Imcon.Auth.User

  @default_jwt_secret Application.get_env(:imcon, User)[:jwt_secret]

  def generate_token_from_user(user, expires \\ &default_expires/0, jwt_secret \\ @default_jwt_secret)
      when is_binary(jwt_secret) and is_function(expires) do
    %{user_id: user.id, username: Imcon.Auth.username(user)}
    |> generate_token(expires.(), jwt_secret)
  end

Can someone tell me where to dig. I do not have a stone flower.


#7

How does the function generate_token_from_user get called? I’m assuming it’s in the session controller so what are the contents of the create function in there?


#8
import Imcon.Auth.Api, only: [authenticate_user: 2]

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json", "xml"]
    plug :fetch_session
    plug Imcon.Auth.Api, repo: Imcon.Repo

  end

  scope "/api", ImconWeb do
    pipe_through [:api, :authenticate_user]

    resources "/channels", ChannelController, only: [:create, :index] do
      resources "/messages", MessageController, only: [:index]
      resources "/messages", MessageController, only: [], singleton: true do
        post "/read", ChannelController, :read, singleton: true
      end
    end

    resources "/direct_channels", DirectChannelController, only: [:index]
    post "/direct_channels/join", DirectChannelController, :join

    resources "/channel_users", ChannelUserController, only: [:create]
    resources "/user", UserController, only: [:index] do
    end
    
    scope "/v1" do

      get "/current_user", CurrentUserController, :show

      resources "/tree", TreeController, only: [:index, :create] do
        resources "/leaflet", LeafletController, only: [:show]
      end
    end
  end

  scope "/api", ImconWeb do
    pipe_through :api

    delete "/session", SessionController, :delete

    post "/registration", UserController, :create
    post "/session", SessionController, :create
  end

  scope "/", ImconWeb do
    pipe_through :browser # Use the default browser stack

    get "/*path", PageController, :index
  end
end

auth_api.ex

defmodule Imcon.Auth.Api do
  import Plug.Conn
  alias Comeonin.Bcrypt
  import Joken

  alias Imcon.Auth.User

  @default_jwt_secret Application.get_env(:imcon, User)[:jwt_secret]
  @default_expires_in 7 * 24 * 60 * 60 # 7 day

  def init(opts) do
    Keyword.fetch!(opts, :repo)
  end

  def call(conn, repo) do
    if conn.assigns[:current_user] do
      conn
    else
      user_id = get_user_id(conn)
      user = user_id && repo.get(User, user_id)
      assign(conn, :current_user, user)
    end
  end

  def authenticate_user(conn, _opts) do
    if conn.assigns[:current_user] do
      conn
    else
      conn
      |> put_status(:unauthorized)
      |> Phoenix.Controller.json(%{message: "Can't be authorized!"})
      |> halt
    end
  end

  def login(conn, user) do
    conn
    |> assign(:current_user, user)
    |> assign(:auth_token, generate_token_from_user(user))
  end

  def login_by_email_pass(conn, email, pass, opts) do
    repo = Keyword.fetch!(opts, :repo)
    user = repo.get_by(User, email: email)

    cond do
      user && Bcrypt.checkpw(pass, user.encrypted_password) ->
        {:ok, login(conn, user)}
      user ->
        {:error, :unauthorized, conn}
      true ->
        Bcrypt.dummy_checkpw()
        {:error, :not_found, conn}
    end
  end

  def generate_token_from_user(user, expires \\ &default_expires/0, jwt_secret \\ @default_jwt_secret)
      when is_binary(jwt_secret) and is_function(expires) do
    %{user_id: user.id, username: Imcon.Auth.username(user)}
    |> generate_token(expires.(), jwt_secret)
  end

  defp default_expires, do: :os.system_time(:seconds) + @default_expires_in

  def generate_token(data, expires, jwt_secret) do
    data
    |> token
    |> with_exp(expires)
    |> with_signer(hs256(jwt_secret))
    |> sign
    |> get_compact
  end

  # This only parse the token, doesn't validate exp
  def parse_token(token, jwt_secret \\ @default_jwt_secret)
  def parse_token("Bearer " <> token, jwt_secret) do
    parse_token(token, jwt_secret)
  end
  def parse_token(token, jwt_secret) do
    token
    |> token
    |> with_signer(hs256(jwt_secret))
  end

  defp get_user_id(conn) do
    case Plug.Conn.get_req_header(conn, "authorization") do
      [token] -> get_user_id_from_token(token)
      _       -> nil
    end
  end

  defp get_user_id_from_token(token) do
    case token
          |> parse_token
          |> with_validation("exp", &(&1 >= :os.system_time(:seconds)))
          |> verify
          |> get_claims
          |> Map.fetch("user_id") do
      {:ok, user_id} -> user_id
      _              -> nil
    end
  end

end

#9

I tried to make authorization without a Gardian, as in ExChat on Joken.


#10

Can you show the contents of the session controller file too?


#11
defmodule ImconWeb.SessionController do
  use ImconWeb, :controller

  alias Imcon.Auth.Api

  # plug :scrub_params, "session" when action in [:create]

  def create(conn, %{"email" => email, "password" => password}) do
    case Api.login_by_email_pass(conn, email, password, repo: Repo) do
      {:ok, conn} ->
        render(conn, token: conn.assigns[:auth_token])
      {:error, _reason, conn} ->
        conn
        |> put_status(:unauthorized)
        |> render(ErrorView, :error, message: "Неправильный адрес электронной почты или пароль!")
    end

  end

  def delete(conn, _params) do

    conn
    |> delete_session(:current_user_id)
    |> put_status(:unauthorized)
    |> render("delete.json")

  end

end