gpreston

gpreston

Socket authentication with Pow

I’m using Pow for user authentication and I’m having trouble figuring out how to use it with sockets. I want my sockets to pass a token on connect and use that to authorize. I have an APIAuthPlug module that follows the guide in the documentation for implementing Pow on an API. This module uses Pow.Plug.verify_token/4 to determine if a token that was passed is valid. The issue is the first parameter is a Plug.Conn struct, not a Phoenix.Socket, so what function would I use to do authentication? I use Pow.Plug.sign_token/4 to generate the tokens in the first place, so would I not be able to reuse the same API tokens for sockets?

Marked As Solved

danschultzer

danschultzer

Pow Core Team

:man_facepalming: I should have have tested this, yeah, I added a fingerprint, here’s the whole module again, and should be working:

defmodule MyAppWeb.ApiAuthPlug do
  @moduledoc false
  use Pow.Plug.Base

  alias Plug.Conn
  alias Pow.{Config, Plug, Store.CredentialsCache}
  alias PowPersistentSession.Store.PersistentSessionCache
  alias MyAppWeb.Endpoint

  @doc """
  Fetches the user from access token.
  """
  @impl true
  @spec fetch(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  def fetch(conn, config) do
    with {:ok, signed_token} <- fetch_access_token(conn),
         {user, _metadata}   <- get_credentials(conn, signed_token, config) do
      {conn, user}
    else
      _any -> {conn, nil}
    end
  end

  @spec get_credentials(Conn.t(), binary(), Config.t()) :: map() | nil
  def get_credentials(conn, signed_token, config) do
    with {:ok, token}     <- verify_token(conn, signed_token, config),
         {user, metadata} <- CredentialsCache.get(store_config(config), token) do
      {user, metadata}
    else
      _any -> nil
    end
  end

  @doc """
  Creates an access and renewal token for the user.

  The tokens are added to the `conn.private` as `:api_access_token` and
  `:api_renewal_token`. The renewal token is stored in the access token
  metadata and vice versa.

  Both tokens will also store a fingerprint in the metadata that's either
  fetched from `conn.private[:pow_api_session_fingerprint]` or randomly
  generated.
  """
  @impl true
  @spec create(Conn.t(), map(), Config.t()) :: {Conn.t(), map()}
  def create(conn, user, config) do
    store_config  = store_config(config)
    access_token  = Pow.UUID.generate()
    fingerprint   = conn.private[:pow_api_session_fingerprint] || Pow.UUID.generate()
    renewal_token = Pow.UUID.generate()
    conn          =
      conn
      |> Conn.put_private(:api_access_token, sign_token(conn, access_token, config))
      |> Conn.put_private(:api_renewal_token, sign_token(conn, renewal_token, config))

    CredentialsCache.put(store_config, access_token, {user, fingerprint: fingerprint, renewal_token: renewal_token})
    PersistentSessionCache.put(store_config, renewal_token, {[id: user.id], fingerprint: fingerprint, access_token: access_token})

    {conn, user}
  end

  @doc """
  Delete the access token from the cache.

  The renewal token is deleted by fetching it from the access token metadata.
  """
  @impl true
  @spec delete(Conn.t(), Config.t()) :: Conn.t()
  def delete(conn, config) do
    store_config = store_config(config)

    with {:ok, signed_token} <- fetch_access_token(conn),
         {:ok, token}        <- verify_token(conn, signed_token, config),
         {_user, metadata}   <- CredentialsCache.get(store_config, token) do

      PersistentSessionCache.delete(store_config, metadata[:renewal_token])
      CredentialsCache.delete(store_config, token)

      Endpoint.broadcast("users_socket:" <> metadata[:fingerprint], "disconnect", %{})
    else
      _any -> :ok
    end

    conn
  end

  @doc """
  Creates new tokens using the renewal token.

  The access token, if any, will be deleted by fetching it from the renewal
  token metadata. The renewal token will be deleted from the store after the
  it has been fetched.

  `:pow_api_session_fingerprint` will be set in `conn.private` with the
  `:fingerprint` fetched from the metadata, to ensure it will be persisted in
  the tokens generated in `create/2`.
  """
  @spec renew(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  def renew(conn, config) do
    store_config = store_config(config)

    with {:ok, signed_token} <- fetch_access_token(conn),
         {:ok, token}        <- verify_token(conn, signed_token, config),
         {clauses, metadata} <- PersistentSessionCache.get(store_config, token) do

      CredentialsCache.delete(store_config, metadata[:access_token])
      PersistentSessionCache.delete(store_config, token)

      conn
      |> Conn.put_private(:pow_api_session_fingerprint, metadata[:fingerprint])
      |> load_and_create_session({clauses, metadata}, config)
    else
      _any -> {conn, nil}
    end
  end

  defp load_and_create_session(conn, {clauses, _metadata}, config) do
    case Pow.Operations.get_by(clauses, config) do
      nil  -> {conn, nil}
      user -> create(conn, user, config)
    end
  end

  defp sign_token(conn, token, config) do
    Plug.sign_token(conn, signing_salt(), token, config)
  end

  defp signing_salt(), do: Atom.to_string(__MODULE__)

  defp fetch_access_token(conn) do
    case Conn.get_req_header(conn, "authorization") do
      [token | _rest] -> {:ok, token}
      _any            -> :error
    end
  end

  defp verify_token(conn, token, config),
    do: Plug.verify_token(conn, signing_salt(), token, config)

  defp store_config(config) do
    backend = Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache)

    [backend: backend]
  end
end

As you can see in the above I also close all sockets when delete is called. I ran the above through the PowDemo API app to be sure it works :slight_smile:

Also Liked

danschultzer

danschultzer

Pow Core Team

Here’s what I do in a production app.

Update the fetch/2 method so we can expose a get_credentials/3 method to the APIAuthPlug:

defmodule MyAppWeb.APIAuthPlug do
  @moduledoc false
  use Pow.Plug.Base

  alias Plug.Conn
  alias Pow.{Config, Plug, Store.CredentialsCache}
  alias PowPersistentSession.Store.PersistentSessionCache

  @doc """
  Fetches the user from access token.
  """
  @impl true
  @spec fetch(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  def fetch(conn, config) do
    with {:ok, signed_token} <- fetch_access_token(conn),
         {:ok, token}        <- get_credentials(conn, signed_token, config) do
      {conn, user}
    else
      _any -> {conn, nil}
    end
  end

  @spec get_credentials(Conn.t(), binary(), Config.t()) :: map() | nil
  def get_credentials(conn, signed_token, config) do
    with {:ok, token}      <- verify_token(conn, signed_token, config),
         {user, _metadata} <- CredentialsCache.get(store_config(config), token) do
      {conn, user}
    else
      _any -> {conn, nil}
    end
  end

  @doc """
  Creates an access and renewal token for the user.

  The tokens are added to the `conn.private` as `:api_access_token` and
  `:api_renewal_token`. The renewal token is stored in the access token
  metadata and vice versa.
  """
  @impl true
  @spec create(Conn.t(), map(), Config.t()) :: {Conn.t(), map()}
  def create(conn, user, config) do
    store_config  = store_config(config)
    access_token  = Pow.UUID.generate()
    renewal_token = Pow.UUID.generate()
    conn          =
      conn
      |> Conn.put_private(:api_access_token, sign_token(conn, access_token, config))
      |> Conn.put_private(:api_renewal_token, sign_token(conn, renewal_token, config))

    CredentialsCache.put(store_config, access_token, {user, [renewal_token: renewal_token]})
    PersistentSessionCache.put(store_config, renewal_token, {[id: user.id], [access_token: access_token]})

    {conn, user}
  end

  @doc """
  Delete the access token from the cache.

  The renewal token is deleted by fetching it from the access token metadata.
  """
  @impl true
  @spec delete(Conn.t(), Config.t()) :: Conn.t()
  def delete(conn, config) do
    store_config = store_config(config)

    with {:ok, signed_token} <- fetch_access_token(conn),
         {:ok, token}        <- verify_token(conn, signed_token, config),
         {_user, metadata}   <- CredentialsCache.get(store_config, token) do

      PersistentSessionCache.delete(store_config, metadata[:renewal_token])
      CredentialsCache.delete(store_config, token)
    else
      _any -> :ok
    end

    conn
  end

  @doc """
  Creates new tokens using the renewal token.

  The access token, if any, will be deleted by fetching it from the renewal
  token metadata. The renewal token will be deleted from the store after the
  it has been fetched.
  """
  @spec renew(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  def renew(conn, config) do
    store_config = store_config(config)

    with {:ok, signed_token} <- fetch_access_token(conn),
         {:ok, token}        <- verify_token(conn, signed_token, config),
         {clauses, metadata} <- PersistentSessionCache.get(store_config, token) do

      CredentialsCache.delete(store_config, metadata[:access_token])
      PersistentSessionCache.delete(store_config, token)

      load_and_create_session(conn, {clauses, metadata}, config)
    else
      _any -> {conn, nil}
    end
  end

  defp load_and_create_session(conn, {clauses, _metadata}, config) do
    case Pow.Operations.get_by(clauses, config) do
      nil  -> {conn, nil}
      user -> create(conn, user, config)
    end
  end

  defp sign_token(conn, token, config) do
    Plug.sign_token(conn, signing_salt(), token, config)
  end

  defp signing_salt(), do: Atom.to_string(__MODULE__)

  defp fetch_access_token(conn) do
    case Conn.get_req_header(conn, "authorization") do
      [token | _rest] -> {:ok, token}
      _any            -> :error
    end
  end

  defp verify_token(conn, token, config),
    do: Plug.verify_token(conn, signing_salt(), token, config)

  defp store_config(config) do
    backend = Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache)

    [backend: backend]
  end
end

Then this can be used in the UserSocket:

  def connect(%{"token" => token} = _params, socket, %{pow_config: config}) do
    %Plug.Conn{secret_key_base: socket.endpoint.config(:secret_key_base)}
    |> ApiAuthPlug.get_credentials(token, config)
    |> case do
      nil -> :error

      {user, metadata} ->
        fingerprint = Keyword.fetch!(metadata, :fingerprint)
        socket      =
          socket
          |> assign(:session_fingerprint, fingerprint)
          |> assign(:user_id, user.id)

        {:ok, socket}
    end
  end
  # ...

  def id(%{assigns: %{session_fingerprint: session_fingerprint}}), do: "user_socket:#{session_fingerprint}"

Notice that I’m passing pow_config in the connect_info. This is done in the endpoint:

  @pow_config otp_app: :my_app

  # ...

  socket("/user_socket", MyAppWeb.UserSocket,
    websocket: [
      connect_info: [pow_config: @pow_config]
    ]
  )

  # ...

  plug Pow.Plug.Session, @pow_config

I’m working on a Pow.Phoenix.Socket module to help with all this though :smile:

mindok

mindok

Have you had a read through https://github.com/danschultzer/pow/issues/271 ? I don’t know if that covers your use case - there are a few options presented there.

Where Next?

Popular in Questions Top

gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

We're in Beta

About us Mission Statement