pow-auth / pow

Robust, modular, and extendable user authentication system
https://powauth.com
MIT License
1.59k stars 153 forks source link

Replacing Phoenix.Token with JWT-based for signing and verifiying of tokens #669

Closed jeepers3327 closed 2 years ago

jeepers3327 commented 2 years ago

Is there a guide where I can make as reference on implementing JWT-based signing/verifying instead of Phoenix.Token? I've looked at Pow.Plug.MessageVerifier and based on that it seems I need to implement two functions sign/4 and verify/4.

I think it's okay to use Phoenix.Token when the API will only be consumed by a web app but I think its better to have change this one with JWT since other languages implemented it and it can be decoded in the client side.

I'm using Joken at the moment but no idea what would be the claims look like.

  @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))

Any docs or guides will probably help me setup this one.

Thank you.

danschultzer commented 2 years ago

It's trivial to update the signing mechanism by just updating the configuration with message_verifier: MyMessageVerifier, but since this is the API module I would just update it directly instead. Also one of the important advantages of using JWT is that it's stateless, so in this case I would reconsider the use of the session logic.

First let's keep using the session logic, and just encapsulate the stateful token in the a JWT:

defmodule MyAppWeb.APIAuthPlug do
  # ...

  defp sign_token(_conn, token, _config) do
    MyToken.generate_and_sign!(%{"sub" => token})
  end

  # ...

  defp verify_token(_conn, token, _config) do
    case MyToken.verify_and_validate(jwt) do
      {:ok, %{"sub" => token}} -> {:ok, token}
      {:error, error} -> {:error, error}
    end
  end

  # ...
end

As you can see this won't really do anything since JWT normally carries a bunch of user info. Of course you can just update the code to pass in the user with the token in sign_token/4, and then add any user info to the claims.

However, you should consider the alternative of using stateless token entirely instead of depending on the session logic:

defmodule MyAppWeb.APIAuthPlug do
  # ...

  def fetch(conn, config) do
    with {:ok, signed_token} <- fetch_access_token(conn),
         {:ok, %{"sub" => user_id}} <- verify_token(conn, signed_token, config) do
      {conn, Repo.get!(User, user_id)}
    else
      _any -> {conn, nil}
    end
  end

  def create(conn, user, config) do
    conn = Conn.put_private(conn, :api_access_token, sign_token(conn, user, config))

    {conn, user}
  end

  # ...

  defp sign_token(_conn, user, _config) do
    MyToken.generate_and_sign!(%{"sub" => user.id})
  end

  # ...

  defp verify_token(_conn, token, _config) do
    MyToken.verify_and_validate(token)
  end

  # ...
end

The renew/2 and delete/2 functions would be removed entirely, and the session controller should be updated to only have create/2. There is no renewal token in this case, the user would sign in again when the token expires or you could have an endpoint to generate a new JWT from existing valid JWT.

There are many aspects to this and you should consider the pros and cons of these approaches. It's dependent on your business logic, and security considerations.