How to create web browser fingerprints to be used when verifying JWT access tokens?

Thus, making sure that the token is being used by the same web browser used to authenticate in the first place?

Note: I am using Guardian.

I would advise You to inspect one of your conn. Put this inside any controller action

IO.inspect conn

This is an extract of a %Plug.Conn{}…

  req_headers: [
    {"host", "localhost:4000"},
    {"user-agent",
     "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0"},
    {"accept", "application/json"},
    {"accept-language", "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"},
    {"accept-encoding", "gzip, deflate"},
    {"referer", "http://localhost:4000/main"},
    {"authorization",
     "Bearer SFMyNTY.g3QAAAACZAAEZGF0YWEBZAAGc2lnbmVkbgYA5ygrVWEB.kIFM_OUaENG973G2tVVu0HvRTe6UA3S0RUh-4_lniuQ"},
    {"connection", "keep-alive"},
    {"cache-control", "max-age=0"}
  ],

You can get user agent here. You can also see an authorization token.

Next step is to see how You sign your token, here is my way, with Phoenix.Token, but it does not matter if You sign this with Guardian.

  @spec sign(term) :: String.t()
  def sign(user), do: Phoenix.Token.sign(NextWeb.Endpoint, @salt, user.id)

Here You would add the user agent hash, or anything related, with the user id.

Inside the login controller, after login, You can sign token, and send it back to user.

    case Accounting.authenticate(session_params) do
      {:ok, user} ->
        token = sign(user)

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

      {:error, _reason} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render("error.json")
    end

Almost there, now You need the plugs. You can write your own, or use the one coming with guardian.

Check for those plugs:

  • verify_header
  • ensure_authenticated

But I guess it is quite a lot of work for no real security gain. If You can steal a token, You might as well steal the user agent string :slight_smile:

And You might not want a nightly browser build to invalidate your tokens.

4 Likes

@kokolegorille thank for the detailed response. I have seen the idea of using browser fingerprints here:

https://blog.imaginea.com/stateless-authentication-implementation-using-jwt-nginxlua-and-memcached/

But still cannot understand how to generate them client side. Maybe, as you have said that they are simply hashes of the user agent header value.

It does not look too different from how I authenticate my users with phoenix. Except for the memcached part. This could be replaced by a GenServer I think…

If your problem is frontend logic… I cannot tell You, as You use Vue.js, and me React

1 Like

@kokolegorille, what I understand is that it is all about hashing the user agent inside the token and verifying server side that the user agent of each subsequent request matches against the hashed agent inside the token? Correct?

I guess that indeed adds some protection against token hijacking.

Yes it does, but it also includes checking that the user-agent does not change, because that would invalidate the user access.

This one of my plug to check the token

defmodule NextWeb.Plug.EnsureAuthenticated do
  import Plug.Conn
  import NextWeb.TokenHelpers

  def init(opts \\ []) do
    opts
  end

  def call(conn, _opts) do
    token = conn.assigns[:token]

    with {:ok, user_id} <- verify_token(token) do
      conn |> assign(:user_id, user_id)
    else
      {:error, _reason} ->
        conn
        |> put_status(:unauthorized)
        |> put_resp_content_type("application/json")
        |> send_resp(401, Poison.encode!(%{error: "Unauthorized"}))
        |> halt()
    end
  end
end

What You could do is

  def call(conn, _opts) do
    token = conn.assigns[:token]

    with {:ok, {user_id, user_agent}} <- verify_token(token),
      :ok <- check_user_agent_is_ok(user_agent) do
      conn |> assign(:user_id, user_id, user_agent: user_agent)
    else
      {:error, _reason} ->
        conn
        |> put_status(:unauthorized)
        |> put_resp_content_type("application/json")
        |> send_resp(401, Poison.encode!(%{error: "Unauthorized"}))
        |> halt()
    end
  end

Something like this I guess.