pow-auth / pow_assent

Multi-provider authentication for your Pow enabled app
https://powauth.com
MIT License
318 stars 50 forks source link

[Potential bug]: Callback creates user but returns nil for access and refresh tokens #215

Closed jacobwarren closed 1 year ago

jacobwarren commented 2 years ago

Environment:

I'm currently running PowAssent to authenticate users on a React front-end to my Elixir backend. I can successfully create the user, but the access and refresh tokens both return nil, and upon returning to login, already registered users trip the following error:

Request: POST /v1/auth/facebook/callback
** (exit) an exception was raised:
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not a binary
:erlang.binary_to_term(nil)
(elixir 1.12.2) lib/enum.ex:1582: Enum."-map/2-lists^map/1-0-"/2
(api 1.0.0) lib/api_web/redis_cache.ex:124: APIWeb.Pow.RedisCache.populate_values/2
(api 1.0.0) lib/api_web/redis_cache.ex:94: APIWeb.Pow.RedisCache.do_scan/3
(elixir 1.12.2) lib/stream.ex:1517: anonymous fn/5 in Stream.resource/3
(elixir 1.12.2) lib/enum.ex:3952: Enum.reverse/1
(elixir 1.12.2) lib/enum.ex:3311: Enum.to_list/1
(pow 1.0.24) lib/pow/store/credentials_cache.ex:87: Pow.Store.CredentialsCache.fetch_sessions/3
(pow 1.0.24) lib/pow/store/credentials_cache.ex:237: Pow.Store.CredentialsCache.do_delete_user_sessions_with_fingerprint/3
(pow 1.0.24) lib/pow/store/credentials_cache.ex:119: Pow.Store.CredentialsCache.put/3
(pow 1.0.24) lib/pow/plug/session.ex:205: anonymous fn/6 in Pow.Plug.Session.before_send_create/3
(elixir 1.12.2) lib/enum.ex:2385: Enum."-reduce/3-lists^foldl/2-0-"/3
(plug 1.12.1) lib/plug/conn.ex:1690: Plug.Conn.run_before_send/2
(plug 1.12.1) lib/plug/conn.ex:399: Plug.Conn.send_resp/1
(api 1.0.0) lib/api_web/controllers/api/auth_controller.ex:1: APIWeb.API.AuthController.action/2
(api 1.0.0) lib/api_web/controllers/api/auth_controller.ex:1: APIWeb.API.AuthController.phoenix_controller_pipeline/2
(phoenix 1.5.10) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2
(api 1.0.0) lib/api_web/endpoint.ex:1: APIWeb.Endpoint.plug_builder_call/2
(api 1.0.0) lib/plug/debugger.ex:136: APIWeb.Endpoint."call (overridable 3)"/2
(api 1.0.0) lib/api_web/endpoint.ex:1: APIWeb.Endpoint.call/2
[error] CRASH REPORT Process <0.7216.0> with 0 neighbours exited with reason: {{{{#{'__exception__' => true,'__struct__' => 'Elixir.ArgumentError',message => <<"errors were found at the given arguments:\n\n  * 1st argument: not a binary\n">>},[{erlang,binary_to_term,[nil],[{error_info,#{module => erl_erts_errors}}]},{'Elixir.Enum','-map/2-lists^map/1-0-',2,[{file,"lib/enum.ex"},{line,1582}]},{'Elixir.APIWeb.Pow.RedisCache',populate_values,2,[{file,"lib/api_web/redis_cache.ex"},{line,124}]},{'Elixir.APIWeb.Pow.RedisCache',do_scan,3,[{file,"lib/api_web/redis_cache.ex"},...]},...]},...},...},...}
[error] Cowboy stream 7 with ranch listener 'Elixir.APIWeb.Endpoint.HTTP' and connection process <0.7202.0> had its request process exit with reason: {{{#{'__exception__' => true,'__struct__' => 'Elixir.ArgumentError',message => <<"errors were found at the given arguments:\n\n  * 1st argument: not a binary\n">>},[{erlang,binary_to_term,[nil],[{error_info,#{module => erl_erts_errors}}]},{'Elixir.Enum','-map/2-lists^map/1-0-',2,[{file,"lib/enum.ex"},{line,1582}]},{'Elixir.APIWeb.Pow.RedisCache',populate_values,2,[{file,"lib/api_web/redis_cache.ex"},{line,124}]},{'Elixir.APIWeb.Pow.RedisCache',do_scan,3,[{file,"lib/api_web/redis_cache.ex"},{line,...}]},...]},...},...}

Upon successful registration, while the user is created, the token is not returned, so I'm not sure if can trust the user. Here is the shape of the returned object:

{data: {…}, status: 200, statusText: "", headers: {…}, config: {…}, …}
config: {url: "https://[obfuscated].com/register", method: "post", data: "{\"session_params\":{\"state\":\"33ae4d9b8060fe2df33ee8…be4qjxTfZw3x8e_8iy3KO28z-qilwouUI6un_R9TVwIxpIw\"}", headers: {…}, transformRequest: Array(1), …}
data:
data:
access_token: null
renewal_token: null
[[Prototype]]: Object

[[Prototype]]: Object

headers: {cache-control: "max-age=0, private, must-revalidate", content-length: "46", content-type: "application/json; charset=utf-8"}
request: XMLHttpRequest {readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, onreadystatechange: ƒ, …}
status: 200
statusText: ""

On the backend, here's what I'm running with.

Config:

config :api, :pow,
  repo: API.Repo,
  user: API.Models.User,
  users_context: API.Users,
  mailer_backend: Notifications.Mailer,
  cache_store_backend: APIWeb.Pow.RedisCache,
  extensions: [PowEmailConfirmation, PowResetPassword, PowInvitation, PowPersistentSession],
  controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks

config :api, :pow_assent,
  providers: [
    facebook: [
      client_id: System.get_env("FB_CLIENT_ID"),
      client_secret: System.get_env("FB_CLIENT_SECRET"),
      authorization_params: [scope: "public_profile,email,pages_show_list"],
      strategy: Assent.Strategy.Facebook
    ]
  ]

...

Controller for handling auth requests and callbacks:

defmodule APIWeb.API.AuthController do
  use APIWeb, :controller

  alias Plug.Conn
  alias PowAssent.Plug
  alias PowAssent.Ecto.UserIdentities.Context

  def recaptcha(conn, params) do
    IO.inspect [token: params["token"]]
    case Recaptcha.verify(params["token"]) do
      {:ok, response} ->
        Conn.send_resp(conn, 200, "")
      {:error, errors} ->
        Conn.send_resp(conn, 404, "")
    end
  end

  @spec new(Conn.t(), map()) :: Conn.t()
  def new(conn, %{"provider" => provider}) do
    conn
    |> Plug.authorize_url(provider, redirect_uri(conn))
    |> case do
      {:ok, url, conn} ->
        json(conn, %{data: %{url: url, session_params: conn.private[:pow_assent_session_params]}})

      {:error, _error, conn} ->
        conn
        |> put_status(500)
        |> json(%{error: %{status: 500, message: "An unexpected error occurred"}})
    end
  end

  defp redirect_uri(conn) do
    "https://[obfuscated].com/register"
  end

  @spec callback(Conn.t(), map()) :: Conn.t()
  def callback(conn, %{"provider" => provider} = params) do
    session_params = Map.fetch!(params, "session_params")
    params         = Map.drop(params, ["provider", "session_params"])

    conn
    |> Conn.put_private(:pow_assent_session_params, session_params)
    |> Plug.callback_upsert(provider, params, "")
    |> case do
      {:ok, conn} ->
        # return current user by api_access_token
        json(conn, %{data: %{access_token: conn.private[:api_access_token], renewal_token: conn.private[:api_renewal_token]}})

      {:error, conn} ->
        IO.inspect [ERROR_CON: conn]

        conn
        |> put_status(500)
        |> json(%{error: %{status: 500, message: "An unexpected error occurred"}})
    end
  end
end

On the frontend here is the code used to trigger the Facebook dialog:

<FacebookLogin
  appId="___"
  autoLoad
  callback={props.responseFacebook}
  cookie={true}
  xfbml={true}
  scope="public_profile,email,pages_show_list"
  redirectUri="https://[obfuscated].com/register"
  render={renderProps => (
    <button
      type="button"
      onClick={() => {
        props.authorizeFacebook();
        renderProps.onClick();
      }}
      className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
    >
      <span className="sr-only">Sign in with Facebook</span>
      <svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
        <path
          fillRule="evenodd"
          d="M20 10c0-5.523-4.477-10-10-10S0 4.477 0 10c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V10h2.54V7.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V10h2.773l-.443 2.89h-2.33v6.988C16.343 19.128 20 14.991 20 10z"
          clipRule="evenodd"
        />
      </svg>
    </button>
  )}
/>

Regular authentication works perfectly using email and password. I then store a cookie through GraphQL for those users.

I'm not sure if my think on OAuth is off, but I'm expecting to get an access and a refresh token back possessing the user's Oauth creds so I can know that I can trust them as well as find their user much like I do via my auth plug with:

def fetch(conn, config) do
  token = fetch_auth_token(conn)

  config
  |> store_config()
  |> CredentialsCache.get(token)
  |> case do
    :not_found        -> {conn, nil}
    {user, _metadata} -> {conn, user}
  end
end

Am I thinking about this correctly? Thank you!

joshuataylor commented 2 years ago

I'm also getting this.

danschultzer commented 2 years ago

There seems to be several things going on here. First can you try to update the RedisCache to the latest revision: https://hexdocs.pm/pow/1.0.25/redis_cache_store_backend.html#content

That should take care of the nil raise error. After this, could you update the AuthController to raise error on missing keys: https://github.com/pow-auth/pow_assent/commit/5b18531d8590e2b28ca8d3e8c10f22a8ba0eb58f

Finally can you ensure that the APIAuthPlug is correct: https://hexdocs.pm/pow/1.0.25/api.html#content

Let me know if this resolve it, and thanks for your patience!