pow-auth / assent

Multi-provider framework in Elixir
https://powauth.com
MIT License
391 stars 45 forks source link

Using Tesla for HTTP #90

Closed bamorim closed 3 years ago

bamorim commented 3 years ago

Hey, first of all, thanks for the library, really good, but I have one question:

Do you think it could make sense to use Tesla as the HTTP abstraction so the library could use any of the Tesla adapters out there? Or we could have a an Assent.HTTPAdapter for Tesla, but then we would have adapter of the adapter xD.

Tesla currently have adapters for Finch, Gun, hackney, httpc, ibrowse and Mint.

Tesla also have a test adapter which might come in handy.

https://hexdocs.pm/tesla/Tesla.Adapter.html#content

danschultzer commented 3 years ago

I prefer Assent to have as few dependencies as possible. The reason why I included mint (as an optional dependency) is to have HTTP/2 support, but if there was native support in Elixir/OTP then I would opt for that instead.

It's very easy to set up a HTTP adapter for Assent. Just in case anyone finds this issue and wonder how it would look leveraging Tesla, the below logic will work using the default httpc adapter in Tesla (with SSL options enabled).

It should be pretty easy to adjust it from here using your own adapter, just remember that Assent will decode JSON automatically.

defmodule MyApp.Assent.TeslaHTTPAdapter do
  @behaviour Assent.HTTPAdapter

  alias Assent.HTTPAdapter.HTTPResponse

  def request(method, url, body, headers, opts \\ nil)
  def request(method, url, body, headers, nil) do
    %{host: host} = URI.parse(url)

    ssl_opts = [
      verify: :verify_peer,
      depth: 99,
      cacerts: :certifi.cacerts(),
      verify_fun: {&:ssl_verify_hostname.verify_fun/3, check_hostname: to_charlist(host)},
      customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
    ]

    request(method, url, body, headers, ssl: ssl_opts)
  end
  def request(method, url, body, headers, opts) do
    []
    |> Tesla.client({Tesla.Adapter.Httpc, opts})
    |> Tesla.request(method: method, url: url, body: body, headers: headers, opts: opts)
    |> case do
      {:ok, env} ->
        {:ok, %HTTPResponse{status: env.status, headers: env.headers, body: env.body}}

      {:error, error} ->
        {:error, error}
    end
  end
end
defmodule MyApp.Assent.TeslaHTTPAdapterTest do
  use ExUnit.Case

  alias ExUnit.CaptureLog
  alias Assent.HTTPAdapter.HTTPResponse
  alias MyApp.Assent.TeslaHTTPAdapter

  @wrong_host_certificate_url "https://wrong.host.badssl.com"
  @hsts_certificate_url "https://hsts.badssl.com"
  @unreachable_http_url "http://localhost:8888/"

  describe "request/4" do
    test "handles SSL" do
      assert {:ok, %HTTPResponse{status: 200}} = TeslaHTTPAdapter.request(:get, @hsts_certificate_url, nil, [])
      assert {:error, :econnrefused} = TeslaHTTPAdapter.request(:get, @wrong_host_certificate_url, nil, [])

      CaptureLog.capture_log(fn ->
        assert {:ok, %HTTPResponse{status: 200}} = TeslaHTTPAdapter.request(:get, @wrong_host_certificate_url, nil, [], ssl: [])
      end) =~ "Authenticity is not established by certificate path validatio"

      assert {:error, :econnrefused} = TeslaHTTPAdapter.request(:get, @unreachable_http_url, nil, [])
    end

    test "handles query in URL" do
      bypass = Bypass.open()

      Bypass.expect_once(bypass, "GET", "/get", fn conn ->
        assert conn.query_string == "a=1"

        Plug.Conn.send_resp(conn, 200, "")
      end)

      assert {:ok, %HTTPResponse{status: 200}} = TeslaHTTPAdapter.request(:get, "http://localhost:#{bypass.port}/get?a=1", nil, [])
    end

    test "handles POST" do
      bypass = Bypass.open()

      Bypass.expect_once(bypass, "POST", "/post", fn conn ->
        {:ok, body, conn} = Plug.Conn.read_body(conn, [])
        params = URI.decode_query(body)

        assert params["a"] == "1"
        assert params["b"] == "2"
        assert Plug.Conn.get_req_header(conn, "content-type") == ["application/x-www-form-urlencoded"]

        Plug.Conn.send_resp(conn, 200, "")
      end)

      assert {:ok, %HTTPResponse{status: 200}} = TeslaHTTPAdapter.request(:post, "http://localhost:#{bypass.port}/post", "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}])
    end
  end
end