diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d97672b --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,12 @@ +# Used by "mix format" and to export configuration. +export_locals_without_parens = [ + plug: 1, + plug: 2, + get: 2 +] + +[ + inputs: ["{mix,.formatter}.exs", "{integration,lib,test}/**/*.{ex,exs}"], + locals_without_parens: export_locals_without_parens, + export: [locals_without_parens: export_locals_without_parens] +] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5055920..04bf6a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,20 @@ on: workflow_call: jobs: + lint: + env: + MIX_ENV: test + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: 26.0 + elixir-version: 1.15 + - run: mix deps.get + - run: mix compile --warnings-as-errors + - run: mix credo --strict --ignore design.tagtodo + - run: mix format --check-formatted + test: strategy: matrix: @@ -32,4 +46,3 @@ jobs: - run: mix deps.get - run: mix compile --warnings-as-errors - run: mix test - - run: mix credo --ignore design.tagtodo diff --git a/integration/server.exs b/integration/server.exs index 0a56c09..4172c9d 100644 --- a/integration/server.exs +++ b/integration/server.exs @@ -23,16 +23,17 @@ defmodule IntegrationServer.Router do {:ok, modules} = :application.get_key(:assent, :modules) @path_modules modules - |> Enum.map(&{Module.split(&1), &1}) - |> Enum.map(fn - {["Assent", "Strategy", uri], module} -> {uri, module} - _any -> nil - end) - |> Enum.reject(&is_nil/1) - |> Enum.into(%{}) - - defp secret_key_base(conn, _opts), - do: %{conn | secret_key_base: "LG8WiSkAlUlwVJpISmRYsi7aJV/Qlv65FXyxwWXxp1QUzQY3hzEfg73YKfKZPpe0"} + |> Enum.map(&{Module.split(&1), &1}) + |> Enum.map(fn + {["Assent", "Strategy", uri], module} -> {uri, module} + _any -> nil + end) + |> Enum.reject(&is_nil/1) + |> Enum.into(%{}) + + defp secret_key_base(conn, _opts) do + %{conn | secret_key_base: "LG8WiSkAlUlwVJpISmRYsi7aJV/Qlv65FXyxwWXxp1QUzQY3hzEfg73YKfKZPpe0"} + end get "/" do list = @@ -50,7 +51,7 @@ defmodule IntegrationServer.Router do {:ok, %{url: url, session_params: session_params}} = module.authorize_url(config!(provider)) - Logger.info("Redirecting to #{inspect url} with session params #{inspect session_params}") + Logger.info("Redirecting to #{inspect(url)} with session params #{inspect(session_params)}") html = Plug.HTML.html_escape(url) body = "You are being redirected." diff --git a/lib/assent.ex b/lib/assent.ex index 8dafe08..7d02a94 100644 --- a/lib/assent.ex +++ b/lib/assent.ex @@ -9,7 +9,7 @@ defmodule Assent do defexception [:key] def message(exception) do - "CSRF detected with param key #{inspect exception.key}" + "CSRF detected with param key #{inspect(exception.key)}" end end @@ -17,8 +17,8 @@ defmodule Assent do defexception [:expected_key, :params] def message(exception) do - expected_key = inspect exception.expected_key - params = inspect Map.keys(exception.params) + expected_key = inspect(exception.expected_key) + params = inspect(Map.keys(exception.params)) "Expected #{expected_key} in params, got: #{params}" end @@ -75,11 +75,11 @@ defmodule Assent do """ The server was unreachable. - HTTP Adapter: #{inspect exception.http_adapter} + HTTP Adapter: #{inspect(exception.http_adapter)} Request URL: #{url} Reason: - #{inspect exception.reason} + #{inspect(exception.reason)} """ end end diff --git a/lib/assent/config.ex b/lib/assent/config.ex index 0f0ce41..a01e746 100644 --- a/lib/assent/config.ex +++ b/lib/assent/config.ex @@ -9,7 +9,7 @@ defmodule Assent.Config do defexception [:key] def message(exception) do - "Key #{inspect exception.key} not found in config" + "Key #{inspect(exception.key)} not found in config" end end @@ -22,7 +22,7 @@ defmodule Assent.Config do def fetch(config, key) do case Keyword.fetch(config, key) do {:ok, value} -> {:ok, value} - :error -> {:error, MissingKeyError.exception(key: key)} + :error -> {:error, MissingKeyError.exception(key: key)} end end diff --git a/lib/assent/http_adapter.ex b/lib/assent/http_adapter.ex index df34866..a486c3e 100644 --- a/lib/assent/http_adapter.ex +++ b/lib/assent/http_adapter.ex @@ -33,13 +33,13 @@ defmodule Assent.HTTPAdapter do """ @type header :: {binary(), binary()} - @type t :: %__MODULE__{ - http_adapter: atom(), - request_url: binary(), - status: integer(), - headers: [header()], - body: binary() | term() - } + @type t :: %__MODULE__{ + http_adapter: atom(), + request_url: binary(), + status: integer(), + headers: [header()], + body: binary() | term() + } defstruct http_adapter: nil, request_url: nil, status: 200, headers: [], body: "" @@ -47,7 +47,7 @@ defmodule Assent.HTTPAdapter do [request_url | _rest] = String.split(response.request_url, "?", parts: 2) """ - HTTP Adapter: #{inspect response.http_adapter} + HTTP Adapter: #{inspect(response.http_adapter)} Request URL: #{request_url} Response status: #{response.status} @@ -56,7 +56,7 @@ defmodule Assent.HTTPAdapter do #{Enum.reduce(response.headers, "", fn {k, v}, acc -> acc <> "\n#{k}: #{v}" end)} Response body: - #{inspect response.body} + #{inspect(response.body)} """ end end @@ -65,7 +65,8 @@ defmodule Assent.HTTPAdapter do @type body :: binary() | nil @type headers :: [{binary(), binary()}] - @callback request(method(), binary(), body(), headers(), Keyword.t()) :: {:ok, map()} | {:error, any()} + @callback request(method(), binary(), body(), headers(), Keyword.t()) :: + {:ok, map()} | {:error, any()} @doc """ Sets a user agent header diff --git a/lib/assent/http_adapter/finch.ex b/lib/assent/http_adapter/finch.ex index aa16b05..a3b93b0 100644 --- a/lib/assent/http_adapter/finch.ex +++ b/lib/assent/http_adapter/finch.ex @@ -1,35 +1,46 @@ if Code.ensure_loaded?(Finch) do -defmodule Assent.HTTPAdapter.Finch do - @moduledoc """ - HTTP adapter module for making http requests with Finch. + defmodule Assent.HTTPAdapter.Finch do + @moduledoc """ + HTTP adapter module for making http requests with Finch. - The Finch adapter must be configured with the supervisor by passing it as an - option: + The Finch adapter must be configured with the supervisor by passing it as an + option: - http_adapter: {Assent.HTTPAdapter.Finch, [supervisor: MyFinch]} + http_adapter: {Assent.HTTPAdapter.Finch, [supervisor: MyFinch]} - See `Assent.HTTPAdapter` for more. - """ - alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse} + See `Assent.HTTPAdapter` for more. + """ + alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse} - @behaviour HTTPAdapter + @behaviour HTTPAdapter - @impl HTTPAdapter - def request(method, url, body, headers, finch_opts \\ nil) do - headers = headers ++ [HTTPAdapter.user_agent_header()] - opts = finch_opts || [] + @impl HTTPAdapter + def request(method, url, body, headers, finch_opts \\ nil) do + headers = headers ++ [HTTPAdapter.user_agent_header()] + opts = finch_opts || [] - supervisor = Keyword.get(opts, :supervisor) || raise "Missing `:supervisor` option for the #{inspect __MODULE__} configuration" - build_opts = Keyword.get(opts, :build, []) - request_opts = Keyword.get(opts, :request, []) + supervisor = + Keyword.get(opts, :supervisor) || + raise "Missing `:supervisor` option for the #{inspect(__MODULE__)} configuration" - method - |> Finch.build(url, headers, body, build_opts) - |> Finch.request(supervisor, request_opts) - |> case do - {:ok, response} -> {:ok, %HTTPResponse{status: response.status, headers: response.headers, body: response.body}} - {:error, error} -> {:error, error} + build_opts = Keyword.get(opts, :build, []) + request_opts = Keyword.get(opts, :request, []) + + method + |> Finch.build(url, headers, body, build_opts) + |> Finch.request(supervisor, request_opts) + |> case do + {:ok, response} -> + {:ok, + %HTTPResponse{ + status: response.status, + headers: response.headers, + body: response.body + }} + + {:error, error} -> + {:error, error} + end end end end -end diff --git a/lib/assent/http_adapter/httpc.ex b/lib/assent/http_adapter/httpc.ex index cc8f327..781cf30 100644 --- a/lib/assent/http_adapter/httpc.ex +++ b/lib/assent/http_adapter/httpc.ex @@ -24,7 +24,7 @@ defmodule Assent.HTTPAdapter.Httpc do headers = headers ++ [HTTPAdapter.user_agent_header()] request = httpc_request(url, body, headers) - opts = parse_httpc_ssl_opts(httpc_opts, url) + opts = parse_httpc_ssl_opts(httpc_opts, url) method |> :httpc.request(request, opts, []) @@ -32,8 +32,9 @@ defmodule Assent.HTTPAdapter.Httpc do end defp raise_on_missing_httpc! do - Code.ensure_loaded?(:httpc) || raise """ - #{inspect __MODULE__} requires `:httpc` to be included in your + Code.ensure_loaded?(:httpc) || + raise """ + #{inspect(__MODULE__)} requires `:httpc` to be included in your application. Please add `:inets` to `:extra_applications`: @@ -51,8 +52,8 @@ defmodule Assent.HTTPAdapter.Httpc do end defp httpc_request(url, body, headers) do - url = to_charlist(url) - headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end) + url = to_charlist(url) + headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end) do_httpc_request(url, body, headers) end @@ -60,29 +61,36 @@ defmodule Assent.HTTPAdapter.Httpc do defp do_httpc_request(url, nil, headers) do {url, headers} end + defp do_httpc_request(url, body, headers) do {content_type, headers} = split_content_type_headers(headers) - body = to_charlist(body) + body = to_charlist(body) {url, headers, content_type, body} end defp split_content_type_headers(headers) do - case List.keytake(headers, 'content-type', 0) do - nil -> {'text/plain', headers} + case List.keytake(headers, ~c"content-type", 0) do + nil -> {~c"text/plain", headers} {{_, ct}, headers} -> {ct, headers} end end defp format_response({:ok, {{_, status, _}, headers, body}}) do - headers = Enum.map(headers, fn {key, value} -> {String.downcase(to_string(key)), to_string(value)} end) - body = IO.iodata_to_binary(body) + headers = + Enum.map(headers, fn {key, value} -> + {String.downcase(to_string(key)), to_string(value)} + end) + + body = IO.iodata_to_binary(body) {:ok, %HTTPResponse{status: status, headers: headers, body: body}} end + defp format_response({:error, error}), do: {:error, error} defp parse_httpc_ssl_opts(nil, url), do: parse_httpc_ssl_opts([], url) + defp parse_httpc_ssl_opts(opts, url) do uri = URI.parse(url) @@ -113,21 +121,30 @@ defmodule Assent.HTTPAdapter.Httpc do # OTP >= 22 hostname_match_check = try do - [customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]] + [ + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ] + ] rescue _e in UndefinedFunctionError -> [] end - Keyword.merge([ - verify: :verify_peer, - depth: 99, - verify_fun: {&:ssl_verify_hostname.verify_fun/3, check_hostname: to_charlist(uri.host)} - ] ++ hostname_match_check, ssl_opts) - end + Keyword.merge( + [ + verify: :verify_peer, + depth: 99, + verify_fun: + {&:ssl_verify_hostname.verify_fun/3, check_hostname: to_charlist(uri.host)} + ] ++ hostname_match_check, + ssl_opts + ) + end end defp raise_on_missing_ssl_verify_fun! do - Code.ensure_loaded?(:ssl_verify_hostname) || raise """ + Code.ensure_loaded?(:ssl_verify_hostname) || + raise """ This request can NOT be verified for valid SSL certificate. Please add `:ssl_verify_fun` to your projects dependencies: @@ -158,7 +175,8 @@ defmodule Assent.HTTPAdapter.Httpc do end defp raise_on_missing_certifi! do - Code.ensure_loaded?(:certifi) || raise """ + Code.ensure_loaded?(:certifi) || + raise """ This request requires a CA trust store. Please add `:certifi` to your projects dependencies: diff --git a/lib/assent/http_adapter/mint.ex b/lib/assent/http_adapter/mint.ex index fe14722..f7d9741 100644 --- a/lib/assent/http_adapter/mint.ex +++ b/lib/assent/http_adapter/mint.ex @@ -1,102 +1,120 @@ if Code.ensure_loaded?(Mint.HTTP) do -defmodule Assent.HTTPAdapter.Mint do - @moduledoc false - alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse} + defmodule Assent.HTTPAdapter.Mint do + @moduledoc false + alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse} - @behaviour HTTPAdapter + @behaviour HTTPAdapter - @impl HTTPAdapter - def request(method, url, body, headers, mint_opts \\ nil) do - IO.warn("#{inspect __MODULE__} is deprecated, consider use #{inspect Assent.HTTPAdapter.Finch} instead") + @impl HTTPAdapter + def request(method, url, body, headers, mint_opts \\ nil) do + IO.warn( + "#{inspect(__MODULE__)} is deprecated, consider use #{inspect(Assent.HTTPAdapter.Finch)} instead" + ) - headers = headers ++ [HTTPAdapter.user_agent_header()] + headers = headers ++ [HTTPAdapter.user_agent_header()] - %{scheme: scheme, port: port, host: host, path: path, query: query} = URI.parse(url) + %{scheme: scheme, port: port, host: host, path: path, query: query} = URI.parse(url) - path = add_query_to_path(path, query) + path = add_query_to_path(path, query) - scheme - |> open_mint_conn(host, port, mint_opts) - |> mint_request(method, path, headers, body) - |> format_response() - end + scheme + |> open_mint_conn(host, port, mint_opts) + |> mint_request(method, path, headers, body) + |> format_response() + end - defp add_query_to_path(path, query) when is_binary(query), do: path <> "?" <> query - defp add_query_to_path(path, _any), do: path + defp add_query_to_path(path, query) when is_binary(query), do: path <> "?" <> query + defp add_query_to_path(path, _any), do: path - defp open_mint_conn(scheme, host, port, nil), do: open_mint_conn(scheme, host, port, []) - defp open_mint_conn("http", host, port, opts), do: open_mint_conn(:http, host, port, opts) - defp open_mint_conn("https", host, port, opts), do: open_mint_conn(:https, host, port, opts) - defp open_mint_conn(scheme, host, port, opts) when is_atom(scheme) do - transport_opts = Keyword.get(opts, :transport_opts, []) - opts = Keyword.put(opts, :transport_opts, transport_opts) + defp open_mint_conn(scheme, host, port, nil), do: open_mint_conn(scheme, host, port, []) + defp open_mint_conn("http", host, port, opts), do: open_mint_conn(:http, host, port, opts) + defp open_mint_conn("https", host, port, opts), do: open_mint_conn(:https, host, port, opts) - Mint.HTTP.connect(scheme, host, port, opts) - end + defp open_mint_conn(scheme, host, port, opts) when is_atom(scheme) do + transport_opts = Keyword.get(opts, :transport_opts, []) + opts = Keyword.put(opts, :transport_opts, transport_opts) - defp mint_request(resp, :get, path, headers, body), do: mint_request(resp, "GET", path, headers, body) - defp mint_request(resp, :post, path, headers, body), do: mint_request(resp, "POST", path, headers, body) - defp mint_request(resp, method, nil, headers, body), do: mint_request(resp, method, "/", headers, body) - defp mint_request({:ok, conn}, method, path, headers, body) do - conn - |> Mint.HTTP.request(method, path, headers, body) - |> await_response() - end - defp mint_request({:error, error}, _method, _path, _headers, _body), do: {:error, error} + Mint.HTTP.connect(scheme, host, port, opts) + end + + defp mint_request(resp, :get, path, headers, body), + do: mint_request(resp, "GET", path, headers, body) - defp await_response({:ok, conn, request_ref}), do: await_response(conn, request_ref) - defp await_response(conn, request_ref, timeout \\ 5_000, responses \\ []) do - start_time = monotonic_timestamp() + defp mint_request(resp, :post, path, headers, body), + do: mint_request(resp, "POST", path, headers, body) - receive do - {:tcp, _, _} = message -> handle_response(conn, request_ref, message, timeout, start_time, responses) - {:ssl, _, _} = message -> handle_response(conn, request_ref, message, timeout, start_time, responses) - after - timeout -> {:error, :timeout} + defp mint_request(resp, method, nil, headers, body), + do: mint_request(resp, method, "/", headers, body) + + defp mint_request({:ok, conn}, method, path, headers, body) do + conn + |> Mint.HTTP.request(method, path, headers, body) + |> await_response() end - end - defp monotonic_timestamp, do: :erlang.monotonic_time(:millisecond) + defp mint_request({:error, error}, _method, _path, _headers, _body), do: {:error, error} + + defp await_response({:ok, conn, request_ref}), do: await_response(conn, request_ref) - defp handle_response(conn, request_ref, message, timeout, start_time, prev_responses) do - case Mint.HTTP.stream(conn, message) do - {:ok, conn, responses} -> - case completed?(responses) do - true -> - {:ok, prev_responses ++ responses} + defp await_response(conn, request_ref, timeout \\ 5_000, responses \\ []) do + start_time = monotonic_timestamp() - false -> - new_timeout = new_timeout(timeout, start_time) + receive do + {:tcp, _, _} = message -> + handle_response(conn, request_ref, message, timeout, start_time, responses) + + {:ssl, _, _} = message -> + handle_response(conn, request_ref, message, timeout, start_time, responses) + after + timeout -> {:error, :timeout} + end + end - await_response(conn, request_ref, new_timeout, prev_responses ++ responses) - end + defp monotonic_timestamp, do: :erlang.monotonic_time(:millisecond) - {:error, _, e, _} -> - {:error, e} + defp handle_response(conn, request_ref, message, timeout, start_time, prev_responses) do + case Mint.HTTP.stream(conn, message) do + {:ok, conn, responses} -> + case completed?(responses) do + true -> + {:ok, prev_responses ++ responses} - :unknown -> - new_timeout = new_timeout(timeout, start_time) + false -> + new_timeout = new_timeout(timeout, start_time) - await_response(conn, request_ref, new_timeout, prev_responses) + await_response(conn, request_ref, new_timeout, prev_responses ++ responses) + end + + {:error, _, e, _} -> + {:error, e} + + :unknown -> + new_timeout = new_timeout(timeout, start_time) + + await_response(conn, request_ref, new_timeout, prev_responses) + end end - end - defp new_timeout(timeout, start_time), do: max(timeout - (monotonic_timestamp() - start_time), 0) + defp new_timeout(timeout, start_time), + do: max(timeout - (monotonic_timestamp() - start_time), 0) - defp completed?([{:done, _request_ref} | _rest]), do: true - defp completed?([_resp | responses]), do: completed?(responses) - defp completed?([]), do: false + defp completed?([{:done, _request_ref} | _rest]), do: true + defp completed?([_resp | responses]), do: completed?(responses) + defp completed?([]), do: false - defp format_response({:ok, responses}) do - [{:status, _, status}, {:headers, _, headers} | responses] = responses - body = merge_body(responses) + defp format_response({:ok, responses}) do + [{:status, _, status}, {:headers, _, headers} | responses] = responses + body = merge_body(responses) - {:ok, %HTTPResponse{status: status, headers: headers, body: body}} - end - defp format_response({:error, response}), do: {:error, response} + {:ok, %HTTPResponse{status: status, headers: headers, body: body}} + end - defp merge_body([{:data, _request, new_body} | rest], body), do: merge_body(rest, body <> new_body) - defp merge_body(_rest, body), do: body - defp merge_body(responses), do: merge_body(responses, "") -end + defp format_response({:error, response}), do: {:error, response} + + defp merge_body([{:data, _request, new_body} | rest], body), + do: merge_body(rest, body <> new_body) + + defp merge_body(_rest, body), do: body + defp merge_body(responses), do: merge_body(responses, "") + end end diff --git a/lib/assent/http_adapter/req.ex b/lib/assent/http_adapter/req.ex index 4feec96..1e70991 100644 --- a/lib/assent/http_adapter/req.ex +++ b/lib/assent/http_adapter/req.ex @@ -19,19 +19,25 @@ if Code.ensure_loaded?(Req) do opts = req_opts || [] opts = - Keyword.merge([ - method: method, - url: url, - headers: headers, - body: body - ], opts) + Keyword.merge( + [ + method: method, + url: url, + headers: headers, + body: body + ], + opts + ) opts |> Req.new() |> Req.request() |> case do {:ok, response} -> - headers = Enum.map(headers, fn {key, value} -> {String.downcase(to_string(key)), to_string(value)} end) + headers = + Enum.map(headers, fn {key, value} -> + {String.downcase(to_string(key)), to_string(value)} + end) {:ok, %HTTPResponse{status: response.status, headers: headers, body: response.body}} @@ -40,4 +46,4 @@ if Code.ensure_loaded?(Req) do end end end - end +end diff --git a/lib/assent/jwt_adapter.ex b/lib/assent/jwt_adapter.ex index 4cd935a..3f65d6c 100644 --- a/lib/assent/jwt_adapter.ex +++ b/lib/assent/jwt_adapter.ex @@ -34,7 +34,8 @@ defmodule Assent.JWTAdapter do alias Assent.Config @callback sign(map(), binary(), binary(), Keyword.t()) :: {:ok, binary()} | {:error, term()} - @callback verify(binary(), binary() | map() | nil, Keyword.t()) :: {:ok, map()} | {:error, term()} + @callback verify(binary(), binary() | map() | nil, Keyword.t()) :: + {:ok, map()} | {:error, term()} @doc """ Generates a signed JSON Web Token signature @@ -59,7 +60,7 @@ defmodule Assent.JWTAdapter do default_jwt_adapter = Application.get_env(:assent, :jwt_adapter, Assent.JWTAdapter.AssentJWT) case Keyword.get(opts, :jwt_adapter, default_jwt_adapter) do - {adapter, opts} -> {adapter, Keyword.merge(default_opts, opts)} + {adapter, opts} -> {adapter, Keyword.merge(default_opts, opts)} adapter when is_atom(adapter) -> {adapter, default_opts} end end @@ -70,14 +71,14 @@ defmodule Assent.JWTAdapter do @spec load_private_key(Config.t()) :: {:ok, binary()} | {:error, term()} def load_private_key(config) do case Config.fetch(config, :private_key_path) do - {:ok, path} -> read(path) + {:ok, path} -> read(path) {:error, _any} -> Config.fetch(config, :private_key) end end defp read(path) do case File.read(path) do - {:error, error} -> {:error, "Failed to read \"#{path}\", got; #{inspect error}"} + {:error, error} -> {:error, "Failed to read \"#{path}\", got; #{inspect(error)}"} {:ok, content} -> {:ok, content} end end diff --git a/lib/assent/jwt_adapter/assent_jwt.ex b/lib/assent/jwt_adapter/assent_jwt.ex index ad8d868..05fb2bd 100644 --- a/lib/assent/jwt_adapter/assent_jwt.ex +++ b/lib/assent/jwt_adapter/assent_jwt.ex @@ -18,8 +18,8 @@ defmodule Assent.JWTAdapter.AssentJWT do @impl JWTAdapter def sign(claims, alg, secret_or_private_key, opts) do - with {:ok, header} <- encode_header(alg, opts), - {:ok, claims} <- encode_claims(claims, opts) do + with {:ok, header} <- encode_header(alg, opts), + {:ok, claims} <- encode_claims(claims, opts) do do_sign(header, claims, alg, secret_or_private_key) end end @@ -32,8 +32,11 @@ defmodule Assent.JWTAdapter.AssentJWT do end case encode_json_base64(header, opts) do - {:ok, encoded_header} -> {:ok, encoded_header} - {:error, error} -> {:error, Error.exception(message: "Failed to encode header", reason: error, data: header)} + {:ok, encoded_header} -> + {:ok, encoded_header} + + {:error, error} -> + {:error, Error.exception(message: "Failed to encode header", reason: error, data: header)} end end @@ -46,8 +49,11 @@ defmodule Assent.JWTAdapter.AssentJWT do defp encode_claims(claims, opts) do case encode_json_base64(claims, opts) do - {:ok, encoded_claims} -> {:ok, encoded_claims} - {:error, error} -> {:error, Error.exception(message: "Failed to encode claims", reason: error, data: claims)} + {:ok, encoded_claims} -> + {:ok, encoded_claims} + + {:error, error} -> + {:error, Error.exception(message: "Failed to encode claims", reason: error, data: claims)} end end @@ -55,8 +61,12 @@ defmodule Assent.JWTAdapter.AssentJWT do message = header <> "." <> claims case sign_message(message, alg, secret_or_private_key) do - {:ok, signature} -> {:ok, "#{message}.#{Base.url_encode64(signature, padding: false)}"} - {:error, error} -> {:error, Error.exception(message: "Failed to sign JWT", reason: error, data: {message, alg})} + {:ok, signature} -> + {:ok, "#{message}.#{Base.url_encode64(signature, padding: false)}"} + + {:error, error} -> + {:error, + Error.exception(message: "Failed to sign JWT", reason: error, data: {message, alg})} end end @@ -70,10 +80,9 @@ defmodule Assent.JWTAdapter.AssentJWT do # Per https://tools.ietf.org/html/rfc7515#appendix-A.3.1 with {:ok, sha_alg} <- sha2_alg(sha_bit_size), - {:ok, key} <- decode_pem(private_key) do - + {:ok, key} <- decode_pem(private_key) do der_signature = :public_key.sign(message, sha_alg, key) - {:'ECDSA-Sig-Value', r, s} = :public_key.der_decode(:'ECDSA-Sig-Value', der_signature) + {:"ECDSA-Sig-Value", r, s} = :public_key.der_decode(:"ECDSA-Sig-Value", der_signature) r_bin = sha_bit_pad(int_to_bin(r), sha_bit_size) s_bin = sha_bit_pad(int_to_bin(s), sha_bit_size) @@ -81,15 +90,15 @@ defmodule Assent.JWTAdapter.AssentJWT do end end - defp sign_message(message, <<_, "S", sha_bit_size :: binary>>, private_key) do + defp sign_message(message, <<_, "S", sha_bit_size::binary>>, private_key) do with {:ok, sha_alg} <- sha2_alg(sha_bit_size), - {:ok, key} <- decode_pem(private_key) do - + {:ok, key} <- decode_pem(private_key) do {:ok, :public_key.sign(message, sha_alg, key)} end end - defp sign_message(_message, alg, _jwk), do: {:error, "Unsupported JWT alg #{alg} or invalid JWK"} + defp sign_message(_message, alg, _jwk), + do: {:error, "Unsupported JWT alg #{alg} or invalid JWK"} defp sha2_alg("256"), do: {:ok, :sha256} defp sha2_alg("384"), do: {:ok, :sha384} @@ -98,9 +107,9 @@ defmodule Assent.JWTAdapter.AssentJWT do defp decode_pem(pem) do case :public_key.pem_decode(pem) do - [] -> {:error, "Invalid private key"} + [] -> {:error, "Invalid private key"} [entry] -> {:ok, :public_key.pem_entry_decode(entry)} - _any -> {:error, "Private key should only have one entry"} + _any -> {:error, "Private key should only have one entry"} end end @@ -121,40 +130,51 @@ defmodule Assent.JWTAdapter.AssentJWT do defp lpad_binary(binary, length) when length > 0 do :binary.copy(<<0>>, length - byte_size(binary)) <> binary end + defp lpad_binary(binary, _length), do: binary @impl JWTAdapter def verify(token, secret_or_public_key, opts) do with {:ok, encoded_jwt} <- split(token), {:ok, alg, header} <- decode_header(encoded_jwt.header, opts), - {:ok, claims} <- decode_claims(encoded_jwt.claims, opts), - {:ok, signature} <- decode_signature(encoded_jwt.signature), - {:ok, verified} <- do_verify(encoded_jwt.header, encoded_jwt.claims, signature, alg, secret_or_public_key) do - - {:ok, %{ - header: header, - claims: claims, - signature: signature, - verified?: verified - }} + {:ok, claims} <- decode_claims(encoded_jwt.claims, opts), + {:ok, signature} <- decode_signature(encoded_jwt.signature), + {:ok, verified} <- + do_verify(encoded_jwt.header, encoded_jwt.claims, signature, alg, secret_or_public_key) do + {:ok, + %{ + header: header, + claims: claims, + signature: signature, + verified?: verified + }} end end defp split(token) do case String.split(token, ".") do - [header, claims, signature] -> {:ok, %{header: header, claims: claims, signature: signature}} - parts -> {:error, Error.exception(message: "JWT must have exactly three parts", reason: :invalid_format, data: parts)} + [header, claims, signature] -> + {:ok, %{header: header, claims: claims, signature: signature}} + + parts -> + {:error, + Error.exception( + message: "JWT must have exactly three parts", + reason: :invalid_format, + data: parts + )} end end defp decode_header(header, opts) do with {:ok, json_library} <- Config.fetch(opts, :json_library), - {:ok, header} <- decode_base64_url(header), - {:ok, header} <- decode_json(header, json_library), - {:ok, alg} <- fetch_alg(header) do + {:ok, header} <- decode_base64_url(header), + {:ok, header} <- decode_json(header, json_library), + {:ok, alg} <- fetch_alg(header) do {:ok, alg, header} else - {:error, error} -> {:error, Error.exception(message: "Failed to decode header", reason: error, data: header)} + {:error, error} -> + {:error, Error.exception(message: "Failed to decode header", reason: error, data: header)} end end @@ -177,18 +197,23 @@ defmodule Assent.JWTAdapter.AssentJWT do defp decode_claims(claims, opts) do with {:ok, json_library} <- Config.fetch(opts, :json_library), - {:ok, claims} <- decode_base64_url(claims), - {:ok, claims} <- decode_json(claims, json_library) do + {:ok, claims} <- decode_base64_url(claims), + {:ok, claims} <- decode_json(claims, json_library) do {:ok, claims} else - {:error, error} -> {:error, Error.exception(message: "Failed to decode claims", reason: error, data: claims)} + {:error, error} -> + {:error, Error.exception(message: "Failed to decode claims", reason: error, data: claims)} end end defp decode_signature(signature) do case decode_base64_url(signature) do - {:ok, signature} -> {:ok, signature} - {:error, error} -> {:error, Error.exception(message: "Failed to decode signature", reason: error, data: signature)} + {:ok, signature} -> + {:ok, signature} + + {:error, error} -> + {:error, + Error.exception(message: "Failed to decode signature", reason: error, data: signature)} end end @@ -196,8 +221,16 @@ defmodule Assent.JWTAdapter.AssentJWT do message = "#{header}.#{claims}" case verify_message(message, signature, alg, secret_or_public_key) do - {:ok, verified} -> {:ok, verified} - {:error, error} -> {:error, Error.exception(message: "Failed to verify signature", reason: error, data: {message, signature, alg})} + {:ok, verified} -> + {:ok, verified} + + {:error, error} -> + {:error, + Error.exception( + message: "Failed to verify signature", + reason: error, + data: {message, signature, alg} + )} end end @@ -213,23 +246,22 @@ defmodule Assent.JWTAdapter.AssentJWT do defp verify_message(message, signature, "ES" <> sha_bit_size, public_key) do with {:ok, sha_alg} <- sha2_alg(sha_bit_size), - {:ok, pem} <- decode_key(public_key) do - + {:ok, pem} <- decode_key(public_key) do # Per https://tools.ietf.org/html/rfc7515#appendix-A.3.1 - size = :erlang.byte_size(signature) + size = :erlang.byte_size(signature) {r_bin, s_bin} = :erlang.split_binary(signature, Integer.floor_div(size, 2)) - r = :crypto.bytes_to_integer(r_bin) - s = :crypto.bytes_to_integer(s_bin) - der_signature = :public_key.der_encode(:'ECDSA-Sig-Value', {:'ECDSA-Sig-Value', r, s}) + r = :crypto.bytes_to_integer(r_bin) + s = :crypto.bytes_to_integer(s_bin) + der_signature = :public_key.der_encode(:"ECDSA-Sig-Value", {:"ECDSA-Sig-Value", r, s}) {:ok, :public_key.verify(message, sha_alg, der_signature, pem)} end end - defp verify_message(message, signature, <<_, "S", sha_bit_size :: binary>>, public_key) do + defp verify_message(message, signature, <<_, "S", sha_bit_size::binary>>, public_key) do with {:ok, sha_alg} <- sha2_alg(sha_bit_size), - {:ok, pem} <- decode_key(public_key) do + {:ok, pem} <- decode_key(public_key) do {:ok, :public_key.verify(message, sha_alg, signature, pem)} end end diff --git a/lib/assent/jwt_adapter/jose.ex b/lib/assent/jwt_adapter/jose.ex index b8f9442..31efc24 100644 --- a/lib/assent/jwt_adapter/jose.ex +++ b/lib/assent/jwt_adapter/jose.ex @@ -49,17 +49,19 @@ defmodule Assent.JWTAdapter.JOSE do {%{}, %{"signature" => signature}} = JOSE.JWS.expand(token) - {:ok, %{ - header: header, - claims: claims, - signature: signature, - verified?: verified - }} + {:ok, + %{ + header: header, + claims: claims, + signature: signature, + verified?: verified + }} end defp verify_message(token, _alg, nil) do {false, JOSE.JWT.peek_payload(token)} end + defp verify_message(token, alg, secret_or_public_key) do {verified, payload, _} = alg diff --git a/lib/assent/strategies/apple.ex b/lib/assent/strategies/apple.ex index 09200ba..be1f3f8 100644 --- a/lib/assent/strategies/apple.ex +++ b/lib/assent/strategies/apple.ex @@ -73,7 +73,7 @@ defmodule Assent.Strategy.Apple do @impl true def callback(config, params) do with {:ok, client_secret} <- gen_client_secret(config), - {:ok, user_info} <- decode_user_params(config, params) do + {:ok, user_info} <- decode_user_params(config, params) do config |> Config.put(:client_secret, client_secret) |> Config.put(:user, user_info) @@ -85,17 +85,17 @@ defmodule Assent.Strategy.Apple do defp gen_client_secret(config) do timestamp = :os.system_time(:second) - config = + + config = config |> default_config() |> Keyword.merge(config) - with {:ok, base_url} <- Config.fetch(config, :base_url), - {:ok, client_id} <- Config.fetch(config, :client_id), - {:ok, team_id} <- Config.fetch(config, :team_id), - :ok <- ensure_private_key_id(config), + with {:ok, base_url} <- Config.fetch(config, :base_url), + {:ok, client_id} <- Config.fetch(config, :client_id), + {:ok, team_id} <- Config.fetch(config, :team_id), + :ok <- ensure_private_key_id(config), {:ok, private_key} <- JWTAdapter.load_private_key(config) do - claims = %{ "aud" => base_url, "iss" => team_id, @@ -127,12 +127,13 @@ defmodule Assent.Strategy.Apple do @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["sub"], - "email" => user["email"], - "email_verified" => true, - "given_name" => Map.get(user, "name", %{})["firstName"], - "family_name" => Map.get(user, "name", %{})["lastName"] - }} + {:ok, + %{ + "sub" => user["sub"], + "email" => user["email"], + "email_verified" => true, + "given_name" => Map.get(user, "name", %{})["firstName"], + "family_name" => Map.get(user, "name", %{})["lastName"] + }} end end diff --git a/lib/assent/strategies/auth0.ex b/lib/assent/strategies/auth0.ex index c8903e9..f95e941 100644 --- a/lib/assent/strategies/auth0.ex +++ b/lib/assent/strategies/auth0.ex @@ -19,19 +19,19 @@ defmodule Assent.Strategy.Auth0 do @impl true def default_config(config) do - append_domain_config(config, [ + append_domain_config(config, authorize_url: "/authorize", token_url: "/oauth/token", user_url: "/userinfo", authorization_params: [scope: "openid profile email"], auth_method: :client_secret_post - ]) + ) end defp append_domain_config(config, default) do case Config.fetch(config, :domain) do {:ok, domain} -> Config.put(default, :base_url, prepend_scheme(domain)) - _error -> default + _error -> default end end diff --git a/lib/assent/strategies/basecamp.ex b/lib/assent/strategies/basecamp.ex index ef25fbf..8c2d175 100644 --- a/lib/assent/strategies/basecamp.ex +++ b/lib/assent/strategies/basecamp.ex @@ -34,15 +34,16 @@ defmodule Assent.Strategy.Basecamp do @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["identity"]["id"], - "name" => "#{user["identity"]["first_name"]} #{user["identity"]["last_name"]}", - "given_name" => user["identity"]["first_name"], - "family_name" => user["identity"]["last_name"], - "email" => user["identity"]["email_address"] - }, - %{ - "basecamp_accounts" => user["accounts"] - }} + {:ok, + %{ + "sub" => user["identity"]["id"], + "name" => "#{user["identity"]["first_name"]} #{user["identity"]["last_name"]}", + "given_name" => user["identity"]["first_name"], + "family_name" => user["identity"]["last_name"], + "email" => user["identity"]["email_address"] + }, + %{ + "basecamp_accounts" => user["accounts"] + }} end end diff --git a/lib/assent/strategies/discord.ex b/lib/assent/strategies/discord.ex index b1620ad..d57b111 100644 --- a/lib/assent/strategies/discord.ex +++ b/lib/assent/strategies/discord.ex @@ -28,13 +28,14 @@ defmodule Assent.Strategy.Discord do @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["id"], - "preferred_username" => user["username"], - "email" => user["email"], - "email_verified" => user["verified"], - "picture" => picture_url(user), - }} + {:ok, + %{ + "sub" => user["id"], + "preferred_username" => user["username"], + "email" => user["email"], + "email_verified" => user["verified"], + "picture" => picture_url(user) + }} end defp picture_url(user) do diff --git a/lib/assent/strategies/facebook.ex b/lib/assent/strategies/facebook.ex index f01cf6d..b9272a8 100644 --- a/lib/assent/strategies/facebook.ex +++ b/lib/assent/strategies/facebook.ex @@ -87,16 +87,17 @@ defmodule Assent.Strategy.Facebook do @impl true def normalize(config, user) do with {:ok, base_url} <- Config.fetch(config, :base_url) do - {:ok, %{ - "sub" => user["id"], - "name" => user["name"], - "given_name" => user["first_name"], - "middle_name" => user["middle_name"], - "family_name" => user["last_name"], - "profile" => user["link"], - "picture" => picture_url(base_url, user), - "email" => user["email"] - }} + {:ok, + %{ + "sub" => user["id"], + "name" => user["name"], + "given_name" => user["first_name"], + "middle_name" => user["middle_name"], + "family_name" => user["last_name"], + "profile" => user["link"], + "picture" => picture_url(base_url, user), + "email" => user["email"] + }} end end diff --git a/lib/assent/strategies/github.ex b/lib/assent/strategies/github.ex index 9eb7ce2..d11a9ea 100644 --- a/lib/assent/strategies/github.ex +++ b/lib/assent/strategies/github.ex @@ -37,15 +37,16 @@ defmodule Assent.Strategy.Github do @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["id"], - "name" => user["name"], - "preferred_username" => user["login"], - "profile" => user["html_url"], - "picture" => user["avatar_url"], - "email" => user["email"], - "email_verified" => user["email_verified"] - }} + {:ok, + %{ + "sub" => user["id"], + "name" => user["name"], + "preferred_username" => user["login"], + "profile" => user["html_url"], + "picture" => user["avatar_url"], + "email" => user["email"], + "email_verified" => user["email_verified"] + }} end @impl true @@ -67,10 +68,12 @@ defmodule Assent.Strategy.Github do {:ok, Map.merge(user, %{"email" => email, "email_verified" => verified})} end + defp process_email_response({:error, error}, _user), do: {:error, error} defp get_primary_email([%{"verified" => verified, "primary" => true, "email" => email} | _rest]), do: {email, verified} + defp get_primary_email([_ | rest]), do: get_primary_email(rest) defp get_primary_email(_any), do: {nil, false} end diff --git a/lib/assent/strategies/gitlab.ex b/lib/assent/strategies/gitlab.ex index f53c0e3..41fffe5 100644 --- a/lib/assent/strategies/gitlab.ex +++ b/lib/assent/strategies/gitlab.ex @@ -28,13 +28,14 @@ defmodule Assent.Strategy.Gitlab do @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["id"], - "name" => user["name"], - "preferred_username" => user["username"], - "picture" => user["avatar_url"], - "email" => user["email"], - "email_verified" => not is_nil(user["confirmed_at"]) - }} + {:ok, + %{ + "sub" => user["id"], + "name" => user["name"], + "preferred_username" => user["username"], + "picture" => user["avatar_url"], + "email" => user["email"], + "email_verified" => not is_nil(user["confirmed_at"]) + }} end end diff --git a/lib/assent/strategies/google.ex b/lib/assent/strategies/google.ex index 318079d..2582f73 100644 --- a/lib/assent/strategies/google.ex +++ b/lib/assent/strategies/google.ex @@ -37,25 +37,29 @@ defmodule Assent.Strategy.Google do authorize_url: "https://accounts.google.com/o/oauth2/v2/auth", token_url: "/oauth2/v4/token", user_url: "/oauth2/v3/userinfo", - authorization_params: [scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"], + authorization_params: [ + scope: + "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" + ], auth_method: :client_secret_post ] end @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["sub"], - "name" => user["name"], - "given_name" => user["given_name"], - "family_name" => user["family_name"], - "picture" => user["picture"], - "email" => user["email"], - "email_verified" => user["email_verified"], - "locale" => user["locale"] - }, - %{ - "google_hd" => user["hd"] - }} + {:ok, + %{ + "sub" => user["sub"], + "name" => user["name"], + "given_name" => user["given_name"], + "family_name" => user["family_name"], + "picture" => user["picture"], + "email" => user["email"], + "email_verified" => user["email_verified"], + "locale" => user["locale"] + }, + %{ + "google_hd" => user["hd"] + }} end end diff --git a/lib/assent/strategies/instagram.ex b/lib/assent/strategies/instagram.ex index cea0ee5..d7674b6 100644 --- a/lib/assent/strategies/instagram.ex +++ b/lib/assent/strategies/instagram.ex @@ -46,9 +46,10 @@ defmodule Assent.Strategy.Instagram do @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["id"], - "preferred_username" => user["username"] - }} + {:ok, + %{ + "sub" => user["id"], + "preferred_username" => user["username"] + }} end end diff --git a/lib/assent/strategies/oauth.ex b/lib/assent/strategies/oauth.ex index 27fa6dc..34a40f1 100644 --- a/lib/assent/strategies/oauth.ex +++ b/lib/assent/strategies/oauth.ex @@ -46,7 +46,16 @@ defmodule Assent.Strategy.OAuth do @behaviour Assent.Strategy alias Assent.Strategy, as: Helpers - alias Assent.{Config, HTTPAdapter.HTTPResponse, JWTAdapter, MissingParamError, InvalidResponseError, RequestError, UnexpectedResponseError} + + alias Assent.{ + Config, + HTTPAdapter.HTTPResponse, + InvalidResponseError, + JWTAdapter, + MissingParamError, + RequestError, + UnexpectedResponseError + } @doc """ Generate authorization URL for request phase. @@ -62,11 +71,13 @@ defmodule Assent.Strategy.OAuth do - `:authorization_params` - The authorization parameters, defaults to `[]` """ @impl true - @spec authorize_url(Config.t()) :: {:ok, %{url: binary(), session_params: %{oauth_token_secret: binary()}}} | {:error, term()} + @spec authorize_url(Config.t()) :: + {:ok, %{url: binary(), session_params: %{oauth_token_secret: binary()}}} + | {:error, term()} def authorize_url(config) do case Config.fetch(config, :redirect_uri) do {:ok, redirect_uri} -> authorize_url(config, redirect_uri) - {:error, error} -> {:error, error} + {:error, error} -> {:error, error} end end @@ -75,15 +86,18 @@ defmodule Assent.Strategy.OAuth do |> get_request_token([{"oauth_callback", redirect_uri}]) |> build_authorize_url(config) |> case do - {:ok, url, oauth_token_secret} -> {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} - {:error, error} -> {:error, error} + {:ok, url, oauth_token_secret} -> + {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} + + {:error, error} -> + {:error, error} end end defp get_request_token(config, oauth_params) do with {:ok, base_url} <- Config.__base_url__(config) do request_token_url = Config.get(config, :request_token_url, "/request_token") - url = process_url(base_url, request_token_url) + url = process_url(base_url, request_token_url) config |> do_request(:post, base_url, url, [], oauth_params) @@ -93,13 +107,22 @@ defmodule Assent.Strategy.OAuth do defp process_url(base_url, url) do case String.downcase(url) do - <<"http://"::utf8, _::binary>> -> url + <<"http://"::utf8, _::binary>> -> url <<"https://"::utf8, _::binary>> -> url - _ -> base_url <> url + _ -> base_url <> url end end - defp do_request(config, method, base_url, url, params, oauth_params, headers \\ [], token_secret \\ nil) do + defp do_request( + config, + method, + base_url, + url, + params, + oauth_params, + headers \\ [], + token_secret \\ nil + ) do params = params |> Enum.to_list() @@ -108,11 +131,20 @@ defmodule Assent.Strategy.OAuth do signature_method = Config.get(config, :signature_method, :hmac_sha1) with {:ok, oauth_params} <- gen_oauth_params(config, signature_method, oauth_params), - {:ok, signed_header} <- signed_header(config, signature_method, method, url, oauth_params, params, token_secret) do - req_headers = request_headers(method, [signed_header] ++ headers) - req_body = request_body(method, params) - query_params = url_params(method, params) - url = Helpers.to_url(base_url, url, query_params) + {:ok, signed_header} <- + signed_header( + config, + signature_method, + method, + url, + oauth_params, + params, + token_secret + ) do + req_headers = request_headers(method, [signed_header] ++ headers) + req_body = request_body(method, params) + query_params = url_params(method, params) + url = Helpers.to_url(base_url, url, query_params) Helpers.request(method, url, req_body, req_headers, config) end @@ -135,11 +167,12 @@ defmodule Assent.Strategy.OAuth do end defp signed_header(config, signature_method, method, url, oauth_params, params, token_secret) do - uri = URI.parse(url) - query_params = Map.to_list(URI.decode_query(uri.query || "")) + uri = URI.parse(url) + query_params = Map.to_list(URI.decode_query(uri.query || "")) request_params = params ++ query_params ++ oauth_params - with {:ok, signature} <- gen_signature(config, method, uri, request_params, signature_method, token_secret) do + with {:ok, signature} <- + gen_signature(config, method, uri, request_params, signature_method, token_secret) do oauth_header_value = Enum.map_join([{"oauth_signature", signature} | oauth_params], ", ", fn {key, value} -> percent_encode(key) <> "=\"" <> percent_encode(value) <> "\"" @@ -173,6 +206,7 @@ defmodule Assent.Strategy.OAuth do {:ok, signature} end end + defp gen_signature(config, method, uri, request_params, :rsa_sha1, _token_secret) do with {:ok, pem} <- JWTAdapter.load_private_key(config), {:ok, private_key} <- decode_pem(pem) do @@ -185,6 +219,7 @@ defmodule Assent.Strategy.OAuth do {:ok, signature} end end + defp gen_signature(config, _method, _url, _request_params, :plaintext, token_secret), do: encoded_shared_secret(config, token_secret) @@ -227,11 +262,13 @@ defmodule Assent.Strategy.OAuth do defp decode_pem(pem) do case :public_key.pem_decode(pem) do [entry] -> {:ok, :public_key.pem_entry_decode(entry)} - _any -> {:error, "Private key should only have one entry"} + _any -> {:error, "Private key should only have one entry"} end end - defp request_headers(:post, headers), do: [{"content-type", "application/x-www-form-urlencoded"}] ++ headers + defp request_headers(:post, headers), + do: [{"content-type", "application/x-www-form-urlencoded"}] ++ headers + defp request_headers(_method, headers), do: headers defp request_body(:post, req_params), do: URI.encode_query(req_params) @@ -240,12 +277,29 @@ defmodule Assent.Strategy.OAuth do defp url_params(:post, _params), do: [] defp url_params(_method, params), do: params - defp process_token_response({:ok, %HTTPResponse{status: 200, body: body} = response}) when is_binary(body), do: process_token_response({:ok, %{response | body: URI.decode_query(body)}}) - defp process_token_response({:ok, %HTTPResponse{status: 200, body: %{"oauth_token" => _, "oauth_token_secret" => _} = token}}), do: {:ok, token} + defp process_token_response({:ok, %HTTPResponse{status: 200, body: body} = response}) + when is_binary(body) do + process_token_response({:ok, %{response | body: URI.decode_query(body)}}) + end + + defp process_token_response( + {:ok, + %HTTPResponse{ + status: 200, + body: %{"oauth_token" => _, "oauth_token_secret" => _} = token + }} + ) do + {:ok, token} + end + defp process_token_response(any), do: process_response(any) - defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, UnexpectedResponseError.exception(response: response)} - defp process_response({:error, %HTTPResponse{} = response}), do: {:error, InvalidResponseError.exception(response: response)} + defp process_response({:ok, %HTTPResponse{} = response}), + do: {:error, UnexpectedResponseError.exception(response: response)} + + defp process_response({:error, %HTTPResponse{} = response}), + do: {:error, InvalidResponseError.exception(response: response)} + defp process_response({:error, error}), do: {:error, error} defp build_authorize_url({:ok, token}, config) do @@ -253,18 +307,19 @@ defmodule Assent.Strategy.OAuth do {:ok, oauth_token} <- fetch_from_token(token, "oauth_token"), {:ok, oauth_token_secret} <- fetch_from_token(token, "oauth_token_secret") do authorization_url = Config.get(config, :authorize_url, "/authorize") - params = authorization_params(config, oauth_token: oauth_token) - url = Helpers.to_url(base_url, authorization_url, params) + params = authorization_params(config, oauth_token: oauth_token) + url = Helpers.to_url(base_url, authorization_url, params) {:ok, url, oauth_token_secret} end end + defp build_authorize_url({:error, error}, _config), do: {:error, error} defp fetch_from_token(token, key) do case Map.fetch(token, key) do {:ok, value} -> {:ok, value} - :error -> {:error, "No `#{key}` in token map"} + :error -> {:error, "No `#{key}` in token map"} end end @@ -287,30 +342,43 @@ defmodule Assent.Strategy.OAuth do `authorize_url/1`, optional """ @impl true - @spec callback(Config.t(), map(), atom()) :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Config.t(), map(), atom()) :: + {:ok, %{user: map(), token: map()}} | {:error, term()} def callback(config, params, strategy \\ __MODULE__) do - with {:ok, oauth_token} <- fetch_oauth_token(params), + with {:ok, oauth_token} <- fetch_oauth_token(params), {:ok, oauth_verifier} <- fetch_oauth_verifier(params), - {:ok, token} <- get_access_token(config, oauth_token, oauth_verifier), - {:ok, user} <- strategy.fetch_user(config, token) do + {:ok, token} <- get_access_token(config, oauth_token, oauth_verifier), + {:ok, user} <- strategy.fetch_user(config, token) do {:ok, %{user: user, token: token}} end end defp fetch_oauth_token(%{"oauth_token" => code}), do: {:ok, code} - defp fetch_oauth_token(params), do: {:error, MissingParamError.exception(expected_key: "oauth_token", params: params)} + + defp fetch_oauth_token(params), + do: {:error, MissingParamError.exception(expected_key: "oauth_token", params: params)} defp fetch_oauth_verifier(%{"oauth_verifier" => code}), do: {:ok, code} - defp fetch_oauth_verifier(params), do: {:error, MissingParamError.exception(expected_key: "oauth_verifier", params: params)} + + defp fetch_oauth_verifier(params), + do: {:error, MissingParamError.exception(expected_key: "oauth_verifier", params: params)} defp get_access_token(config, oauth_token, oauth_verifier) do with {:ok, base_url} <- Config.__base_url__(config) do - access_token_url = Config.get(config, :access_token_url, "/access_token") - url = process_url(base_url, access_token_url) + access_token_url = Config.get(config, :access_token_url, "/access_token") + url = process_url(base_url, access_token_url) oauth_token_secret = Kernel.get_in(config, [:session_params, :oauth_token_secret]) config - |> do_request(:post, base_url, url, [], [{"oauth_token", oauth_token}, {"oauth_verifier", oauth_verifier}], [], oauth_token_secret) + |> do_request( + :post, + base_url, + url, + [], + [{"oauth_token", oauth_token}, {"oauth_verifier", oauth_verifier}], + [], + oauth_token_secret + ) |> process_token_response() end end @@ -318,14 +386,24 @@ defmodule Assent.Strategy.OAuth do @doc """ Performs a signed HTTP request to the API using the oauth token. """ - @spec request(Config.t(), map(), atom(), binary(), map() | Keyword.t(), [{binary(), binary()}]) :: {:ok, map()} | {:error, term()} + @spec request(Config.t(), map(), atom(), binary(), map() | Keyword.t(), [{binary(), binary()}]) :: + {:ok, map()} | {:error, term()} def request(config, token, method, url, params \\ [], headers \\ []) do with {:ok, base_url} <- Config.__base_url__(config), {:ok, oauth_token} <- fetch_from_token(token, "oauth_token"), {:ok, oauth_token_secret} <- fetch_from_token(token, "oauth_token_secret") do url = process_url(base_url, url) - do_request(config, method, base_url, url, params, [{"oauth_token", oauth_token}], headers, oauth_token_secret) + do_request( + config, + method, + base_url, + url, + params, + [{"oauth_token", oauth_token}], + headers, + oauth_token_secret + ) end end @@ -340,6 +418,9 @@ defmodule Assent.Strategy.OAuth do end defp process_user_response({:ok, %HTTPResponse{status: 200, body: user}}), do: {:ok, user} - defp process_user_response({:error, %HTTPResponse{status: 401} = response}), do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} + + defp process_user_response({:error, %HTTPResponse{status: 401} = response}), + do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} + defp process_user_response(any), do: process_response(any) end diff --git a/lib/assent/strategies/oauth/base.ex b/lib/assent/strategies/oauth/base.ex index 00b935a..e4ae8e6 100644 --- a/lib/assent/strategies/oauth/base.ex +++ b/lib/assent/strategies/oauth/base.ex @@ -72,7 +72,8 @@ defmodule Assent.Strategy.OAuth.Base do |> OAuth.authorize_url() end - @spec callback(Keyword.t(), map(), module()) :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Keyword.t(), map(), module()) :: + {:ok, %{user: map(), token: map()}} | {:error, term()} def callback(config, params, strategy) do config = set_config(config, strategy) diff --git a/lib/assent/strategies/oauth2.ex b/lib/assent/strategies/oauth2.ex index 99f8647..127e061 100644 --- a/lib/assent/strategies/oauth2.ex +++ b/lib/assent/strategies/oauth2.ex @@ -66,7 +66,18 @@ defmodule Assent.Strategy.OAuth2 do @behaviour Assent.Strategy alias Assent.Strategy, as: Helpers - alias Assent.{CallbackCSRFError, CallbackError, Config, HTTPAdapter.HTTPResponse, JWTAdapter, MissingParamError, InvalidResponseError, RequestError, UnexpectedResponseError} + + alias Assent.{ + CallbackCSRFError, + CallbackError, + Config, + HTTPAdapter.HTTPResponse, + InvalidResponseError, + JWTAdapter, + MissingParamError, + RequestError, + UnexpectedResponseError + } @doc """ Generate authorization URL for request phase. @@ -80,14 +91,15 @@ defmodule Assent.Strategy.OAuth2 do - `:authorization_params` - The authorization parameters, defaults to `[]` """ @impl true - @spec authorize_url(Config.t()) :: {:ok, %{session_params: %{state: binary()}, url: binary()}} | {:error, term()} + @spec authorize_url(Config.t()) :: + {:ok, %{session_params: %{state: binary()}, url: binary()}} | {:error, term()} def authorize_url(config) do with {:ok, redirect_uri} <- Config.fetch(config, :redirect_uri), - {:ok, base_url} <- Config.__base_url__(config), - {:ok, client_id} <- Config.fetch(config, :client_id) do - params = authorization_params(config, client_id, redirect_uri) + {:ok, base_url} <- Config.__base_url__(config), + {:ok, client_id} <- Config.fetch(config, :client_id) do + params = authorization_params(config, client_id, redirect_uri) authorize_url = Config.get(config, :authorize_url, "/oauth/authorize") - url = Helpers.to_url(base_url, authorize_url, params) + url = Helpers.to_url(base_url, authorize_url, params) {:ok, %{url: url, session_params: %{state: params[:state]}}} end @@ -100,7 +112,8 @@ defmodule Assent.Strategy.OAuth2 do response_type: "code", client_id: client_id, state: gen_state(), - redirect_uri: redirect_uri] + redirect_uri: redirect_uri + ] |> Keyword.merge(params) |> List.keysort(0) end @@ -127,30 +140,39 @@ defmodule Assent.Strategy.OAuth2 do `authorize_url/1`, optional """ @impl true - @spec callback(Config.t(), map(), atom()) :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Config.t(), map(), atom()) :: + {:ok, %{user: map(), token: map()}} | {:error, term()} def callback(config, params, strategy \\ __MODULE__) do with {:ok, session_params} <- Config.fetch(config, :session_params), - :ok <- check_error_params(params), - {:ok, code} <- fetch_code_param(params), - {:ok, redirect_uri} <- Config.fetch(config, :redirect_uri), - :ok <- maybe_check_state(session_params, params), - {:ok, token} <- grant_access_token(config, "authorization_code", code: code, redirect_uri: redirect_uri) do - + :ok <- check_error_params(params), + {:ok, code} <- fetch_code_param(params), + {:ok, redirect_uri} <- Config.fetch(config, :redirect_uri), + :ok <- maybe_check_state(session_params, params), + {:ok, token} <- + grant_access_token( + config, + "authorization_code", + code: code, + redirect_uri: redirect_uri + ) do fetch_user_with_strategy(config, token, strategy) end end defp check_error_params(%{"error" => _} = params) do - message = params["error_description"] || params["error_reason"] || params["error"] - error = params["error"] + message = params["error_description"] || params["error_reason"] || params["error"] + error = params["error"] error_uri = params["error_uri"] {:error, CallbackError.exception(message: message, error: error, error_uri: error_uri)} end + defp check_error_params(_params), do: :ok defp fetch_code_param(%{"code" => code}), do: {:ok, code} - defp fetch_code_param(params), do: {:error, MissingParamError.exception(expected_key: "code", params: params)} + + defp fetch_code_param(params), + do: {:error, MissingParamError.exception(expected_key: "code", params: params)} defp maybe_check_state(%{state: stored_state}, %{"state" => provided_state}) do case Assent.constant_time_compare(stored_state, provided_state) do @@ -158,41 +180,43 @@ defmodule Assent.Strategy.OAuth2 do false -> {:error, CallbackCSRFError.exception(key: "state")} end end + defp maybe_check_state(%{state: _state}, params) do {:error, MissingParamError.exception(expected_key: "state", params: params)} end + defp maybe_check_state(_session_params, _params), do: :ok defp authentication_params(nil, config) do with {:ok, client_id} <- Config.fetch(config, :client_id) do - headers = [] - body = [client_id: client_id] + body = [client_id: client_id] {:ok, headers, body} end end + defp authentication_params(:client_secret_basic, config) do - with {:ok, client_id} <- Config.fetch(config, :client_id), + with {:ok, client_id} <- Config.fetch(config, :client_id), {:ok, client_secret} <- Config.fetch(config, :client_secret) do - - auth = Base.encode64("#{client_id}:#{client_secret}") + auth = Base.encode64("#{client_id}:#{client_secret}") headers = [{"authorization", "Basic #{auth}"}] - body = [] + body = [] {:ok, headers, body} end end + defp authentication_params(:client_secret_post, config) do - with {:ok, client_id} <- Config.fetch(config, :client_id), + with {:ok, client_id} <- Config.fetch(config, :client_id), {:ok, client_secret} <- Config.fetch(config, :client_secret) do - headers = [] - body = [client_id: client_id, client_secret: client_secret] + body = [client_id: client_id, client_secret: client_secret] {:ok, headers, body} end end + defp authentication_params(:client_secret_jwt, config) do alg = Config.get(config, :jwt_algorithm, "HS256") @@ -200,24 +224,29 @@ defmodule Assent.Strategy.OAuth2 do jwt_authentication_params(alg, client_secret, config) end end + defp authentication_params(:private_key_jwt, config) do alg = Config.get(config, :jwt_algorithm, "RS256") - with {:ok, pem} <- JWTAdapter.load_private_key(config), + with {:ok, pem} <- JWTAdapter.load_private_key(config), {:ok, _private_key_id} <- Config.fetch(config, :private_key_id) do jwt_authentication_params(alg, pem, config) end end + defp authentication_params(method, _config) do {:error, "Invalid `:auth_method` #{method}"} end defp jwt_authentication_params(alg, secret, config) do - with {:ok, claims} <- jwt_claims(config), - {:ok, token} <- Helpers.sign_jwt(claims, alg, secret, config) do - + with {:ok, claims} <- jwt_claims(config), + {:ok, token} <- Helpers.sign_jwt(claims, alg, secret, config) do headers = [] - body = [client_assertion: token, client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"] + + body = [ + client_assertion: token, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + ] {:ok, headers, body} end @@ -226,16 +255,16 @@ defmodule Assent.Strategy.OAuth2 do defp jwt_claims(config) do timestamp = :os.system_time(:second) - with {:ok, base_url} <- Config.__base_url__(config), + with {:ok, base_url} <- Config.__base_url__(config), {:ok, client_id} <- Config.fetch(config, :client_id) do - - {:ok, %{ - "iss" => client_id, - "sub" => client_id, - "aud" => base_url, - "iat" => timestamp, - "exp" => timestamp + 60 - }} + {:ok, + %{ + "iss" => client_id, + "sub" => client_id, + "aud" => base_url, + "iat" => timestamp, + "exp" => timestamp + 60 + }} end end @@ -243,16 +272,16 @@ defmodule Assent.Strategy.OAuth2 do Grants an access token. """ @spec grant_access_token(Config.t(), binary(), Keyword.t()) :: {:ok, map()} | {:error, term()} - def grant_access_token(config, grant_type, params) do - auth_method = Config.get(config, :auth_method, nil) - token_url = Config.get(config, :token_url, "/oauth/token") + def grant_access_token(config, grant_type, params) do + auth_method = Config.get(config, :auth_method, nil) + token_url = Config.get(config, :token_url, "/oauth/token") - with {:ok, base_url} <- Config.__base_url__(config), + with {:ok, base_url} <- Config.__base_url__(config), {:ok, auth_headers, auth_body} <- authentication_params(auth_method, config) do headers = [{"content-type", "application/x-www-form-urlencoded"}] ++ auth_headers - params = Keyword.merge(params, Keyword.put(auth_body, :grant_type, grant_type)) - url = Helpers.to_url(base_url, token_url) - body = URI.encode_query(params) + params = Keyword.merge(params, Keyword.put(auth_body, :grant_type, grant_type)) + url = Helpers.to_url(base_url, token_url) + body = URI.encode_query(params) :post |> Helpers.request(url, body, headers, config) @@ -260,18 +289,28 @@ defmodule Assent.Strategy.OAuth2 do end end - defp process_access_token_response({:ok, %HTTPResponse{status: status, body: %{"access_token" => _} = token}}) when status in [200, 201], do: {:ok, token} + defp process_access_token_response( + {:ok, %HTTPResponse{status: status, body: %{"access_token" => _} = token}} + ) + when status in [200, 201] do + {:ok, token} + end + defp process_access_token_response(any), do: process_response(any) - defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, UnexpectedResponseError.exception(response: response)} - defp process_response({:error, %HTTPResponse{} = response}), do: {:error, InvalidResponseError.exception(response: response)} + defp process_response({:ok, %HTTPResponse{} = response}), + do: {:error, UnexpectedResponseError.exception(response: response)} + + defp process_response({:error, %HTTPResponse{} = response}), + do: {:error, InvalidResponseError.exception(response: response)} + defp process_response({:error, error}), do: {:error, error} defp fetch_user_with_strategy(config, token, strategy) do config |> strategy.fetch_user(token) |> case do - {:ok, user} -> {:ok, %{token: token, user: user}} + {:ok, user} -> {:ok, %{token: token, user: user}} {:error, error} -> {:error, error} end end @@ -282,28 +321,34 @@ defmodule Assent.Strategy.OAuth2 do @spec refresh_access_token(Config.t(), map(), Keyword.t()) :: {:ok, map()} | {:error, term()} def refresh_access_token(config, token, params \\ []) do with {:ok, refresh_token} <- fetch_from_token(token, "refresh_token") do - grant_access_token(config, "refresh_token", Keyword.put(params, :refresh_token, refresh_token)) + grant_access_token( + config, + "refresh_token", + Keyword.put(params, :refresh_token, refresh_token) + ) end end @doc """ Performs a HTTP request to the API using the access token. """ - @spec request(Config.t(), map(), atom(), binary(), map() | Keyword.t(), [{binary(), binary()}]) :: {:ok, map()} | {:error, term()} + @spec request(Config.t(), map(), atom(), binary(), map() | Keyword.t(), [{binary(), binary()}]) :: + {:ok, map()} | {:error, term()} def request(config, token, method, url, params \\ [], headers \\ []) do - with {:ok, base_url} <- Config.__base_url__(config), + with {:ok, base_url} <- Config.__base_url__(config), {:ok, auth_headers} <- authorization_headers(config, token) do - req_headers = request_headers(method, auth_headers ++ headers) - req_body = request_body(method, params) - params = url_params(method, params) - url = Helpers.to_url(base_url, url, params) + req_body = request_body(method, params) + params = url_params(method, params) + url = Helpers.to_url(base_url, url, params) Helpers.request(method, url, req_body, req_headers, config) end end - defp request_headers(:post, headers), do: [{"content-type", "application/x-www-form-urlencoded"}] ++ headers + defp request_headers(:post, headers), + do: [{"content-type", "application/x-www-form-urlencoded"}] ++ headers + defp request_headers(_method, headers), do: headers defp request_body(:post, params), do: URI.encode_query(params) @@ -317,7 +362,8 @@ defmodule Assent.Strategy.OAuth2 do Uses `request/6` to fetch the user data. """ - @spec fetch_user(Config.t(), map(), map() | Keyword.t(), [{binary(), binary()}]) :: {:ok, map()} | {:error, term()} + @spec fetch_user(Config.t(), map(), map() | Keyword.t(), [{binary(), binary()}]) :: + {:ok, map()} | {:error, term()} def fetch_user(config, token, params \\ [], headers \\ []) do with {:ok, user_url} <- Config.fetch(config, :user_url) do config @@ -334,11 +380,13 @@ defmodule Assent.Strategy.OAuth2 do authorization_headers(config, token, type) end + defp authorization_headers(_config, token, "bearer") do with {:ok, access_token} <- fetch_from_token(token, "access_token") do {:ok, [{"authorization", "Bearer #{access_token}"}]} end end + defp authorization_headers(_config, _token, type) do {:error, "Authorization with token type `#{type}` not supported"} end @@ -346,11 +394,14 @@ defmodule Assent.Strategy.OAuth2 do defp fetch_from_token(token, key) do case Map.fetch(token, key) do {:ok, value} -> {:ok, value} - :error -> {:error, "No `#{key}` in token map"} + :error -> {:error, "No `#{key}` in token map"} end end defp process_user_response({:ok, %HTTPResponse{status: 200, body: user}}), do: {:ok, user} - defp process_user_response({:error, %HTTPResponse{status: 401} = response}), do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} + + defp process_user_response({:error, %HTTPResponse{status: 401} = response}), + do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} + defp process_user_response(any), do: process_response(any) end diff --git a/lib/assent/strategies/oauth2/base.ex b/lib/assent/strategies/oauth2/base.ex index 6e7c612..baeb013 100644 --- a/lib/assent/strategies/oauth2/base.ex +++ b/lib/assent/strategies/oauth2/base.ex @@ -59,14 +59,16 @@ defmodule Assent.Strategy.OAuth2.Base do end end - @spec authorize_url(Keyword.t(), module()) :: {:ok, %{session_params: %{state: binary()}, url: binary()}} + @spec authorize_url(Keyword.t(), module()) :: + {:ok, %{session_params: %{state: binary()}, url: binary()}} def authorize_url(config, strategy) do config |> set_config(strategy) |> OAuth2.authorize_url() end - @spec callback(Keyword.t(), map(), module()) :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Keyword.t(), map(), module()) :: + {:ok, %{user: map(), token: map()}} | {:error, term()} def callback(config, params, strategy) do config = set_config(config, strategy) diff --git a/lib/assent/strategies/oidc.ex b/lib/assent/strategies/oidc.ex index 79caa4b..d2faab0 100644 --- a/lib/assent/strategies/oidc.ex +++ b/lib/assent/strategies/oidc.ex @@ -68,7 +68,15 @@ defmodule Assent.Strategy.OIDC do @behaviour Assent.Strategy alias Assent.Strategy, as: Helpers - alias Assent.{Config, HTTPAdapter.HTTPResponse, RequestError, UnexpectedResponseError, InvalidResponseError, Strategy.OAuth2} + + alias Assent.{ + Config, + HTTPAdapter.HTTPResponse, + InvalidResponseError, + RequestError, + Strategy.OAuth2, + UnexpectedResponseError + } @doc """ Generates an authorization URL for request phase. @@ -86,11 +94,18 @@ defmodule Assent.Strategy.OIDC do See `Assent.Strategy.OAuth2.authorize_url/1` for more. """ @impl true - @spec authorize_url(Config.t()) :: {:ok, %{session_params: %{state: binary()} | %{state: binary(), nonce: binary()}, url: binary()}} | {:error, term()} + @spec authorize_url(Config.t()) :: + {:ok, + %{ + session_params: %{state: binary()} | %{state: binary(), nonce: binary()}, + url: binary() + }} + | {:error, term()} def authorize_url(config) do with {:ok, openid_config} <- openid_configuration(config), - {:ok, authorize_url} <- fetch_from_openid_config(openid_config, "authorization_endpoint"), - {:ok, params} <- authorization_params(config) do + {:ok, authorize_url} <- + fetch_from_openid_config(openid_config, "authorization_endpoint"), + {:ok, params} <- authorization_params(config) do config |> Config.put(:authorization_params, params) |> Config.put(:authorize_url, authorize_url) @@ -101,15 +116,17 @@ defmodule Assent.Strategy.OIDC do defp openid_configuration(config) do case Config.get(config, :openid_configuration, nil) do - nil -> fetch_openid_configuration(config) + nil -> fetch_openid_configuration(config) openid_config -> {:ok, openid_config} end end defp fetch_openid_configuration(config) do with {:ok, base_url} <- Config.__base_url__(config) do - configuration_url = Config.get(config, :openid_configuration_uri, "/.well-known/openid-configuration") - url = Helpers.to_url(base_url, configuration_url) + configuration_url = + Config.get(config, :openid_configuration_uri, "/.well-known/openid-configuration") + + url = Helpers.to_url(base_url, configuration_url) :get |> Helpers.request(url, nil, [], config) @@ -117,17 +134,26 @@ defmodule Assent.Strategy.OIDC do end end - defp process_openid_configuration_response({:ok, %HTTPResponse{status: 200, body: configuration}}), do: {:ok, configuration} + defp process_openid_configuration_response( + {:ok, %HTTPResponse{status: 200, body: configuration}} + ) do + {:ok, configuration} + end + defp process_openid_configuration_response(any), do: process_response(any) - defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, UnexpectedResponseError.exception(response: response)} - defp process_response({:error, %HTTPResponse{} = response}), do: {:error, InvalidResponseError.exception(response: response)} + defp process_response({:ok, %HTTPResponse{} = response}), + do: {:error, UnexpectedResponseError.exception(response: response)} + + defp process_response({:error, %HTTPResponse{} = response}), + do: {:error, InvalidResponseError.exception(response: response)} + defp process_response({:error, error}), do: {:error, error} defp fetch_from_openid_config(config, key) do case Map.fetch(config, key) do {:ok, value} -> {:ok, value} - :error -> {:error, "`#{key}` not found in OpenID configuration"} + :error -> {:error, "`#{key}` not found in OpenID configuration"} end end @@ -142,8 +168,8 @@ defmodule Assent.Strategy.OIDC do end defp add_default_scope_param(params, config) do - scope = Config.get(params, :scope, "") - default = Config.get(config, :openid_default_scope, "openid") + scope = Config.get(params, :scope, "") + default = Config.get(config, :openid_default_scope, "openid") new_scope = String.trim(default <> " " <> scope) Config.put(params, :scope, new_scope) @@ -151,7 +177,7 @@ defmodule Assent.Strategy.OIDC do defp add_nonce_param(params, config) do case Config.get(config, :nonce, nil) do - nil -> params + nil -> params nonce -> Config.put(params, :nonce, nonce) end end @@ -170,6 +196,7 @@ defmodule Assent.Strategy.OIDC do {:ok, Map.put(resp, :session_params, session_params)} end end + defp add_nonce_to_session_params({:error, error}, _config), do: {:error, error} @@ -189,12 +216,12 @@ defmodule Assent.Strategy.OIDC do See `Assent.Strategy.OAuth2.callback/3` for more. """ @impl true - @spec callback(Config.t(), map(), atom()) :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Config.t(), map(), atom()) :: + {:ok, %{user: map(), token: map()}} | {:error, term()} def callback(config, params, strategy \\ __MODULE__) do with {:ok, openid_config} <- openid_configuration(config), - {:ok, method} <- fetch_client_authentication_method(openid_config, config), - {:ok, token_url} <- fetch_from_openid_config(openid_config, "token_endpoint") do - + {:ok, method} <- fetch_client_authentication_method(openid_config, config), + {:ok, token_url} <- fetch_from_openid_config(openid_config, "token_endpoint") do config |> Config.put(:openid_configuration, openid_config) |> Config.put(:auth_method, method) @@ -204,13 +231,13 @@ defmodule Assent.Strategy.OIDC do end defp fetch_client_authentication_method(openid_config, config) do - method = Config.get(config, :client_authentication_method, "client_secret_basic") + method = Config.get(config, :client_authentication_method, "client_secret_basic") methods = Map.get(openid_config, "token_endpoint_auth_methods_supported") supported_method? = if is_nil(methods), do: true, else: method in methods case supported_method? do - true -> to_client_auth_method(method) + true -> to_client_auth_method(method) false -> {:error, "Unsupported client authentication method: #{method}"} end end @@ -219,7 +246,9 @@ defmodule Assent.Strategy.OIDC do defp to_client_auth_method("client_secret_post"), do: {:ok, :client_secret_post} defp to_client_auth_method("client_secret_jwt"), do: {:ok, :client_secret_jwt} defp to_client_auth_method("private_key_jwt"), do: {:ok, :private_key_jwt} - defp to_client_auth_method(method), do: {:error, "Invalid client authentication method: #{method}"} + + defp to_client_auth_method(method), + do: {:error, "Invalid client authentication method: #{method}"} # https://openid.net/specs/draft-jones-json-web-token-07.html#ReservedClaimName @reserved_jwt_names ~w(exp nbf iat iss aud prn jti typ) @@ -228,7 +257,7 @@ defmodule Assent.Strategy.OIDC do @id_token_names ~w(iss sub aud exp iat auth_time nonce acr amr azp at_hash c_hash sub_jwk) # All ID Token claim names to be excluded from the user params - @id_token_names_to_exclude Enum.uniq(@reserved_jwt_names ++ @id_token_names -- ~w(sub)) + @id_token_names_to_exclude Enum.uniq(@reserved_jwt_names ++ (@id_token_names -- ~w(sub))) @doc """ Fetches user params from ID token. @@ -239,15 +268,19 @@ defmodule Assent.Strategy.OIDC do @spec fetch_user(Config.t(), map()) :: {:ok, map()} | {:error, term()} def fetch_user(config, token) do with {:ok, id_token} <- fetch_id_token(token), - {:ok, jwt} <- validate_id_token(config, id_token) do + {:ok, jwt} <- validate_id_token(config, id_token) do {:ok, Map.drop(jwt.claims, @id_token_names_to_exclude)} end end defp fetch_id_token(token) do case Map.fetch(token, "id_token") do - {:ok, id_token} -> {:ok, id_token} - :error -> {:error, "The `id_token` key not found in token params, only found these keys: #{Enum.join(Map.keys(token), ", ")}"} + {:ok, id_token} -> + {:ok, id_token} + + :error -> + {:error, + "The `id_token` key not found in token params, only found these keys: #{Enum.join(Map.keys(token), ", ")}"} end end @@ -265,24 +298,24 @@ defmodule Assent.Strategy.OIDC do expected_alg = Config.get(config, :id_token_signed_response_alg, "RS256") with {:ok, openid_config} <- openid_configuration(config), - {:ok, client_id} <- Config.fetch(config, :client_id), - {:ok, issuer} <- fetch_from_openid_config(openid_config, "issuer"), - {:ok, jwt} <- verify_jwt(id_token, openid_config, config), - :ok <- validate_required_fields(jwt), - :ok <- validate_issuer_identifier(jwt, issuer), - :ok <- validate_audience(jwt, client_id, config), - :ok <- validate_authorization_party(jwt, client_id, config), - :ok <- validate_alg(jwt, expected_alg), - :ok <- validate_verified(jwt), - :ok <- validate_expiration(jwt), - :ok <- validate_issued_at(jwt, config), - :ok <- validate_nonce(jwt, config) do + {:ok, client_id} <- Config.fetch(config, :client_id), + {:ok, issuer} <- fetch_from_openid_config(openid_config, "issuer"), + {:ok, jwt} <- verify_jwt(id_token, openid_config, config), + :ok <- validate_required_fields(jwt), + :ok <- validate_issuer_identifier(jwt, issuer), + :ok <- validate_audience(jwt, client_id, config), + :ok <- validate_authorization_party(jwt, client_id, config), + :ok <- validate_alg(jwt, expected_alg), + :ok <- validate_verified(jwt), + :ok <- validate_expiration(jwt), + :ok <- validate_issued_at(jwt, config), + :ok <- validate_nonce(jwt, config) do {:ok, jwt} end end defp verify_jwt(token, openid_config, config) do - with {:ok, header} <- peek_header(token, config), + with {:ok, header} <- peek_header(token, config), {:ok, secret_or_key} <- fetch_secret(header, openid_config, config) do Helpers.verify_jwt(token, secret_or_key, config) end @@ -290,21 +323,23 @@ defmodule Assent.Strategy.OIDC do defp peek_header(encoded, config) do with [header, _, _] <- String.split(encoded, "."), - {:ok, json} <- Base.url_decode64(header, padding: false) do + {:ok, json} <- Base.url_decode64(header, padding: false) do Config.json_library(config).decode(json) else {:error, error} -> {:error, error} - _any -> {:error, "The ID Token is not a valid JWT"} + _any -> {:error, "The ID Token is not a valid JWT"} end end defp fetch_secret(%{"alg" => "none"}, _openid_config, _config), do: {:ok, ""} + defp fetch_secret(%{"alg" => "HS" <> _rest}, _openid_config, config) do Config.fetch(config, :client_secret) end + defp fetch_secret(header, openid_config, config) do with {:ok, jwks_uri} <- fetch_from_openid_config(openid_config, "jwks_uri"), - {:ok, keys} <- fetch_public_keys(jwks_uri, config) do + {:ok, keys} <- fetch_public_keys(jwks_uri, config) do find_key(header, keys) end end @@ -315,53 +350,78 @@ defmodule Assent.Strategy.OIDC do |> process_public_keys_response() end - defp process_public_keys_response({:ok, %HTTPResponse{status: 200, body: %{"keys" => keys}}}), do: {:ok, keys} + defp process_public_keys_response({:ok, %HTTPResponse{status: 200, body: %{"keys" => keys}}}), + do: {:ok, keys} + defp process_public_keys_response({:ok, %HTTPResponse{status: 200}}), do: {:ok, []} defp process_public_keys_response(any), do: process_response(any) defp find_key(%{"kid" => kid}, [%{"kid" => kid} = key | _keys]), do: {:ok, key} - defp find_key(%{"kid" => _kid} = header, [%{"kid" => _other} | keys]), do: find_key(header, keys) - defp find_key(%{"kid" => kid}, []), do: {:error, "No keys found for the `kid` value \"#{kid}\" provided in ID Token"} + + defp find_key(%{"kid" => _kid} = header, [%{"kid" => _other} | keys]), + do: find_key(header, keys) + + defp find_key(%{"kid" => kid}, []), + do: {:error, "No keys found for the `kid` value \"#{kid}\" provided in ID Token"} + defp find_key(_header, []), do: {:error, "No keys found in `jwks_uri` provider configuration"} defp find_key(_header, [key]), do: {:ok, key} - defp find_key(_header, _keys), do: {:error, "Multiple public keys found in provider configuration and no `kid` value in ID Token"} + + defp find_key(_header, _keys) do + {:error, + "Multiple public keys found in provider configuration and no `kid` value in ID Token"} + end defp validate_required_fields(%{claims: claims}) do Enum.find_value(~w(iss sub aud exp iat), :ok, fn key -> case Map.has_key?(claims, key) do - true -> nil + true -> nil false -> {:error, "Missing `#{key}` in ID Token claims"} end end) end defp validate_issuer_identifier(%{claims: %{"iss" => iss}}, iss), do: :ok - defp validate_issuer_identifier(%{claims: %{"iss" => iss}}, _iss), do: {:error, "Invalid issuer \"#{iss}\" in ID Token"} - defp validate_audience(%{claims: %{"aud" => aud} = claims} = jwt, client_id, config) when is_binary(aud) do + defp validate_issuer_identifier(%{claims: %{"iss" => iss}}, _iss), + do: {:error, "Invalid issuer \"#{iss}\" in ID Token"} + + defp validate_audience(%{claims: %{"aud" => aud} = claims} = jwt, client_id, config) + when is_binary(aud) do validate_audience(%{jwt | claims: %{claims | "aud" => [aud]}}, client_id, config) end + defp validate_audience(%{claims: %{"aud" => [client_id]}}, client_id, _config), do: :ok + defp validate_audience(%{claims: %{"aud" => auds}}, client_id, config) do trusted_audiences = Config.get(config, :trusted_audiences, []) ++ [client_id] missing_client_id? = client_id not in auds - untrusted_auds = Enum.filter(auds, & &1 not in trusted_audiences) + untrusted_auds = Enum.filter(auds, &(&1 not in trusted_audiences)) case {missing_client_id?, untrusted_auds} do - {false, []} -> :ok - {true, _} -> {:error, "`:client_id` not in audience #{inspect auds} in ID Token"} - {false, untrusted_auds} -> {:error, "Untrusted audience(s) #{inspect untrusted_auds} in ID Token"} + {false, []} -> + :ok + + {true, _} -> + {:error, "`:client_id` not in audience #{inspect(auds)} in ID Token"} + + {false, untrusted_auds} -> + {:error, "Untrusted audience(s) #{inspect(untrusted_auds)} in ID Token"} end end defp validate_authorization_party(%{claims: %{"azp" => client_id}}, client_id, _config), do: :ok + defp validate_authorization_party(%{claims: %{"azp" => azp}}, _client_id, _config) do {:error, "Invalid authorized party \"#{azp}\" in ID Token"} end + defp validate_authorization_party(_jwt, _client_id, _config), do: :ok defp validate_alg(%{header: %{"alg" => alg}}, alg), do: :ok - defp validate_alg(%{header: %{"alg" => alg}}, expected_alg), do: {:error, "Expected `alg` in ID Token to be \"#{expected_alg}\", got \"#{alg}\""} + + defp validate_alg(%{header: %{"alg" => alg}}, expected_alg), + do: {:error, "Expected `alg` in ID Token to be \"#{expected_alg}\", got \"#{alg}\""} defp validate_verified(%{verified?: true}), do: :ok defp validate_verified(%{verified?: false}), do: {:error, "Invalid JWT signature for ID Token"} @@ -370,7 +430,7 @@ defmodule Assent.Strategy.OIDC do now = :os.system_time(:second) case exp > now do - true -> :ok + true -> :ok false -> {:error, "The ID Token has expired"} end end @@ -386,7 +446,7 @@ defmodule Assent.Strategy.OIDC do now = :os.system_time(:second) case iat + ttl > now do - true -> :ok + true -> :ok false -> {:error, "The ID Token was issued too long ago"} end end @@ -403,10 +463,13 @@ defmodule Assent.Strategy.OIDC do false -> {:error, "Invalid `nonce` included in ID Token"} end end + defp validate_for_nonce(%{nonce: _nonce}, _jwt), do: {:error, "`nonce` is not included in ID Token"} + defp validate_for_nonce(_any, %{claims: %{"nonce" => _nonce}}), do: {:error, "`nonce` included in ID Token but doesn't exist in session params"} + defp validate_for_nonce(_any, _jwt), do: :ok @doc """ @@ -421,9 +484,10 @@ defmodule Assent.Strategy.OIDC do @spec fetch_userinfo(Config.t(), map()) :: {:ok, map()} | {:error, term()} def fetch_userinfo(config, token) do with {:ok, openid_config} <- openid_configuration(config), - {:ok, userinfo_url} <- fetch_from_openid_config(openid_config, "userinfo_endpoint"), - {:ok, claims} <- fetch_from_userinfo_endpoint(config, openid_config, token, userinfo_url), - :ok <- validate_userinfo_sub(config, token["id_token"], claims) do + {:ok, userinfo_url} <- fetch_from_openid_config(openid_config, "userinfo_endpoint"), + {:ok, claims} <- + fetch_from_userinfo_endpoint(config, openid_config, token, userinfo_url), + :ok <- validate_userinfo_sub(config, token["id_token"], claims) do {:ok, claims} end end @@ -434,18 +498,30 @@ defmodule Assent.Strategy.OIDC do |> process_userinfo_response(openid_config, config) end - defp process_userinfo_response({:ok, %HTTPResponse{status: 200, body: body, headers: headers}}, openid_config, config) do + defp process_userinfo_response( + {:ok, %HTTPResponse{status: 200, body: body, headers: headers}}, + openid_config, + config + ) do case List.keyfind(headers, "content-type", 0) do {"content-type", "application/jwt" <> _rest} -> process_jwt(body, openid_config, config) - _any -> {:ok, body} + _any -> {:ok, body} end end - defp process_userinfo_response({:error, %HTTPResponse{status: 401} = response}, _openid_config, _config), do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} + + defp process_userinfo_response( + {:error, %HTTPResponse{status: 401} = response}, + _openid_config, + _config + ) do + {:error, RequestError.exception(message: "Unauthorized token", response: response)} + end + defp process_userinfo_response(any, _openid_config, _config), do: process_response(any) defp process_jwt(body, openid_config, config) do with {:ok, jwt} <- verify_jwt(body, openid_config, config), - :ok <- validate_verified(jwt) do + :ok <- validate_verified(jwt) do {:ok, jwt.claims} end end @@ -455,7 +531,12 @@ defmodule Assent.Strategy.OIDC do validate_userinfo_sub(config, jwt.claims, claims) end end + defp validate_userinfo_sub(_config, %{"sub" => sub}, %{"sub" => sub}), do: :ok - defp validate_userinfo_sub(_config, %{"sub" => _sub_1}, %{"sub" => _sub_2}), do: {:error, "`sub` in userinfo response not the same as in ID Token"} - defp validate_userinfo_sub(_config, %{"sub" => _sub}, _claims), do: {:error, "`sub` not in userinfo response"} + + defp validate_userinfo_sub(_config, %{"sub" => _sub_1}, %{"sub" => _sub_2}), + do: {:error, "`sub` in userinfo response not the same as in ID Token"} + + defp validate_userinfo_sub(_config, %{"sub" => _sub}, _claims), + do: {:error, "`sub` not in userinfo response"} end diff --git a/lib/assent/strategies/oidc/base.ex b/lib/assent/strategies/oidc/base.ex index 061ec7e..78de2e3 100644 --- a/lib/assent/strategies/oidc/base.ex +++ b/lib/assent/strategies/oidc/base.ex @@ -48,14 +48,21 @@ defmodule Assent.Strategy.OIDC.Base do end end - @spec authorize_url(Keyword.t(), module()) :: {:ok, %{session_params: %{state: binary()} | %{state: binary(), nonce: binary()}, url: binary()}} | {:error, term()} + @spec authorize_url(Keyword.t(), module()) :: + {:ok, + %{ + session_params: %{state: binary()} | %{state: binary(), nonce: binary()}, + url: binary() + }} + | {:error, term()} def authorize_url(config, strategy) do config |> set_config(strategy) |> OIDC.authorize_url() end - @spec callback(Keyword.t(), map(), module()) :: {:ok, %{user: map(), token: map()}} | {:error, term()} + @spec callback(Keyword.t(), map(), module()) :: + {:ok, %{user: map(), token: map()}} | {:error, term()} def callback(config, params, strategy) do config = set_config(config, strategy) diff --git a/lib/assent/strategies/slack.ex b/lib/assent/strategies/slack.ex index 2edb518..02fe5a9 100644 --- a/lib/assent/strategies/slack.ex +++ b/lib/assent/strategies/slack.ex @@ -49,9 +49,10 @@ defmodule Assent.Strategy.Slack do defp authorization_params(config) do default = [scope: "openid email profile"] + case Config.fetch(config, :team_id) do {:ok, team_id} -> Config.put(default, :team, team_id) - _error -> default + _error -> default end end end diff --git a/lib/assent/strategies/stripe.ex b/lib/assent/strategies/stripe.ex index 5f712fb..1be45f6 100644 --- a/lib/assent/strategies/stripe.ex +++ b/lib/assent/strategies/stripe.ex @@ -40,14 +40,14 @@ defmodule Assent.Strategy.Stripe do user_url: "/v1/account", auth_method: :client_secret_post ] - end @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["id"], - "email" => user["email"] - }} + {:ok, + %{ + "sub" => user["id"], + "email" => user["email"] + }} end end diff --git a/lib/assent/strategies/twitter.ex b/lib/assent/strategies/twitter.ex index a8718b9..560f9a0 100644 --- a/lib/assent/strategies/twitter.ex +++ b/lib/assent/strategies/twitter.ex @@ -26,7 +26,8 @@ defmodule Assent.Strategy.Twitter do request_token_url: "/oauth/request_token", authorize_url: "/oauth/authenticate", access_token_url: "/oauth/access_token", - user_url: "/1.1/account/verify_credentials.json?include_entities=false&skip_status=true&include_email=true", + user_url: + "/1.1/account/verify_credentials.json?include_entities=false&skip_status=true&include_email=true" ] end @@ -34,22 +35,26 @@ defmodule Assent.Strategy.Twitter do @impl true def callback(config, params) do case Map.has_key?(params, "denied") do - true -> {:error, CallbackError.exception(message: "The user denied the authorization request")} - false -> Base.callback(config, params, __MODULE__) + true -> + {:error, CallbackError.exception(message: "The user denied the authorization request")} + + false -> + Base.callback(config, params, __MODULE__) end end @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["id"], - "name" => user["name"], - "preferred_username" => user["screen_name"], - "profile" => "https://twitter.com/#{user["screen_name"]}", - "picture" => user["profile_image_url_https"], - "website" => user["url"], - "email" => user["email"], - "email_verified" => true - }} + {:ok, + %{ + "sub" => user["id"], + "name" => user["name"], + "preferred_username" => user["screen_name"], + "profile" => "https://twitter.com/#{user["screen_name"]}", + "picture" => user["profile_image_url_https"], + "website" => user["url"], + "email" => user["email"], + "email_verified" => true + }} end end diff --git a/lib/assent/strategies/vk.ex b/lib/assent/strategies/vk.ex index 472a89b..68481d3 100644 --- a/lib/assent/strategies/vk.ex +++ b/lib/assent/strategies/vk.ex @@ -25,11 +25,11 @@ defmodule Assent.Strategy.VK do alias Assent.{Config, Strategy.OAuth2} @profile_fields ["uid", "first_name", "last_name", "photo_200", "screen_name"] - @url_params [fields: Enum.join(@profile_fields, ","), v: "5.69", https: "1"] + @url_params [fields: Enum.join(@profile_fields, ","), v: "5.69", https: "1"] @impl true def default_config(config) do - params = Config.get(config, :user_url_params, []) + params = Config.get(config, :user_url_params, []) user_url_params = Config.merge(@url_params, params) [ @@ -45,13 +45,14 @@ defmodule Assent.Strategy.VK do @impl true def normalize(_config, user) do - {:ok, %{ - "sub" => user["id"], - "given_name" => user["first_name"], - "family_name" => user["last_name"], - "picture" => user["photo_200"], - "email" => user["email"] - }} + {:ok, + %{ + "sub" => user["id"], + "given_name" => user["first_name"], + "family_name" => user["last_name"], + "picture" => user["photo_200"], + "email" => user["email"] + }} end @impl true @@ -82,7 +83,7 @@ defmodule Assent.Strategy.VK do Retrieved an invalid response fetching VK user. User response: - #{inspect user} + #{inspect(user)} """) } end diff --git a/lib/assent/strategy.ex b/lib/assent/strategy.ex index defdc2d..5528dc2 100644 --- a/lib/assent/strategy.ex +++ b/lib/assent/strategy.ex @@ -28,13 +28,16 @@ defmodule Assent.Strategy do """ alias Assent.{Config, HTTPAdapter.HTTPResponse, ServerUnreachableError} - @callback authorize_url(Config.t()) :: {:ok, %{:url => binary(), optional(atom()) => any()}} | {:error, term()} - @callback callback(Config.t(), map()) :: {:ok, %{:user => map(), optional(atom()) => any()}} | {:error, term()} + @callback authorize_url(Config.t()) :: + {:ok, %{:url => binary(), optional(atom()) => any()}} | {:error, term()} + @callback callback(Config.t(), map()) :: + {:ok, %{:user => map(), optional(atom()) => any()}} | {:error, term()} @doc """ Makes a HTTP request. """ - @spec request(atom(), binary(), binary() | nil, list(), Config.t()) :: {:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()} + @spec request(atom(), binary(), binary() | nil, list(), Config.t()) :: + {:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()} def request(method, url, body, headers, config) do {http_adapter, opts} = fetch_http_adapter(config) @@ -44,40 +47,51 @@ defmodule Assent.Strategy do |> decode_response(config) end - @default_http_client Enum.find_value([ - {Req1, Assent.HTTPAdapter.Req}, - {:httpc, Assent.HTTPAdapter.Httpc} - ], - fn {dep, module} -> - Code.ensure_loaded?(dep) && {module, []} - end) + @default_http_client Enum.find_value( + [ + {Req1, Assent.HTTPAdapter.Req}, + {:httpc, Assent.HTTPAdapter.Httpc} + ], + fn {dep, module} -> + Code.ensure_loaded?(dep) && {module, []} + end + ) defp fetch_http_adapter(config) do default_http_adapter = Application.get_env(:assent, :http_adapter, @default_http_client) case Config.get(config, :http_adapter, default_http_adapter) do - {http_adapter, opts} -> {http_adapter, opts} + {http_adapter, opts} -> {http_adapter, opts} http_adapter when is_atom(http_adapter) -> {http_adapter, nil} end end - defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) when status in 200..399 do + defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) + when status in 200..399 do {:ok, %{resp | http_adapter: http_adapter, request_url: url}} end - defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) when status in 400..599 do + + defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) + when status in 400..599 do {:error, %{resp | http_adapter: http_adapter, request_url: url}} end + defp parse_status_response({:error, error}, http_adapter, url) do - {:error, ServerUnreachableError.exception(reason: error, http_adapter: http_adapter, request_url: url)} + {:error, + ServerUnreachableError.exception(reason: error, http_adapter: http_adapter, request_url: url)} end @doc """ Decodes a request response. """ - @spec decode_response({:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()}, Config.t()) :: {:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()} - def decode_response({ok_or_error, %{body: body, headers: headers} = resp}, config) when is_binary(body) do + @spec decode_response( + {:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()}, + Config.t() + ) :: {:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()} + def decode_response({ok_or_error, %{body: body, headers: headers} = resp}, config) + when is_binary(body) do case decode_body(headers, body, config) do - {:ok, body} -> {ok_or_error, %{resp | body: body}} + {:ok, body} -> {ok_or_error, %{resp | body: body}} {:error, error} -> {:error, error} end end @@ -86,10 +100,17 @@ defmodule Assent.Strategy do defp decode_body(headers, body, config) do case List.keyfind(headers, "content-type", 0) do - {"content-type", "application/json" <> _rest} -> decode_json(body, config) - {"content-type", "text/javascript" <> _rest} -> decode_json(body, config) - {"content-type", "application/x-www-form-urlencoded" <> _reset} -> {:ok, URI.decode_query(body)} - _any -> {:ok, body} + {"content-type", "application/json" <> _rest} -> + decode_json(body, config) + + {"content-type", "text/javascript" <> _rest} -> + decode_json(body, config) + + {"content-type", "application/x-www-form-urlencoded" <> _reset} -> + {:ok, URI.decode_query(body)} + + _any -> + {:ok, body} end end @@ -103,15 +124,18 @@ defmodule Assent.Strategy do Verifies a JWT """ @spec verify_jwt(binary(), binary() | map() | nil, Config.t()) :: {:ok, map()} | {:error, any()} - def verify_jwt(token, secret, config), do: Assent.JWTAdapter.verify(token, secret, jwt_adapter_opts(config)) + def verify_jwt(token, secret, config), + do: Assent.JWTAdapter.verify(token, secret, jwt_adapter_opts(config)) - defp jwt_adapter_opts(config), do: Keyword.take(config, [:json_library, :jwt_adapter, :private_key_id]) + defp jwt_adapter_opts(config), + do: Keyword.take(config, [:json_library, :jwt_adapter, :private_key_id]) @doc """ Signs a JWT """ @spec sign_jwt(map(), binary(), binary(), Config.t()) :: {:ok, binary()} | {:error, term()} - def sign_jwt(claims, alg, secret, config), do: Assent.JWTAdapter.sign(claims, alg, secret, jwt_adapter_opts(config)) + def sign_jwt(claims, alg, secret, config), + do: Assent.JWTAdapter.sign(claims, alg, secret, jwt_adapter_opts(config)) @doc """ Generates a URL @@ -119,6 +143,7 @@ defmodule Assent.Strategy do @spec to_url(binary(), binary(), Keyword.t()) :: binary() def to_url(base_url, uri, params \\ []) def to_url(base_url, uri, []), do: endpoint(base_url, uri) + def to_url(base_url, uri, params) do endpoint(base_url, uri) <> "?" <> encode_query(params) end @@ -135,19 +160,23 @@ defmodule Assent.Strategy do encode_pair(value, key) end + defp encode_pair({key, value}, encoded_key) do key = encoded_key <> "[" <> encode_value(key) <> "]" encode_pair(value, key) end + defp encode_pair([{_key, _value} | _rest] = values, encoded_key) do Enum.map(values, &encode_pair(&1, encoded_key)) end + defp encode_pair(values, encoded_key) when is_list(values) do key = encoded_key <> "[]" Enum.map(values, &encode_pair(&1, key)) end + defp encode_pair(value, encoded_key) do encoded_key <> "=" <> encode_value(value) end @@ -156,6 +185,7 @@ defmodule Assent.Strategy do defp endpoint(base_url, <<"/"::utf8, _::binary>> = uri), do: base_url <> uri + defp endpoint(_base_url, url), do: url @@ -166,11 +196,14 @@ defmodule Assent.Strategy do """ @spec normalize_userinfo(map(), map()) :: {:ok, map()} def normalize_userinfo(claims, extra \\ %{}) do - standard_claims = Map.take(claims, - ~w(sub name given_name family_name middle_name nickname + standard_claims = + Map.take( + claims, + ~w(sub name given_name family_name middle_name nickname preferred_username profile picture website email email_verified gender birthdate zoneinfo locale phone_number phone_number_verified - address updated_at)) + address updated_at) + ) {:ok, prune(Map.merge(extra, standard_claims))} end @@ -191,14 +224,15 @@ defmodule Assent.Strategy do config |> strategy.normalize(user) |> case do - {:ok, user} -> normalize_userinfo(user) + {:ok, user} -> normalize_userinfo(user) {:ok, user, extra} -> normalize_userinfo(user, extra) - {:error, error} -> {:error, error} + {:error, error} -> {:error, error} end |> case do {:error, error} -> {:error, error} - {:ok, user} -> {:ok, %{results | user: user}} + {:ok, user} -> {:ok, %{results | user: user}} end end + def __normalize__({:error, error}, _config, _strategy), do: {:error, error} end diff --git a/mix.exs b/mix.exs index f9b8efb..a8ff9dc 100644 --- a/mix.exs +++ b/mix.exs @@ -20,8 +20,9 @@ defmodule Assent.MixProject do # Docs name: "Assent", docs: docs(), - - xref: [exclude: [:certifi, :httpc, Mint.HTTP, JOSE.JWT, JOSE.JWK, JOSE.JWS, :ssl_verify_hostname]] + xref: [ + exclude: [:certifi, :httpc, Mint.HTTP, JOSE.JWT, JOSE.JWK, JOSE.JWS, :ssl_verify_hostname] + ] ] end @@ -33,18 +34,17 @@ defmodule Assent.MixProject do defp deps do [ + # JWT libraries {:jose, "~> 1.8", optional: true}, - + # HTTP clients (Jason is required for Req) {:certifi, ">= 0.0.0", optional: true}, {:ssl_verify_fun, ">= 0.0.0", optional: true}, - {:finch, "~> 0.15", optional: true}, {:mint, "~> 1.0", optional: true}, {:req, "~> 0.4", optional: true}, - {:jason, "~> 1.0", optional: true}, # Required for Req - + {:jason, "~> 1.0", optional: true}, + # Docs and tests {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, - {:credo, "~> 1.1", only: [:dev, :test]}, {:test_server, "~> 0.1.0", only: :test}, {:bandit, ">= 0.0.0", only: :test} @@ -74,12 +74,12 @@ defmodule Assent.MixProject do source_url: @source_url, extras: [ "README.md": [filename: "README"], - "CHANGELOG.md": [filename: "CHANGELOG"], + "CHANGELOG.md": [filename: "CHANGELOG"] ], groups_for_modules: [ Strategies: ~r/^Assent\.Strategy/, - "HTTP": ~r/^Assent\.HTTPAdapter.*(? - FinchAdapter.request(:get, TestServer.url(), nil, []) - end + assert_raise RuntimeError, + "Missing `:supervisor` option for the Assent.HTTPAdapter.Finch configuration", + fn -> + FinchAdapter.request(:get, TestServer.url(), nil, []) + end end test "handles HTTP/1" do @@ -22,39 +24,54 @@ defmodule Assent.HTTPAdapter.FinchTest do supervisor = start_supervised_finch!(protocol: :http1) - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = FinchAdapter.request(:get, TestServer.url(), nil, [], supervisor: supervisor) + assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = + FinchAdapter.request(:get, TestServer.url(), nil, [], supervisor: supervisor) end test "handles SSL" do TestServer.start(scheme: :https) TestServer.add("/", via: :get) - supervisor = start_supervised_finch!(conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]]) + supervisor = + start_supervised_finch!( + conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]] + ) - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/2"}} = FinchAdapter.request(:get, TestServer.url(), nil, [], supervisor: supervisor) + assert {:ok, %HTTPResponse{status: 200, body: "HTTP/2"}} = + FinchAdapter.request(:get, TestServer.url(), nil, [], supervisor: supervisor) end test "handles SSL with bad certificate" do TestServer.start(scheme: :https) - supervisor = start_supervised_finch!(conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]]) + supervisor = + start_supervised_finch!( + conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]] + ) bad_host_url = TestServer.url(host: "bad-host.localhost") assert capture_log(fn -> - assert {:error, %Error{reason: :disconnected}} = FinchAdapter.request(:get, bad_host_url, nil, [], supervisor: supervisor) - end) =~ "{bad_cert,hostname_check_failed}" + assert {:error, %Error{reason: :disconnected}} = + FinchAdapter.request(:get, bad_host_url, nil, [], supervisor: supervisor) + end) =~ "{bad_cert,hostname_check_failed}" end test "handles SSL with bad certificate and no verification" do TestServer.start(scheme: :https) TestServer.add("/", via: :get) - supervisor = start_supervised_finch!(conn_opts: [transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none]]) + supervisor = + start_supervised_finch!( + conn_opts: [ + transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none] + ] + ) bad_host_url = TestServer.url(host: "bad-host.localhost") - assert {:ok, %HTTPResponse{status: 200}} = FinchAdapter.request(:get, bad_host_url, nil, [], supervisor: supervisor) + assert {:ok, %HTTPResponse{status: 200}} = + FinchAdapter.request(:get, bad_host_url, nil, [], supervisor: supervisor) end test "handles unreachable host" do @@ -65,42 +82,64 @@ defmodule Assent.HTTPAdapter.FinchTest do supervisor = start_supervised_finch!() assert capture_log(fn -> - assert {:error, %Error{reason: :disconnected}} = FinchAdapter.request(:get, url, nil, [], supervisor: supervisor) - end) =~ "connection refused" + assert {:error, %Error{reason: :disconnected}} = + FinchAdapter.request(:get, url, nil, [], supervisor: supervisor) + end) =~ "connection refused" end test "handles query in URL" do - TestServer.add("/get", via: :get, to: fn conn -> - assert conn.query_string == "a=1" + TestServer.add("/get", + via: :get, + to: fn conn -> + assert conn.query_string == "a=1" - Plug.Conn.send_resp(conn, 200, "") - end) + Plug.Conn.send_resp(conn, 200, "") + end + ) supervisor = start_supervised_finch!() - assert {:ok, %HTTPResponse{status: 200}} = FinchAdapter.request(:get, TestServer.url("/get?a=1"), nil, [], supervisor: supervisor) + assert {:ok, %HTTPResponse{status: 200}} = + FinchAdapter.request(:get, TestServer.url("/get?a=1"), nil, [], + supervisor: supervisor + ) end test "handles POST" do - TestServer.add("/post", via: :post, to: fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn, []) - params = URI.decode_query(body) + TestServer.add("/post", + via: :post, + to: 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"] + assert params["a"] == "1" + assert params["b"] == "2" - Plug.Conn.send_resp(conn, 200, "") - end) + assert Plug.Conn.get_req_header(conn, "content-type") == [ + "application/x-www-form-urlencoded" + ] + + Plug.Conn.send_resp(conn, 200, "") + end + ) supervisor = start_supervised_finch!() - assert {:ok, %HTTPResponse{status: 200}} = FinchAdapter.request(:post, TestServer.url("/post"), "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}], supervisor: supervisor) + assert {:ok, %HTTPResponse{status: 200}} = + FinchAdapter.request( + :post, + TestServer.url("/post"), + "a=1&b=2", + [{"content-type", "application/x-www-form-urlencoded"}], + supervisor: supervisor + ) end end defp start_supervised_finch!(opts \\ []) do - start_supervised!({Finch, name: FinchTest, pools: %{:default => Keyword.put_new(opts, :protocol, :http2)}}) + start_supervised!( + {Finch, name: FinchTest, pools: %{:default => Keyword.put_new(opts, :protocol, :http2)}} + ) FinchTest end diff --git a/test/assent/http_adapter/httpc_test.exs b/test/assent/http_adapter/httpc_test.exs index 56a3203..f7975a4 100644 --- a/test/assent/http_adapter/httpc_test.exs +++ b/test/assent/http_adapter/httpc_test.exs @@ -9,13 +9,26 @@ defmodule Assent.HTTPAdapter.HttpcTest do TestServer.start(scheme: :https) TestServer.add("/", via: :get) - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = Httpc.request(:get, TestServer.url(), nil, [], ssl: [cacerts: TestServer.x509_suite().cacerts]) + assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = + Httpc.request(:get, TestServer.url(), nil, [], + ssl: [cacerts: TestServer.x509_suite().cacerts] + ) File.mkdir_p!("tmp") - File.write!("tmp/cacerts.pem", :public_key.pem_encode(Enum.map(TestServer.x509_suite().cacerts, &{:Certificate, &1, :not_encrypted}))) + + File.write!( + "tmp/cacerts.pem", + :public_key.pem_encode( + Enum.map(TestServer.x509_suite().cacerts, &{:Certificate, &1, :not_encrypted}) + ) + ) + TestServer.add("/", via: :get) - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = Httpc.request(:get, TestServer.url(), nil, [], ssl: [cacertfile: 'tmp/cacerts.pem']) + assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = + Httpc.request(:get, TestServer.url(), nil, [], + ssl: [cacertfile: ~c"tmp/cacerts.pem"] + ) end test "handles SSL with bad certificate" do @@ -24,7 +37,9 @@ defmodule Assent.HTTPAdapter.HttpcTest do bad_host_url = TestServer.url(host: "bad-host.localhost") httpc_opts = [ssl: [cacerts: TestServer.x509_suite().cacerts]] - assert {:error, {:failed_connect, error}} = Httpc.request(:get, bad_host_url, nil, [], httpc_opts) + assert {:error, {:failed_connect, error}} = + Httpc.request(:get, bad_host_url, nil, [], httpc_opts) + assert {:tls_alert, {:handshake_failure, _error}} = fetch_inet_error(error) end @@ -33,9 +48,17 @@ defmodule Assent.HTTPAdapter.HttpcTest do TestServer.add("/", via: :get) bad_host_url = TestServer.url(host: "bad-host.localhost") - httpc_opts = [ssl: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none, verify_fun: {fn _cert, _event, state -> {:valid, state} end, nil}]] - assert {:ok, %HTTPResponse{status: 200}} = Httpc.request(:get, bad_host_url, nil, [], httpc_opts) + httpc_opts = [ + ssl: [ + cacerts: TestServer.x509_suite().cacerts, + verify: :verify_none, + verify_fun: {fn _cert, _event, state -> {:valid, state} end, nil} + ] + ] + + assert {:ok, %HTTPResponse{status: 200}} = + Httpc.request(:get, bad_host_url, nil, [], httpc_opts) end test "with missing ssl_verify_fun" do @@ -66,28 +89,41 @@ defmodule Assent.HTTPAdapter.HttpcTest do end test "handles query in URL" do - TestServer.add("/get", via: :get, to: fn conn -> - assert conn.query_string == "a=1" + TestServer.add("/get", + via: :get, + to: fn conn -> + assert conn.query_string == "a=1" - Plug.Conn.send_resp(conn, 200, "") - end) + Plug.Conn.send_resp(conn, 200, "") + end + ) - assert {:ok, %HTTPResponse{status: 200}} = Httpc.request(:get, TestServer.url("/get?a=1"), nil, []) + assert {:ok, %HTTPResponse{status: 200}} = + Httpc.request(:get, TestServer.url("/get?a=1"), nil, []) end test "handles POST" do - TestServer.add("/post", via: :post, to: 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}} = Httpc.request(:post, TestServer.url("/post"), "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}]) + TestServer.add("/post", + via: :post, + to: 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}} = + Httpc.request(:post, TestServer.url("/post"), "a=1&b=2", [ + {"content-type", "application/x-www-form-urlencoded"} + ]) end end @@ -115,9 +151,15 @@ defmodule Assent.HTTPAdapter.HttpcTest do """) File.cd!("tmp/test_app", fn -> - assert {_stdout, 0} = System.cmd("mix", ["deps.get"]) - assert {_stdout, 0} = System.cmd("mix", ["compile"]) - assert {stdout, 1} = System.cmd("mix", ["run", "-e", "Assent.HTTPAdapter.Httpc.request(:get, \"https://localhost\", nil, [])"], stderr_to_stdout: true) + {_stdout, 0} = System.cmd("mix", ["deps.get"]) + {_stdout, 0} = System.cmd("mix", ["compile"]) + + {stdout, 1} = + System.cmd( + "mix", + ["run", "-e", "Assent.HTTPAdapter.Httpc.request(:get, \"https://localhost\", nil, [])"], + stderr_to_stdout: true + ) stdout end) diff --git a/test/assent/http_adapter/mint_test.exs b/test/assent/http_adapter/mint_test.exs index f931169..88d876a 100644 --- a/test/assent/http_adapter/mint_test.exs +++ b/test/assent/http_adapter/mint_test.exs @@ -11,11 +11,16 @@ defmodule Assent.HTTPAdapter.MintTest do TestServer.start(scheme: :https) TestServer.add("/", via: :get) - mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts], protocols: [:http1]] + mint_opts = [ + transport_opts: [cacerts: TestServer.x509_suite().cacerts], + protocols: [:http1] + ] assert CaptureIO.capture_io(:stderr, fn -> - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = Mint.request(:get, TestServer.url(), nil, [], mint_opts) - end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" + assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = + Mint.request(:get, TestServer.url(), nil, [], mint_opts) + end) =~ + "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" end test "handles SSL with bad certificate" do @@ -25,8 +30,11 @@ defmodule Assent.HTTPAdapter.MintTest do mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts]] assert CaptureIO.capture_io(:stderr, fn -> - assert {:error, %TransportError{reason: {:tls_alert, {:handshake_failure, _error}}}} = Mint.request(:get, bad_host_url, nil, [], mint_opts) - end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" + assert {:error, + %TransportError{reason: {:tls_alert, {:handshake_failure, _error}}}} = + Mint.request(:get, bad_host_url, nil, [], mint_opts) + end) =~ + "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" end test "handles SSL with bad certificate and no verification" do @@ -34,11 +42,16 @@ defmodule Assent.HTTPAdapter.MintTest do TestServer.add("/", via: :get) bad_host_url = TestServer.url(host: "bad-host.localhost") - mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none]] + + mint_opts = [ + transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none] + ] assert CaptureIO.capture_io(:stderr, fn -> - assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:get, bad_host_url, nil, [], mint_opts) - end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" + assert {:ok, %HTTPResponse{status: 200}} = + Mint.request(:get, bad_host_url, nil, [], mint_opts) + end) =~ + "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" end if :crypto.supports()[:curves] do @@ -49,8 +62,10 @@ defmodule Assent.HTTPAdapter.MintTest do mint_opts = [transport_opts: [cacerts: TestServer.x509_suite().cacerts]] assert CaptureIO.capture_io(:stderr, fn -> - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/2"}} = Mint.request(:get, TestServer.url(), nil, [], mint_opts) - end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" + assert {:ok, %HTTPResponse{status: 200, body: "HTTP/2"}} = + Mint.request(:get, TestServer.url(), nil, [], mint_opts) + end) =~ + "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" end else IO.warn("No support curve algorithms, can't test in #{__MODULE__}") @@ -62,37 +77,54 @@ defmodule Assent.HTTPAdapter.MintTest do TestServer.stop() assert CaptureIO.capture_io(:stderr, fn -> - assert {:error, %TransportError{reason: :econnrefused}} = Mint.request(:get, url, nil, []) - end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" + assert {:error, %TransportError{reason: :econnrefused}} = + Mint.request(:get, url, nil, []) + end) =~ + "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" end test "handles query in URL" do - TestServer.add("/get", via: :get, to: fn conn -> - assert conn.query_string == "a=1" + TestServer.add("/get", + via: :get, + to: fn conn -> + assert conn.query_string == "a=1" - Plug.Conn.send_resp(conn, 200, "") - end) + Plug.Conn.send_resp(conn, 200, "") + end + ) assert CaptureIO.capture_io(:stderr, fn -> - assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:get, TestServer.url("/get?a=1"), nil, []) - end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" + assert {:ok, %HTTPResponse{status: 200}} = + Mint.request(:get, TestServer.url("/get?a=1"), nil, []) + end) =~ + "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" end test "handles POST" do - TestServer.add("/post", via: :post, to: fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn, []) - params = URI.decode_query(body) + TestServer.add("/post", + via: :post, + to: fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn, []) + params = URI.decode_query(body) + + assert params["a"] == "1" + assert params["b"] == "2" - assert params["a"] == "1" - assert params["b"] == "2" - assert Plug.Conn.get_req_header(conn, "content-type") == ["application/x-www-form-urlencoded"] + assert Plug.Conn.get_req_header(conn, "content-type") == [ + "application/x-www-form-urlencoded" + ] - Plug.Conn.send_resp(conn, 200, "") - end) + Plug.Conn.send_resp(conn, 200, "") + end + ) assert CaptureIO.capture_io(:stderr, fn -> - assert {:ok, %HTTPResponse{status: 200}} = Mint.request(:post, TestServer.url("/post"), "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}]) - end) =~ "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" + assert {:ok, %HTTPResponse{status: 200}} = + Mint.request(:post, TestServer.url("/post"), "a=1&b=2", [ + {"content-type", "application/x-www-form-urlencoded"} + ]) + end) =~ + "Assent.HTTPAdapter.Mint is deprecated, consider use Assent.HTTPAdapter.Finch instead" end end end diff --git a/test/assent/http_adapter/req_test.exs b/test/assent/http_adapter/req_test.exs index ee56d4b..e646b80 100644 --- a/test/assent/http_adapter/req_test.exs +++ b/test/assent/http_adapter/req_test.exs @@ -12,7 +12,8 @@ defmodule Assent.HTTPAdapter.ReqTest do req_opts = [connect_options: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]]] - assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = Req.request(:get, TestServer.url(), nil, [], req_opts) + assert {:ok, %HTTPResponse{status: 200, body: "HTTP/1.1"}} = + Req.request(:get, TestServer.url(), nil, [], req_opts) end test "handles SSL with bad certificate" do @@ -21,7 +22,8 @@ defmodule Assent.HTTPAdapter.ReqTest do bad_host_url = TestServer.url(host: "bad-host.localhost") req_opts = [connect_options: [transport_opts: [cacerts: TestServer.x509_suite().cacerts]]] - assert {:error, %TransportError{reason: {:tls_alert, {:handshake_failure, _error}}}} = Req.request(:get, bad_host_url, nil, [], req_opts) + assert {:error, %TransportError{reason: {:tls_alert, {:handshake_failure, _error}}}} = + Req.request(:get, bad_host_url, nil, [], req_opts) end test "handles SSL with bad certificate and no verification" do @@ -29,9 +31,15 @@ defmodule Assent.HTTPAdapter.ReqTest do TestServer.add("/", via: :get) bad_host_url = TestServer.url(host: "bad-host.localhost") - req_opts = [connect_options: [transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none]]] - assert {:ok, %HTTPResponse{status: 200}} = Req.request(:get, bad_host_url, nil, [], req_opts) + req_opts = [ + connect_options: [ + transport_opts: [cacerts: TestServer.x509_suite().cacerts, verify: :verify_none] + ] + ] + + assert {:ok, %HTTPResponse{status: 200}} = + Req.request(:get, bad_host_url, nil, [], req_opts) end test "handles unreachable host" do @@ -39,32 +47,46 @@ defmodule Assent.HTTPAdapter.ReqTest do url = TestServer.url() TestServer.stop() - assert {:error, %TransportError{reason: :econnrefused}} = Req.request(:get, url, nil, [], retry: false) + assert {:error, %TransportError{reason: :econnrefused}} = + Req.request(:get, url, nil, [], retry: false) end test "handles query in URL" do - TestServer.add("/get", via: :get, to: fn conn -> - assert conn.query_string == "a=1" + TestServer.add("/get", + via: :get, + to: fn conn -> + assert conn.query_string == "a=1" - Plug.Conn.send_resp(conn, 200, "") - end) + Plug.Conn.send_resp(conn, 200, "") + end + ) - assert {:ok, %HTTPResponse{status: 200}} = Req.request(:get, TestServer.url("/get?a=1"), nil, []) + assert {:ok, %HTTPResponse{status: 200}} = + Req.request(:get, TestServer.url("/get?a=1"), nil, []) end test "handles POST" do - TestServer.add("/post", via: :post, to: 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}} = Req.request(:post, TestServer.url("/post"), "a=1&b=2", [{"content-type", "application/x-www-form-urlencoded"}]) + TestServer.add("/post", + via: :post, + to: 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}} = + Req.request(:post, TestServer.url("/post"), "a=1&b=2", [ + {"content-type", "application/x-www-form-urlencoded"} + ]) end end end diff --git a/test/assent/jwt_adapter/assent_jwt_test.exs b/test/assent/jwt_adapter/assent_jwt_test.exs index 4574655..0053b45 100644 --- a/test/assent/jwt_adapter/assent_jwt_test.exs +++ b/test/assent/jwt_adapter/assent_jwt_test.exs @@ -16,7 +16,12 @@ defmodule Assent.JWTAdapter.AssentJWTTest do test "sign/2 with invalid header" do unencodable = & &1 - assert {:error, error} = AssentJWT.sign(@claims, "HS256", @secret, json_library: Jason, private_key_id: unencodable) + assert {:error, error} = + AssentJWT.sign(@claims, "HS256", @secret, + json_library: Jason, + private_key_id: unencodable + ) + assert error.message == "Failed to encode header" assert %Protocol.UndefinedError{} = error.reason assert %{"typ" => "JWT", "alg" => "HS256", "kid" => _} = error.data @@ -144,49 +149,53 @@ defmodule Assent.JWTAdapter.AssentJWTTest do describe "with private key" do @token "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMzQ1Njc4OTAifQ.Skbmm3dBBdPCt0T1dqgtIYW_xbsmlOMJxC6g4WEgWRbk21tw2r2erDBwPxap4Z1rszWnFrmbULm83YSH-1pcHZ-mdNSqFp4_0mtIR3wHvshLSBhxL_3nuwV0hRYUqjjWOZRsBiZEHi9aZMVTm4dWsQlTJAHqQV1igwayn59d0TKmLSgDMvKxQU59SjBeXjVVia05IK7h6zJQ5GmjpzQmbOVpgig3_fxsuDP5-DXyteXKkLbLU23L_K2Pr8FgiJ_KlG2JpIoUB3DcR_tm-vmtUv-dB6ndqPC4RFgzt_4MCzZdzf-9cE5v0XwDxvKpNvZk-UOvTn6bqFdIChJ_1s8WaA" @private_key """ - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw - kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr - m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi - NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV - 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 - QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs - kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go - amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM - +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 - D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC - 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y - lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ - hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp - bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X - +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B - BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC - 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx - QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz - 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 - Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 - NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j - 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma - 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K - y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB - jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= - -----END RSA PRIVATE KEY----- - """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw + kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr + m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi + NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV + 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 + QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs + kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go + amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM + +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 + D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC + 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y + lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ + hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp + bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X + +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B + BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC + 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx + QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz + 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 + Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 + NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j + 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma + 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K + y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB + jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= + -----END RSA PRIVATE KEY----- + """ @public_key """ - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv - vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc - aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy - tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 - e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb - V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 - MwIDAQAB - -----END PUBLIC KEY----- - """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv + vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc + aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy + tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 + e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb + V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 + MwIDAQAB + -----END PUBLIC KEY----- + """ test "sign/2" do assert AssentJWT.sign(@claims, "RS256", @private_key, json_library: Jason) == {:ok, @token} - refute AssentJWT.sign(@claims, "RS256", @private_key, json_library: Jason, private_key_id: "key_id") == {:ok, @token} + + refute AssentJWT.sign(@claims, "RS256", @private_key, + json_library: Jason, + private_key_id: "key_id" + ) == {:ok, @token} end test "sign/2 with invalid algorithm" do @@ -201,7 +210,9 @@ defmodule Assent.JWTAdapter.AssentJWTTest do assert error.message == "Failed to sign JWT" assert error.reason == "Invalid private key" - assert {:error, error} = AssentJWT.sign(@claims, "RS256", @private_key <> @private_key, json_library: Jason) + assert {:error, error} = + AssentJWT.sign(@claims, "RS256", @private_key <> @private_key, json_library: Jason) + assert error.message == "Failed to sign JWT" assert error.reason == "Private key should only have one entry" end @@ -215,7 +226,8 @@ defmodule Assent.JWTAdapter.AssentJWTTest do @jwk %{ "e" => "AQAB", "kty" => "RSA", - "n" => "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw" + "n" => + "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw" } test "verify/3 with JWK" do @@ -235,18 +247,18 @@ defmodule Assent.JWTAdapter.AssentJWTTest do describe "with private key using ES256" do @token "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA" @private_key """ - -----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 - OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r - 1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G - -----END PRIVATE KEY----- - """ + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 + OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r + 1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G + -----END PRIVATE KEY----- + """ @public_key """ - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 - q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== - -----END PUBLIC KEY----- - """ + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 + q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== + -----END PUBLIC KEY----- + """ @claims %{ "sub" => "1234567890", "name" => "John Doe", @@ -265,7 +277,9 @@ defmodule Assent.JWTAdapter.AssentJWTTest do end test "sign/2 with invalid algorithm" do - assert {:error, error} = AssentJWT.sign(@claims, "ES000", @private_key, json_library: Jason) + assert {:error, error} = + AssentJWT.sign(@claims, "ES000", @private_key, json_library: Jason) + assert error.message == "Failed to sign JWT" assert error.reason == "Invalid SHA-2 algorithm bit size: 000" assert {_, "ES000"} = error.data @@ -276,7 +290,11 @@ defmodule Assent.JWTAdapter.AssentJWTTest do assert error.message == "Failed to sign JWT" assert error.reason == "Invalid private key" - assert {:error, error} = AssentJWT.sign(@claims, "ES256", @private_key <> @private_key, json_library: Jason) + assert {:error, error} = + AssentJWT.sign(@claims, "ES256", @private_key <> @private_key, + json_library: Jason + ) + assert error.message == "Failed to sign JWT" assert error.reason == "Private key should only have one entry" end diff --git a/test/assent/jwt_adapter/jose_test.exs b/test/assent/jwt_adapter/jose_test.exs index d222b6e..356c1e5 100644 --- a/test/assent/jwt_adapter/jose_test.exs +++ b/test/assent/jwt_adapter/jose_test.exs @@ -25,49 +25,53 @@ defmodule Assent.JWTAdapter.JOSETest do describe "with private key" do @token "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMzQ1Njc4OTAifQ.Skbmm3dBBdPCt0T1dqgtIYW_xbsmlOMJxC6g4WEgWRbk21tw2r2erDBwPxap4Z1rszWnFrmbULm83YSH-1pcHZ-mdNSqFp4_0mtIR3wHvshLSBhxL_3nuwV0hRYUqjjWOZRsBiZEHi9aZMVTm4dWsQlTJAHqQV1igwayn59d0TKmLSgDMvKxQU59SjBeXjVVia05IK7h6zJQ5GmjpzQmbOVpgig3_fxsuDP5-DXyteXKkLbLU23L_K2Pr8FgiJ_KlG2JpIoUB3DcR_tm-vmtUv-dB6ndqPC4RFgzt_4MCzZdzf-9cE5v0XwDxvKpNvZk-UOvTn6bqFdIChJ_1s8WaA" @private_key """ - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw - kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr - m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi - NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV - 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 - QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs - kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go - amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM - +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 - D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC - 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y - lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ - hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp - bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X - +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B - BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC - 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx - QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz - 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 - Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 - NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j - 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma - 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K - y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB - jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= - -----END RSA PRIVATE KEY----- - """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw + kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr + m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi + NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV + 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 + QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs + kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go + amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM + +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 + D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC + 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y + lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ + hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp + bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X + +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B + BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC + 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx + QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz + 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 + Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 + NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j + 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma + 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K + y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB + jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= + -----END RSA PRIVATE KEY----- + """ @public_key """ - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv - vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc - aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy - tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 - e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb - V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 - MwIDAQAB - -----END PUBLIC KEY----- - """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv + vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc + aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy + tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 + e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb + V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 + MwIDAQAB + -----END PUBLIC KEY----- + """ test "sign/2" do assert JOSE.sign(@claims, "RS256", @private_key, json_library: Jason) == {:ok, @token} - refute JOSE.sign(@claims, "RS256", @private_key, json_library: Jason, private_key_id: "key_id") == {:ok, @token} + + refute JOSE.sign(@claims, "RS256", @private_key, + json_library: Jason, + private_key_id: "key_id" + ) == {:ok, @token} end test "verify/3" do @@ -79,7 +83,8 @@ defmodule Assent.JWTAdapter.JOSETest do @jwk %{ "e" => "AQAB", "kty" => "RSA", - "n" => "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw" + "n" => + "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw" } test "verify/3 with JWK" do diff --git a/test/assent/strategies/apple_test.exs b/test/assent/strategies/apple_test.exs index 5ffc67d..facca55 100644 --- a/test/assent/strategies/apple_test.exs +++ b/test/assent/strategies/apple_test.exs @@ -39,7 +39,8 @@ defmodule Assent.Strategy.AppleTest do "kid" => "AIDOPK1", "use" => "sig", "alg" => "RS256", - "n" => "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw", + "n" => + "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw", "e" => "AQAB" } @@ -47,12 +48,12 @@ defmodule Assent.Strategy.AppleTest do config = config |> Keyword.delete(:openid_configuration) - |> Keyword.merge([ + |> Keyword.merge( client_id: @client_id, team_id: @team_id, private_key_id: @private_key_id, private_key: @private_key - ]) + ) {:ok, config: config} end @@ -68,17 +69,22 @@ defmodule Assent.Strategy.AppleTest do test "callback/2", %{config: config, callback_params: params} do url = TestServer.url() - expect_oidc_access_token_request([id_token_opts: [claims: @id_token_claims], uri: "/auth/token"], fn _conn, params -> - assert {:ok, jwt} = AssentJWT.verify(params["client_secret"], @public_key, json_library: Jason) - assert jwt.verified? - assert jwt.header["alg"] == "ES256" - assert jwt.header["typ"] == "JWT" - assert jwt.header["kid"] == @private_key_id - assert jwt.claims["iss"] == @team_id - assert jwt.claims["sub"] == @client_id - assert jwt.claims["aud"] == url - assert jwt.claims["exp"] > DateTime.to_unix(DateTime.utc_now()) - end) + expect_oidc_access_token_request( + [id_token_opts: [claims: @id_token_claims], uri: "/auth/token"], + fn _conn, params -> + assert {:ok, jwt} = + AssentJWT.verify(params["client_secret"], @public_key, json_library: Jason) + + assert jwt.verified? + assert jwt.header["alg"] == "ES256" + assert jwt.header["typ"] == "JWT" + assert jwt.header["kid"] == @private_key_id + assert jwt.claims["iss"] == @team_id + assert jwt.claims["sub"] == @client_id + assert jwt.claims["aud"] == url + assert jwt.claims["exp"] > DateTime.to_unix(DateTime.utc_now()) + end + ) expect_oidc_jwks_uri_request(uri: "/auth/keys", keys: [@jwk]) @@ -90,14 +96,20 @@ defmodule Assent.Strategy.AppleTest do expected_user = Map.merge(@user, %{"given_name" => "John", "family_name" => "Doe"}) encoded_user = - Jason.encode!(%{name: %{ - firstName: "John", - lastName: "Doe" - }}) + Jason.encode!(%{ + name: %{ + firstName: "John", + lastName: "Doe" + } + }) params = Map.put(params, "user", encoded_user) - expect_oidc_access_token_request(id_token_opts: [claims: @id_token_claims, kid: @jwk["kid"]], uri: "/auth/token") + expect_oidc_access_token_request( + id_token_opts: [claims: @id_token_claims, kid: @jwk["kid"]], + uri: "/auth/token" + ) + expect_oidc_jwks_uri_request(uri: "/auth/keys", keys: [@jwk]) assert {:ok, %{user: user}} = Apple.callback(config, params) @@ -110,7 +122,8 @@ defmodule Assent.Strategy.AppleTest do |> Keyword.delete(:private_key) |> Keyword.put(:private_key_path, "tmp/missing.pem") - assert Apple.callback(config, params) == {:error, "Failed to read \"tmp/missing.pem\", got; :enoent"} + assert Apple.callback(config, params) == + {:error, "Failed to read \"tmp/missing.pem\", got; :enoent"} end else IO.warn("No support curve algorithms, can't test #{__MODULE__}") diff --git a/test/assent/strategies/auth0_test.exs b/test/assent/strategies/auth0_test.exs index 304dfd0..67c0e46 100644 --- a/test/assent/strategies/auth0_test.exs +++ b/test/assent/strategies/auth0_test.exs @@ -52,6 +52,7 @@ defmodule Assent.Strategy.Auth0Test do expect_oauth2_access_token_request([uri: "/oauth/token"], fn _conn, params -> assert params["client_secret"] == config[:client_secret] end) + expect_oauth2_user_request(@user_response, uri: "/userinfo") assert {:ok, %{user: user}} = Auth0.callback(config, params) diff --git a/test/assent/strategies/azure_ad_test.exs b/test/assent/strategies/azure_ad_test.exs index 16df240..5b4a5da 100644 --- a/test/assent/strategies/azure_ad_test.exs +++ b/test/assent/strategies/azure_ad_test.exs @@ -17,13 +17,15 @@ defmodule Assent.Strategy.AzureADTest do "oid" => "00000000-0000-0000-66f3-3332eca7ea81", "tid" => "3338040d-6c67-4c5b-b112-36a304b66dad", "nonce" => "123523", - "aio" => "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY" + "aio" => + "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY" } @user %{ "name" => "Abe Lincoln", "preferred_username" => "AbeLi@microsoft.com", "sub" => "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - "aio" => "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY", + "aio" => + "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY", "oid" => "00000000-0000-0000-66f3-3332eca7ea81", "tid" => "3338040d-6c67-4c5b-b112-36a304b66dad", "ver" => "2.0" @@ -39,11 +41,25 @@ defmodule Assent.Strategy.AzureADTest do test "callback/2", %{config: config, callback_params: params} do openid_config = config[:openid_configuration] - |> Map.put("issuer", "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0") - |> Map.put("token_endpoint_auth_methods_supported", ["client_secret_post", "private_key_jwt", "client_secret_basic"]) + |> Map.put( + "issuer", + "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + ) + |> Map.put("token_endpoint_auth_methods_supported", [ + "client_secret_post", + "private_key_jwt", + "client_secret_basic" + ]) session_params = Map.put(config[:session_params], :nonce, "123523") - config = Keyword.merge(config, openid_configuration: openid_config, tenant_id: "9188040d-6c67-4c5b-b112-36a304b66dad", client_id: "6cb04018-a3f5-46a7-b995-940c78f5aef3", session_params: session_params) + + config = + Keyword.merge(config, + openid_configuration: openid_config, + tenant_id: "9188040d-6c67-4c5b-b112-36a304b66dad", + client_id: "6cb04018-a3f5-46a7-b995-940c78f5aef3", + session_params: session_params + ) [key | _rest] = expect_oidc_jwks_uri_request() expect_oidc_access_token_request(id_token_opts: [claims: @id_token_claims, kid: key["kid"]]) diff --git a/test/assent/strategies/basecamp_test.exs b/test/assent/strategies/basecamp_test.exs index 9a65364..68b7425 100644 --- a/test/assent/strategies/basecamp_test.exs +++ b/test/assent/strategies/basecamp_test.exs @@ -55,6 +55,7 @@ defmodule Assent.Strategy.BasecampTest do expect_oauth2_access_token_request([uri: "/authorization/token"], fn _conn, params -> assert params["client_secret"] == config[:client_secret] end) + expect_oauth2_user_request(@user_response, uri: "/authorization.json") assert {:ok, %{user: user}} = Basecamp.callback(config, params) diff --git a/test/assent/strategies/discord_test.exs b/test/assent/strategies/discord_test.exs index ad5058b..53eab00 100644 --- a/test/assent/strategies/discord_test.exs +++ b/test/assent/strategies/discord_test.exs @@ -17,7 +17,8 @@ defmodule Assent.Strategy.DiscordTest do @user %{ "email" => "nelly@discordapp.com", "email_verified" => true, - "picture" => "https://cdn.discordapp.com/avatars/80351110224678912/8342729096ea3675442027381ff50dfe", + "picture" => + "https://cdn.discordapp.com/avatars/80351110224678912/8342729096ea3675442027381ff50dfe", "preferred_username" => "Nelly", "sub" => "80351110224678912" } @@ -31,6 +32,7 @@ defmodule Assent.Strategy.DiscordTest do expect_oauth2_access_token_request([uri: "/oauth2/token"], fn _conn, params -> assert params["client_secret"] == config[:client_secret] end) + expect_oauth2_user_request(@user_response, uri: "/users/@me") assert {:ok, %{user: user}} = Discord.callback(config, params) diff --git a/test/assent/strategies/facebook_test.exs b/test/assent/strategies/facebook_test.exs index 49cc5d7..7fcdaf1 100644 --- a/test/assent/strategies/facebook_test.exs +++ b/test/assent/strategies/facebook_test.exs @@ -35,7 +35,11 @@ defmodule Assent.Strategy.FacebookTest do assert conn.params["access_token"] == "access_token" assert conn.params["fields"] == "email,name,first_name,last_name,middle_name,link" - assert conn.params["appsecret_proof"] == Base.encode16(:crypto.mac(:hmac, :sha256, "secret", "access_token"), case: :lower) + + assert conn.params["appsecret_proof"] == + Base.encode16(:crypto.mac(:hmac, :sha256, "secret", "access_token"), + case: :lower + ) end) assert {:ok, %{user: user}} = Facebook.callback(config, params) diff --git a/test/assent/strategies/github_test.exs b/test/assent/strategies/github_test.exs index 4d2aff7..342831d 100644 --- a/test/assent/strategies/github_test.exs +++ b/test/assent/strategies/github_test.exs @@ -84,6 +84,7 @@ defmodule Assent.Strategy.GithubTest do expect_oauth2_access_token_request([uri: "/login/oauth/access_token"], fn _conn, params -> assert params["client_secret"] == config[:client_secret] end) + expect_oauth2_user_request(@user_response, uri: "/user") expect_oauth2_api_request("/user/emails", @emails_response) diff --git a/test/assent/strategies/gitlab_test.exs b/test/assent/strategies/gitlab_test.exs index eaf8efd..8bd990a 100644 --- a/test/assent/strategies/gitlab_test.exs +++ b/test/assent/strategies/gitlab_test.exs @@ -57,6 +57,7 @@ defmodule Assent.Strategy.GitlabTest do expect_oauth2_access_token_request([uri: "/oauth/token"], fn _conn, params -> assert params["client_secret"] == config[:client_secret] end) + expect_oauth2_user_request(@user_response, uri: "/api/v4/user") assert {:ok, %{user: user}} = Gitlab.callback(config, params) diff --git a/test/assent/strategies/google_test.exs b/test/assent/strategies/google_test.exs index 22fbfc5..c2871cd 100644 --- a/test/assent/strategies/google_test.exs +++ b/test/assent/strategies/google_test.exs @@ -9,13 +9,14 @@ defmodule Assent.Strategy.GoogleTest do "name" => "Aaron Parecki", "given_name" => "Aaron", "family_name" => "Parecki", - "picture" => "https://lh4.googleusercontent.com/-kw-iMgD_j34/AAAAAAAAAAI/AAAAAAAAAAc/P1YY91tzesU/photo.jpg", + "picture" => + "https://lh4.googleusercontent.com/-kw-iMgD_j34/AAAAAAAAAAI/AAAAAAAAAAc/P1YY91tzesU/photo.jpg", "email" => "aaron.parecki@okta.com", "email_verified" => true, "locale" => "en", "hd" => "okta.com" - } - @user %{ + } + @user %{ "email" => "aaron.parecki@okta.com", "email_verified" => true, "family_name" => "Parecki", @@ -23,7 +24,8 @@ defmodule Assent.Strategy.GoogleTest do "google_hd" => "okta.com", "name" => "Aaron Parecki", "locale" => "en", - "picture" => "https://lh4.googleusercontent.com/-kw-iMgD_j34/AAAAAAAAAAI/AAAAAAAAAAc/P1YY91tzesU/photo.jpg", + "picture" => + "https://lh4.googleusercontent.com/-kw-iMgD_j34/AAAAAAAAAAI/AAAAAAAAAAc/P1YY91tzesU/photo.jpg", "sub" => "110248495921238986420" } @@ -36,6 +38,7 @@ defmodule Assent.Strategy.GoogleTest do expect_oauth2_access_token_request([uri: "/oauth2/v4/token"], fn _conn, params -> assert params["client_secret"] == config[:client_secret] end) + expect_oauth2_user_request(@user_response, uri: "/oauth2/v3/userinfo") assert {:ok, %{user: user}} = Google.callback(config, params) diff --git a/test/assent/strategies/instagram_test.exs b/test/assent/strategies/instagram_test.exs index 7e2c2af..f3b6a14 100644 --- a/test/assent/strategies/instagram_test.exs +++ b/test/assent/strategies/instagram_test.exs @@ -26,9 +26,15 @@ defmodule Assent.Strategy.InstagramTest do end test "normalizes data", %{config: config, callback_params: params} do - expect_oauth2_access_token_request([uri: "/oauth/access_token", params: %{access_token: "access_token", user: @user_response}], fn _conn, params -> - assert params["client_secret"] == config[:client_secret] - end) + expect_oauth2_access_token_request( + [ + uri: "/oauth/access_token", + params: %{access_token: "access_token", user: @user_response} + ], + fn _conn, params -> + assert params["client_secret"] == config[:client_secret] + end + ) expect_oauth2_user_request(@user_response, [uri: "/me"], fn conn -> conn = Plug.Conn.fetch_query_params(conn) diff --git a/test/assent/strategies/line_test.exs b/test/assent/strategies/line_test.exs index d03c392..ce45540 100644 --- a/test/assent/strategies/line_test.exs +++ b/test/assent/strategies/line_test.exs @@ -4,21 +4,25 @@ defmodule Assent.Strategy.LINETest do alias Assent.Strategy.LINE # From https://developers.line.biz/en/docs/line-login/integrate-line-login/#verify-id-token - @id_token elem(Assent.Strategy.sign_jwt( - %{ - "iss" => "https://access.line.me", - "sub" => "U1234567890abcdef1234567890abcdef ", - "aud" => "1234567890", - "exp" => :os.system_time(:second) + 60, - "iat" => :os.system_time(:second), - "nonce" => "0987654asdf", - "amr" => ["pwd"], - "name" => "Taro Line", - "picture" => "https://sample_line.me/aBcdefg123456" - }, - "HS256", - "secret", - []), 1) + @id_token elem( + Assent.Strategy.sign_jwt( + %{ + "iss" => "https://access.line.me", + "sub" => "U1234567890abcdef1234567890abcdef ", + "aud" => "1234567890", + "exp" => :os.system_time(:second) + 60, + "iat" => :os.system_time(:second), + "nonce" => "0987654asdf", + "amr" => ["pwd"], + "name" => "Taro Line", + "picture" => "https://sample_line.me/aBcdefg123456" + }, + "HS256", + "secret", + [] + ), + 1 + ) @user %{ "name" => "Taro Line", "picture" => "https://sample_line.me/aBcdefg123456", @@ -32,9 +36,17 @@ defmodule Assent.Strategy.LINETest do end test "callback/2", %{config: config, callback_params: params} do - openid_config = Map.merge(config[:openid_configuration], %{"issuer" => "https://access.line.me"}) + openid_config = + Map.merge(config[:openid_configuration], %{"issuer" => "https://access.line.me"}) + session_params = Map.put(config[:session_params], :nonce, "0987654asdf") - config = Keyword.merge(config, openid_configuration: openid_config, client_id: "1234567890", session_params: session_params) + + config = + Keyword.merge(config, + openid_configuration: openid_config, + client_id: "1234567890", + session_params: session_params + ) expect_oidc_access_token_request(id_token: @id_token) diff --git a/test/assent/strategies/linkedin_test.exs b/test/assent/strategies/linkedin_test.exs index a8555f9..5043620 100644 --- a/test/assent/strategies/linkedin_test.exs +++ b/test/assent/strategies/linkedin_test.exs @@ -10,7 +10,8 @@ defmodule Assent.Strategy.LinkedInTest do "name" => "John Doe", "given_name" => "John", "family_name" => "Doe", - "picture" => "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/", + "picture" => + "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/", "locale" => "en-US", "email" => "doe@email.com", "email_verified" => true @@ -22,7 +23,8 @@ defmodule Assent.Strategy.LinkedInTest do "given_name" => "John", "family_name" => "Doe", "locale" => "en-US", - "picture" => "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/", + "picture" => + "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/", "sub" => "782bbtaQ" } @@ -36,11 +38,25 @@ defmodule Assent.Strategy.LinkedInTest do "subject_types_supported" => ["pairwise"], "id_token_signing_alg_values_supported" => ["RS256"], "scopes_supported" => ["openid", "profile", "email"], - "claims_supported" => ["iss", "aud", "iat", "exp", "sub", "name", "given_name", "family_name", "picture", "email", "email_verified", "locale"] + "claims_supported" => [ + "iss", + "aud", + "iat", + "exp", + "sub", + "name", + "given_name", + "family_name", + "picture", + "email", + "email_verified", + "locale" + ] } setup %{config: config} do - openid_configuration = Map.merge(@openid_config, Map.delete(config[:openid_configuration], "issuer")) + openid_configuration = + Map.merge(@openid_config, Map.delete(config[:openid_configuration], "issuer")) config = Keyword.put(config, :openid_configuration, openid_configuration) diff --git a/test/assent/strategies/oauth2_test.exs b/test/assent/strategies/oauth2_test.exs index 59b2459..57d9aba 100644 --- a/test/assent/strategies/oauth2_test.exs +++ b/test/assent/strategies/oauth2_test.exs @@ -4,68 +4,81 @@ defmodule Assent.Strategy.OAuth2Test do alias Assent.InvalidResponseError alias Assent.ServerUnreachableError alias Assent.UnexpectedResponseError - alias Assent.{CallbackCSRFError, CallbackError, Config.MissingKeyError, JWTAdapter.AssentJWT, MissingParamError, RequestError, Strategy.OAuth2} + + alias Assent.{ + CallbackCSRFError, + CallbackError, + Config.MissingKeyError, + JWTAdapter.AssentJWT, + MissingParamError, + RequestError, + Strategy.OAuth2 + } @client_id "s6BhdRkqt3" @client_secret "7Fjfp0ZBr1KtDRbnfVdmIw" @private_key_id "key_id" @private_key """ - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw - kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr - m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi - NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV - 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 - QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs - kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go - amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM - +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 - D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC - 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y - lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ - hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp - bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X - +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B - BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC - 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx - QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz - 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 - Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 - NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j - 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma - 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K - y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB - jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= - -----END RSA PRIVATE KEY----- - """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw + kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr + m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi + NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV + 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 + QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs + kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go + amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM + +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 + D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC + 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y + lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ + hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp + bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X + +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B + BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC + 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx + QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz + 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 + Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 + NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j + 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma + 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K + y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB + jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= + -----END RSA PRIVATE KEY----- + """ @public_key """ - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv - vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc - aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy - tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 - e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb - V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 - MwIDAQAB - -----END PUBLIC KEY----- - """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv + vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc + aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy + tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 + e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb + V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 + MwIDAQAB + -----END PUBLIC KEY----- + """ test "authorize_url/2", %{config: config} do assert {:ok, %{url: url, session_params: %{state: state}}} = - config - |> Keyword.put(:client_id, @client_id) - |> OAuth2.authorize_url() + config + |> Keyword.put(:client_id, @client_id) + |> OAuth2.authorize_url() refute is_nil(state) - assert url =~ TestServer.url("/oauth/authorize?client_id=#{@client_id}&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fcallback&response_type=code&state=#{state}") + + assert url =~ + TestServer.url( + "/oauth/authorize?client_id=#{@client_id}&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fcallback&response_type=code&state=#{state}" + ) end test "authorize_url/2 with state in authorization_param", %{config: config} do assert {:ok, %{session_params: %{state: state}}} = - config - |> Keyword.put(:client_id, @client_id) - |> Keyword.put(:authorization_params, state: "state_test_value") - |> OAuth2.authorize_url() + config + |> Keyword.put(:client_id, @client_id) + |> Keyword.put(:authorization_params, state: "state_test_value") + |> OAuth2.authorize_url() assert state == "state_test_value" end @@ -89,7 +102,11 @@ defmodule Assent.Strategy.OAuth2Test do end test "with error params", %{config: config, callback_params: %{"state" => state}} do - params = %{"error" => "access_denied", "error_description" => "The user denied the request", "state" => state} + params = %{ + "error" => "access_denied", + "error_description" => "The user denied the request", + "state" => state + } assert {:error, %CallbackError{} = error} = OAuth2.callback(config, params) assert error.message == "The user denied the request" @@ -123,7 +140,10 @@ defmodule Assent.Strategy.OAuth2Test do assert error.key == "state" end - test "with state param without state in session_params", %{config: config, callback_params: params} do + test "with state param without state in session_params", %{ + config: config, + callback_params: params + } do config = Keyword.put(config, :session_params, %{}) expect_oauth2_access_token_request([]) @@ -154,11 +174,17 @@ defmodule Assent.Strategy.OAuth2Test do end test "with access token error with 200 response", %{config: config, callback_params: params} do - expect_oauth2_access_token_request(params: %{"error" => "error", "error_description" => "Error description"}) + expect_oauth2_access_token_request( + params: %{"error" => "error", "error_description" => "Error description"} + ) assert {:error, %UnexpectedResponseError{} = error} = OAuth2.callback(config, params) assert Exception.message(error) =~ "An unexpected response was received." - assert error.response.body == %{"error" => "error", "error_description" => "Error description"} + + assert error.response.body == %{ + "error" => "error", + "error_description" => "Error description" + } end test "with access token error with 500 response", %{config: config, callback_params: params} do @@ -179,9 +205,12 @@ defmodule Assent.Strategy.OAuth2Test do end test "with invalid token type", %{config: config, callback_params: params} do - expect_oauth2_access_token_request(params: %{access_token: "access_token", token_type: "invalid"}) + expect_oauth2_access_token_request( + params: %{access_token: "access_token", token_type: "invalid"} + ) - assert OAuth2.callback(config, params) == {:error, "Authorization with token type `invalid` not supported"} + assert OAuth2.callback(config, params) == + {:error, "Authorization with token type `invalid` not supported"} end test "with unreachable `:user_url`", %{config: config, callback_params: params} do @@ -263,9 +292,13 @@ defmodule Assent.Strategy.OAuth2Test do assert params["grant_type"] == "authorization_code" assert params["code"] == "code_test_value" assert params["redirect_uri"] == "http://localhost:4000/auth/callback" - assert params["client_assertion_type"] == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - assert {:ok, jwt} = AssentJWT.verify(params["client_assertion"], @client_secret, json_library: Jason) + assert params["client_assertion_type"] == + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + + assert {:ok, jwt} = + AssentJWT.verify(params["client_assertion"], @client_secret, json_library: Jason) + assert jwt.verified? assert jwt.header["alg"] == "HS256" assert jwt.header["typ"] == "JWT" @@ -294,9 +327,13 @@ defmodule Assent.Strategy.OAuth2Test do assert params["grant_type"] == "authorization_code" assert params["code"] == "code_test_value" assert params["redirect_uri"] == "http://localhost:4000/auth/callback" - assert params["client_assertion_type"] == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - assert {:ok, jwt} = AssentJWT.verify(params["client_assertion"], @public_key, json_library: Jason) + assert params["client_assertion_type"] == + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + + assert {:ok, jwt} = + AssentJWT.verify(params["client_assertion"], @public_key, json_library: Jason) + assert jwt.verified? assert jwt.header["alg"] == "RS256" assert jwt.header["typ"] == "JWT" @@ -312,7 +349,10 @@ defmodule Assent.Strategy.OAuth2Test do assert {:ok, _any} = OAuth2.callback(config, params) end - test "with `:private_key_jwt` auth method with private key as file", %{config: config, callback_params: params} do + test "with `:private_key_jwt` auth method with private key as file", %{ + config: config, + callback_params: params + } do File.mkdir_p!("tmp/") File.write!("tmp/private-key.pem", @private_key) @@ -324,7 +364,9 @@ defmodule Assent.Strategy.OAuth2Test do |> Keyword.put(:private_key_id, @private_key_id) expect_oauth2_access_token_request([], fn _conn, params -> - assert {:ok, jwt} = AssentJWT.verify(params["client_assertion"], @public_key, json_library: Jason) + assert {:ok, jwt} = + AssentJWT.verify(params["client_assertion"], @public_key, json_library: Jason) + assert jwt.verified? assert jwt.header["kid"] == @private_key_id end) @@ -334,7 +376,10 @@ defmodule Assent.Strategy.OAuth2Test do assert {:ok, _any} = OAuth2.callback(config, params) end - test "with `:private_key_jwt` auth method with private key as missing file", %{config: config, callback_params: params} do + test "with `:private_key_jwt` auth method with private key as missing file", %{ + config: config, + callback_params: params + } do config = config |> Keyword.delete(:client_secret) @@ -342,7 +387,8 @@ defmodule Assent.Strategy.OAuth2Test do |> Keyword.put(:private_key_path, "tmp/missing.pem") |> Keyword.put(:private_key_id, @private_key_id) - assert {:error, "Failed to read \"tmp/missing.pem\", got; :enoent"} = OAuth2.callback(config, params) + assert {:error, "Failed to read \"tmp/missing.pem\", got; :enoent"} = + OAuth2.callback(config, params) end test "with 201 response", %{config: config, callback_params: params} do @@ -373,21 +419,36 @@ defmodule Assent.Strategy.OAuth2Test do end test "with missing `refresh_token` in token", %{config: config} do - assert OAuth2.refresh_access_token(config, %{}) == {:error, "No `refresh_token` in token map"} + assert OAuth2.refresh_access_token(config, %{}) == + {:error, "No `refresh_token` in token map"} end test "with refresh token error with 200 response", %{config: config} do - expect_oauth2_access_token_request(params: %{"error" => "error", "error_description" => "Error description"}) + expect_oauth2_access_token_request( + params: %{"error" => "error", "error_description" => "Error description"} + ) + + assert {:error, %UnexpectedResponseError{} = error} = + OAuth2.refresh_access_token(config, %{ + "refresh_token" => "refresh_token_test_value" + }) - assert {:error, %UnexpectedResponseError{} = error} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}) assert Exception.message(error) =~ "An unexpected response was received." - assert error.response.body == %{"error" => "error", "error_description" => "Error description"} + + assert error.response.body == %{ + "error" => "error", + "error_description" => "Error description" + } end test "with fresh token error with 500 response", %{config: config} do expect_oauth2_access_token_request(status_code: 500, params: %{error: "Error"}) - assert {:error, %InvalidResponseError{} = error} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}) + assert {:error, %InvalidResponseError{} = error} = + OAuth2.refresh_access_token(config, %{ + "refresh_token" => "refresh_token_test_value" + }) + assert Exception.message(error) =~ "An invalid response was received." assert error.response.status == 500 assert error.response.body == %{"error" => "Error"} @@ -401,7 +462,11 @@ defmodule Assent.Strategy.OAuth2Test do refute params["client_secret"] end) - assert {:ok, token} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}) + assert {:ok, token} = + OAuth2.refresh_access_token(config, %{ + "refresh_token" => "refresh_token_test_value" + }) + assert token == %{"access_token" => "access_token"} end @@ -412,7 +477,12 @@ defmodule Assent.Strategy.OAuth2Test do assert params["scope"] == "test" end) - assert {:ok, _any} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}, scope: "test") + assert {:ok, _any} = + OAuth2.refresh_access_token( + config, + %{"refresh_token" => "refresh_token_test_value"}, + scope: "test" + ) end end @@ -429,14 +499,18 @@ defmodule Assent.Strategy.OAuth2Test do end test "with missing `access_token` in token", %{config: config, token: token} do - token = Map.delete(token, "access_token") + token = Map.delete(token, "access_token") + + assert OAuth2.request(config, token, :get, "/info") == + {:error, "No `access_token` in token map"} - assert OAuth2.request(config, token, :get, "/info") == {:error, "No `access_token` in token map"} - assert OAuth2.request(config, Map.put(token, "token_type", "bearer"), :get, "/info") == {:error, "No `access_token` in token map"} + assert OAuth2.request(config, Map.put(token, "token_type", "bearer"), :get, "/info") == + {:error, "No `access_token` in token map"} end test "with invalid `token_type` in token", %{config: config, token: token} do - assert OAuth2.request(config, Map.put(token, "token_type", "invalid"), :get, "/info") == {:error, "Authorization with token type `invalid` not supported"} + assert OAuth2.request(config, Map.put(token, "token_type", "invalid"), :get, "/info") == + {:error, "Authorization with token type `invalid` not supported"} end test "gets", %{config: config, token: token} do @@ -462,13 +536,19 @@ defmodule Assent.Strategy.OAuth2Test do test "with `token_type=bearer` in token", %{config: config, token: token} do expect_oauth2_api_request("/info", %{"success" => true}) - assert {:ok, response} = OAuth2.request(config, Map.put(token, "token_type", "bearer"), :get, "/info") + + assert {:ok, response} = + OAuth2.request(config, Map.put(token, "token_type", "bearer"), :get, "/info") + assert response.body == %{"success" => true} end test "with `token_type=Bearer` in token", %{config: config, token: token} do expect_oauth2_api_request("/info", %{"success" => true}) - assert {:ok, response} = OAuth2.request(config, Map.put(token, "token_type", "Bearer"), :get, "/info") + + assert {:ok, response} = + OAuth2.request(config, Map.put(token, "token_type", "Bearer"), :get, "/info") + assert response.body == %{"success" => true} end end @@ -481,14 +561,20 @@ defmodule Assent.Strategy.OAuth2Test do assert {:ok, response} = OAuth2.request(config, token, :post, "/info") assert response.body == %{"success" => true} - expect_oauth2_api_request("/info", %{"success" => true}, [], fn conn -> - {:ok, body, _conn} = Plug.Conn.read_body(conn, []) - params = URI.decode_query(body) + expect_oauth2_api_request( + "/info", + %{"success" => true}, + [], + fn conn -> + {:ok, body, _conn} = Plug.Conn.read_body(conn, []) + params = URI.decode_query(body) - assert params["a"] == "1" - end, "POST") + assert params["a"] == "1" + end, + "POST" + ) - assert {:ok, response} = OAuth2.request(config, token, :post, "/info", [a: 1]) + assert {:ok, response} = OAuth2.request(config, token, :post, "/info", a: 1) assert response.body == %{"success" => true} end end diff --git a/test/assent/strategies/oauth_test.exs b/test/assent/strategies/oauth_test.exs index 65fcbb9..fbc1266 100644 --- a/test/assent/strategies/oauth_test.exs +++ b/test/assent/strategies/oauth_test.exs @@ -2,48 +2,55 @@ defmodule Assent.Strategy.OAuthTest do use Assent.Test.OAuthTestCase alias Assent.UnexpectedResponseError - alias Assent.{Config.MissingKeyError, InvalidResponseError, MissingParamError, ServerUnreachableError, Strategy.OAuth} + + alias Assent.{ + Config.MissingKeyError, + InvalidResponseError, + MissingParamError, + ServerUnreachableError, + Strategy.OAuth + } @private_key """ - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw - kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr - m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi - NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV - 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 - QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs - kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go - amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM - +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 - D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC - 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y - lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ - hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp - bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X - +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B - BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC - 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx - QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz - 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 - Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 - NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j - 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma - 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K - y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB - jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= - -----END RSA PRIVATE KEY----- - """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw + kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr + m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi + NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV + 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 + QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs + kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go + amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM + +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 + D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC + 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y + lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ + hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp + bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X + +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B + BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC + 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx + QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz + 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 + Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 + NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j + 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma + 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K + y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB + jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= + -----END RSA PRIVATE KEY----- + """ @public_key """ - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv - vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc - aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy - tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 - e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb - V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 - MwIDAQAB - -----END PUBLIC KEY----- - """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv + vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc + aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy + tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 + e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb + V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 + MwIDAQAB + -----END PUBLIC KEY----- + """ describe "authorize_url/2" do test "with missing `:redirect_uri` config", %{config: config} do @@ -86,33 +93,53 @@ defmodule Assent.Strategy.OAuthTest do end test "with unexpected successful response", %{config: config} do - expect_oauth_request_token_request(params: %{"error_code" => 215, "error_message" => "Bad Authentication data."}) + expect_oauth_request_token_request( + params: %{"error_code" => 215, "error_message" => "Bad Authentication data."} + ) assert {:error, %UnexpectedResponseError{} = error} = OAuth.authorize_url(config) assert Exception.message(error) =~ "An unexpected response was received." assert error.response.http_adapter == Assent.HTTPAdapter.Httpc assert error.response.request_url == TestServer.url("/request_token") assert error.response.status == 200 - assert error.response.body == %{"error_code" => "215", "error_message" => "Bad Authentication data."} + + assert error.response.body == %{ + "error_code" => "215", + "error_message" => "Bad Authentication data." + } end test "with error response", %{config: config} do - expect_oauth_request_token_request(status_code: 500, params: %{"error_code" => 215, "error_message" => "Bad Authentication data."}) + expect_oauth_request_token_request( + status_code: 500, + params: %{"error_code" => 215, "error_message" => "Bad Authentication data."} + ) assert {:error, %InvalidResponseError{} = error} = OAuth.authorize_url(config) assert Exception.message(error) =~ "An invalid response was received." assert error.response.http_adapter == Assent.HTTPAdapter.Httpc assert error.response.request_url == TestServer.url("/request_token") assert error.response.status == 500 - assert error.response.body == %{"error_code" => "215", "error_message" => "Bad Authentication data."} + + assert error.response.body == %{ + "error_code" => "215", + "error_message" => "Bad Authentication data." + } end test "with json error response", %{config: config} do - expect_oauth_request_token_request(status_code: 500, content_type: "application/json", params: %{"errors" => [%{"code" => 215, "message" => "Bad Authentication data."}]}) + expect_oauth_request_token_request( + status_code: 500, + content_type: "application/json", + params: %{"errors" => [%{"code" => 215, "message" => "Bad Authentication data."}]} + ) assert {:error, %InvalidResponseError{} = error} = OAuth.authorize_url(config) assert error.response.status == 500 - assert error.response.body == %{"errors" => [%{"code" => 215, "message" => "Bad Authentication data."}]} + + assert error.response.body == %{ + "errors" => [%{"code" => 215, "message" => "Bad Authentication data."}] + } end test "with missing `oauth_token` in access token response", %{config: config} do @@ -135,7 +162,11 @@ defmodule Assent.Strategy.OAuthTest do request_token_url = TestServer.url("/request_token") expect_oauth_request_token_request([], fn _conn, oauth_params -> - signature_base_string = gen_signature_base_string("POST&#{URI.encode_www_form(request_token_url)}&", oauth_params) + signature_base_string = + gen_signature_base_string( + "POST&#{URI.encode_www_form(request_token_url)}&", + oauth_params + ) assert oauth_params["oauth_callback"] == "http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fcallback" assert oauth_params["oauth_consumer_key"] == config[:consumer_key] @@ -146,11 +177,16 @@ defmodule Assent.Strategy.OAuthTest do assert oauth_params["oauth_version"] == "1.0" assert {:ok, decoded_signature} = Base.decode64(URI.decode(signature)) - assert :crypto.mac(:hmac, :sha, "#{config[:consumer_secret]}&", signature_base_string) == decoded_signature + + assert :crypto.mac(:hmac, :sha, "#{config[:consumer_secret]}&", signature_base_string) == + decoded_signature + assert String.to_integer(timestamp) <= DateTime.to_unix(DateTime.utc_now()) end) - assert {:ok, %{url: url, session_params: %{oauth_token_secret: "hdhd0244k9j7ao03"}}} = OAuth.authorize_url(config) + assert {:ok, %{url: url, session_params: %{oauth_token_secret: "hdhd0244k9j7ao03"}}} = + OAuth.authorize_url(config) + assert url == TestServer.url("/authorize?oauth_token=hh5s93j4hdidpola") end @@ -159,11 +195,19 @@ defmodule Assent.Strategy.OAuthTest do request_token_url = TestServer.url("/request_token") expect_oauth_request_token_request([], fn conn, oauth_params -> - signature_base_string = gen_signature_base_string("POST&#{URI.encode_www_form(request_token_url)}&", Map.merge(oauth_params, conn.query_params)) + signature_base_string = + gen_signature_base_string( + "POST&#{URI.encode_www_form(request_token_url)}&", + Map.merge(oauth_params, conn.query_params) + ) assert conn.query_params == %{"a" => "1", "b" => "2", "c" => "3"} - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) - assert :crypto.mac(:hmac, :sha, "#{config[:consumer_secret]}&", signature_base_string) == decoded_signature + + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) + + assert :crypto.mac(:hmac, :sha, "#{config[:consumer_secret]}&", signature_base_string) == + decoded_signature end) assert {:ok, _res} = OAuth.authorize_url(config) @@ -174,14 +218,28 @@ defmodule Assent.Strategy.OAuthTest do config = Keyword.put(config, :authorization_params, authorization_params) expect_oauth_request_token_request() - assert {:ok, %{url: url, session_params: %{oauth_token_secret: _oauth_token_secret}}} = OAuth.authorize_url(config) - assert url == TestServer.url("/authorize?another_param=param&oauth_token=hh5s93j4hdidpola&scope=reading+writing") + assert {:ok, %{url: url, session_params: %{oauth_token_secret: _oauth_token_secret}}} = + OAuth.authorize_url(config) + + assert url == + TestServer.url( + "/authorize?another_param=param&oauth_token=hh5s93j4hdidpola&scope=reading+writing" + ) end test "parses URI query response", %{config: config} do - expect_oauth_request_token_request(content_type: "text/html", params: URI.encode_query(%{oauth_token: "encoded_uri_request_token", oauth_token_secret: "encoded_uri_token_secret"})) + expect_oauth_request_token_request( + content_type: "text/html", + params: + URI.encode_query(%{ + oauth_token: "encoded_uri_request_token", + oauth_token_secret: "encoded_uri_token_secret" + }) + ) + + assert {:ok, %{url: url, session_params: %{oauth_token_secret: "encoded_uri_token_secret"}}} = + OAuth.authorize_url(config) - assert {:ok, %{url: url, session_params: %{oauth_token_secret: "encoded_uri_token_secret"}}} = OAuth.authorize_url(config) assert url == TestServer.url("/authorize?oauth_token=encoded_uri_request_token") end end @@ -189,7 +247,7 @@ defmodule Assent.Strategy.OAuthTest do defp gen_signature_base_string(method_uri, params) do encoded_normalized_params = params - |> Enum.reject(&elem(&1, 0) in ["oauth_signature"]) + |> Enum.reject(&(elem(&1, 0) in ["oauth_signature"])) |> Enum.map(fn {key, value} -> key <> "=" <> value end) @@ -228,14 +286,28 @@ defmodule Assent.Strategy.OAuthTest do |> List.first() |> :public_key.pem_entry_decode() - signature_base_string = gen_signature_base_string("POST&#{URI.encode_www_form(request_token_url)}&", oauth_params) + signature_base_string = + gen_signature_base_string( + "POST&#{URI.encode_www_form(request_token_url)}&", + oauth_params + ) assert oauth_params["oauth_signature_method"] == "RSA-SHA1" - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) - assert :public_key.verify(signature_base_string, :sha, decoded_signature, decoded_public_key) + + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) + + assert :public_key.verify( + signature_base_string, + :sha, + decoded_signature, + decoded_public_key + ) end) - assert {:ok, %{url: url, session_params: %{oauth_token_secret: "hdhd0244k9j7ao03"}}} = OAuth.authorize_url(config) + assert {:ok, %{url: url, session_params: %{oauth_token_secret: "hdhd0244k9j7ao03"}}} = + OAuth.authorize_url(config) + assert url == TestServer.url("/authorize?oauth_token=hh5s93j4hdidpola") end @@ -250,7 +322,9 @@ defmodule Assent.Strategy.OAuthTest do expect_oauth_request_token_request() - assert {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} = OAuth.authorize_url(config) + assert {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} = + OAuth.authorize_url(config) + refute is_nil(oauth_token_secret) assert url == TestServer.url("/authorize?oauth_token=hh5s93j4hdidpola") end @@ -261,7 +335,8 @@ defmodule Assent.Strategy.OAuthTest do |> Keyword.delete(:private_key) |> Keyword.put(:private_key_path, "tmp/missing.pem") - assert {:error, "Failed to read \"tmp/missing.pem\", got; :enoent"} = OAuth.authorize_url(config) + assert {:error, "Failed to read \"tmp/missing.pem\", got; :enoent"} = + OAuth.authorize_url(config) end end @@ -282,10 +357,14 @@ defmodule Assent.Strategy.OAuthTest do test "returns url", %{config: config} do expect_oauth_request_token_request([], fn _conn, oauth_params -> assert oauth_params["oauth_signature_method"] == "PLAINTEXT" - assert oauth_params["oauth_signature"] == URI.encode("#{config[:consumer_secret]}&", &URI.char_unreserved?/1) + + assert oauth_params["oauth_signature"] == + URI.encode("#{config[:consumer_secret]}&", &URI.char_unreserved?/1) end) - assert {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} = OAuth.authorize_url(config) + assert {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} = + OAuth.authorize_url(config) + refute is_nil(oauth_token_secret) assert url == TestServer.url("/authorize?oauth_token=hh5s93j4hdidpola") end @@ -302,7 +381,10 @@ defmodule Assent.Strategy.OAuthTest do params = Map.delete(params, "oauth_token") assert {:error, %MissingParamError{} = error} = OAuth.callback(config, params) - assert Exception.message(error) == "Expected \"oauth_token\" in params, got: [\"oauth_verifier\"]" + + assert Exception.message(error) == + "Expected \"oauth_token\" in params, got: [\"oauth_verifier\"]" + assert error.expected_key == "oauth_token" assert error.params == %{"oauth_verifier" => "hfdp7dh39dks9884"} end @@ -311,7 +393,10 @@ defmodule Assent.Strategy.OAuthTest do params = Map.delete(params, "oauth_verifier") assert {:error, %MissingParamError{} = error} = OAuth.callback(config, params) - assert Exception.message(error) == "Expected \"oauth_verifier\" in params, got: [\"oauth_token\"]" + + assert Exception.message(error) == + "Expected \"oauth_verifier\" in params, got: [\"oauth_token\"]" + assert error.expected_key == "oauth_verifier" assert error.params == %{"oauth_token" => "hh5s93j4hdidpola"} end @@ -342,23 +427,36 @@ defmodule Assent.Strategy.OAuthTest do assert error.response.body == %{"error" => "Unknown error"} end - test "with missing `oauth_token` in access token response", %{config: config, callback_params: callback_params} do + test "with missing `oauth_token` in access token response", %{ + config: config, + callback_params: callback_params + } do expect_oauth_access_token_request(params: %{oauth_token_secret: "token_secret"}) - assert {:error, %UnexpectedResponseError{} = error} = OAuth.callback(config, callback_params) + assert {:error, %UnexpectedResponseError{} = error} = + OAuth.callback(config, callback_params) + assert Exception.message(error) =~ "An unexpected response was received." assert error.response.body == %{"oauth_token_secret" => "token_secret"} end - test "with missing `oauth_token_secret` in access token response", %{config: config, callback_params: callback_params} do + test "with missing `oauth_token_secret` in access token response", %{ + config: config, + callback_params: callback_params + } do expect_oauth_access_token_request(params: %{oauth_token: "token"}) - assert {:error, %UnexpectedResponseError{} = error} = OAuth.callback(config, callback_params) + assert {:error, %UnexpectedResponseError{} = error} = + OAuth.callback(config, callback_params) + assert Exception.message(error) =~ "An unexpected response was received." assert error.response.body == %{"oauth_token" => "token"} end - test "bubbles up user request error response", %{config: config, callback_params: callback_params} do + test "bubbles up user request error response", %{ + config: config, + callback_params: callback_params + } do expect_oauth_access_token_request() expect_oauth_user_request(%{error: "Unknown error"}, status_code: 500) @@ -371,7 +469,11 @@ defmodule Assent.Strategy.OAuthTest do access_token_url = TestServer.url("/access_token") expect_oauth_access_token_request([], fn _conn, oauth_params -> - signature_base_string = gen_signature_base_string("POST&#{URI.encode_www_form(access_token_url)}&", oauth_params) + signature_base_string = + gen_signature_base_string( + "POST&#{URI.encode_www_form(access_token_url)}&", + oauth_params + ) assert oauth_params["oauth_consumer_key"] == config[:consumer_key] assert signature = oauth_params["oauth_signature"] @@ -380,7 +482,13 @@ defmodule Assent.Strategy.OAuthTest do assert oauth_params["oauth_verifier"] == callback_params["oauth_verifier"] assert {:ok, decoded_signature} = Base.decode64(URI.decode(signature)) - assert :crypto.mac(:hmac, :sha, "#{config[:consumer_secret]}&#{config[:session_params][:oauth_token_secret]}", signature_base_string) == decoded_signature + + assert :crypto.mac( + :hmac, + :sha, + "#{config[:consumer_secret]}&#{config[:session_params][:oauth_token_secret]}", + signature_base_string + ) == decoded_signature end) expect_oauth_user_request(%{email: nil}) @@ -404,11 +512,13 @@ defmodule Assent.Strategy.OAuthTest do end test "with missing `oauth_token` in token", %{config: config, token: token} do - assert OAuth.request(config, Map.delete(token, "oauth_token"), :get, "/info") == {:error, "No `oauth_token` in token map"} + assert OAuth.request(config, Map.delete(token, "oauth_token"), :get, "/info") == + {:error, "No `oauth_token` in token map"} end test "with missing `oauth_token_secret` in token", %{config: config, token: token} do - assert OAuth.request(config, Map.delete(token, "oauth_token_secret"), :get, "/info") == {:error, "No `oauth_token_secret` in token map"} + assert OAuth.request(config, Map.delete(token, "oauth_token_secret"), :get, "/info") == + {:error, "No `oauth_token_secret` in token map"} end test "with missing `:consumer_key` config", %{config: config, token: token} do @@ -429,7 +539,9 @@ defmodule Assent.Strategy.OAuthTest do info_url = TestServer.url("/info") TestServer.stop() - assert {:error, %ServerUnreachableError{} = error} = OAuth.request(config, token, :get, "/info") + assert {:error, %ServerUnreachableError{} = error} = + OAuth.request(config, token, :get, "/info") + assert Exception.message(error) =~ "The server was unreachable." assert error.http_adapter == Assent.HTTPAdapter.Httpc assert error.request_url == info_url @@ -441,7 +553,8 @@ defmodule Assent.Strategy.OAuthTest do info_url = TestServer.url("/info") expect_oauth_api_request("/info", %{"success" => true}, [], fn _conn, oauth_params -> - signature_base_string = gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", oauth_params) + signature_base_string = + gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", oauth_params) assert oauth_params["oauth_consumer_key"] == config[:consumer_key] assert oauth_params["oauth_nonce"] @@ -463,11 +576,19 @@ defmodule Assent.Strategy.OAuthTest do shared_secret = "#{config[:consumer_secret]}&#{token["oauth_token_secret"]}" info_url = TestServer.url("/info") - expect_oauth_api_request("/info", %{"success" => true}, [params: [a: 1]], fn conn, oauth_params -> - signature_base_string = gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", Map.merge(oauth_params, conn.params)) + expect_oauth_api_request("/info", %{"success" => true}, [params: [a: 1]], fn conn, + oauth_params -> + signature_base_string = + gen_signature_base_string( + "GET&#{URI.encode_www_form(info_url)}&", + Map.merge(oauth_params, conn.params) + ) assert conn.params["a"] == "1" - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) + + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) + assert :crypto.mac(:hmac, :sha, shared_secret, signature_base_string) == decoded_signature end) @@ -479,11 +600,19 @@ defmodule Assent.Strategy.OAuthTest do shared_secret = "#{config[:consumer_secret]}&#{token["oauth_token_secret"]}" info_url = TestServer.url("/info") - expect_oauth_api_request("/info", %{"success" => true}, [params: [a: 1]], fn conn, oauth_params -> - signature_base_string = gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", Map.merge(oauth_params, conn.params)) + expect_oauth_api_request("/info", %{"success" => true}, [params: [a: 1]], fn conn, + oauth_params -> + signature_base_string = + gen_signature_base_string( + "GET&#{URI.encode_www_form(info_url)}&", + Map.merge(oauth_params, conn.params) + ) assert Plug.Conn.get_req_header(conn, "b") == ["2"] - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) + + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) + assert :crypto.mac(:hmac, :sha, shared_secret, signature_base_string) == decoded_signature end) @@ -493,13 +622,16 @@ defmodule Assent.Strategy.OAuthTest do test "with uppercase url", %{config: config, token: token} do shared_secret = "#{config[:consumer_secret]}&#{token["oauth_token_secret"]}" - config = Keyword.put(config, :base_url, String.upcase(config[:base_url])) - info_url = TestServer.url("/info") + config = Keyword.put(config, :base_url, String.upcase(config[:base_url])) + info_url = TestServer.url("/info") expect_oauth_api_request("/INFO", %{"success" => true}, [], fn _conn, oauth_params -> - signature_base_string = gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", oauth_params) + signature_base_string = + gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", oauth_params) + + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) assert :crypto.mac(:hmac, :sha, shared_secret, signature_base_string) == decoded_signature end) @@ -523,11 +655,20 @@ defmodule Assent.Strategy.OAuthTest do |> List.first() |> :public_key.pem_entry_decode() - signature_base_string = gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", oauth_params) + signature_base_string = + gen_signature_base_string("GET&#{URI.encode_www_form(info_url)}&", oauth_params) assert oauth_params["oauth_signature_method"] == "RSA-SHA1" - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) - assert :public_key.verify(signature_base_string, :sha, decoded_signature, decoded_public_key) + + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) + + assert :public_key.verify( + signature_base_string, + :sha, + decoded_signature, + decoded_public_key + ) end) assert {:ok, response} = OAuth.request(config, token, :get, "/info") @@ -539,7 +680,12 @@ defmodule Assent.Strategy.OAuthTest do expect_oauth_api_request("/info", %{"success" => true}, [], fn _conn, oauth_params -> assert oauth_params["oauth_signature_method"] == "PLAINTEXT" - assert oauth_params["oauth_signature"] == URI.encode("#{config[:consumer_secret]}&#{token["oauth_token_secret"]}", &URI.char_unreserved?/1) + + assert oauth_params["oauth_signature"] == + URI.encode( + "#{config[:consumer_secret]}&#{token["oauth_token_secret"]}", + &URI.char_unreserved?/1 + ) end) assert {:ok, response} = OAuth.request(config, token, :get, "/info") @@ -552,27 +698,50 @@ defmodule Assent.Strategy.OAuthTest do shared_secret = "#{config[:consumer_secret]}&#{token["oauth_token_secret"]}" info_url = TestServer.url("/info") - expect_oauth_api_request("/info", %{"success" => true}, [], fn _conn, oauth_params -> - signature_base_string = gen_signature_base_string("POST&#{URI.encode_www_form(info_url)}&", oauth_params) + expect_oauth_api_request( + "/info", + %{"success" => true}, + [], + fn _conn, oauth_params -> + signature_base_string = + gen_signature_base_string("POST&#{URI.encode_www_form(info_url)}&", oauth_params) - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) - assert :crypto.mac(:hmac, :sha, shared_secret, signature_base_string) == decoded_signature - end, "POST") + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) + + assert :crypto.mac(:hmac, :sha, shared_secret, signature_base_string) == decoded_signature + end, + "POST" + ) assert {:ok, response} = OAuth.request(config, token, :post, "/info") assert response.body == %{"success" => true} - expect_oauth_api_request("/info", %{"success" => true}, [params: [a: 1]], fn conn, oauth_params -> - {:ok, body, _conn} = Plug.Conn.read_body(conn, []) - params = URI.decode_query(body) - signature_base_string = gen_signature_base_string("POST&#{URI.encode_www_form(info_url)}&", Map.merge(oauth_params, params)) + expect_oauth_api_request( + "/info", + %{"success" => true}, + [params: [a: 1]], + fn conn, oauth_params -> + {:ok, body, _conn} = Plug.Conn.read_body(conn, []) + params = URI.decode_query(body) - assert params == %{"a" => "1"} - assert {:ok, decoded_signature} = Base.decode64(URI.decode(oauth_params["oauth_signature"])) - assert :crypto.mac(:hmac, :sha, shared_secret, signature_base_string) == decoded_signature - end, "POST") + signature_base_string = + gen_signature_base_string( + "POST&#{URI.encode_www_form(info_url)}&", + Map.merge(oauth_params, params) + ) + + assert params == %{"a" => "1"} + + assert {:ok, decoded_signature} = + Base.decode64(URI.decode(oauth_params["oauth_signature"])) + + assert :crypto.mac(:hmac, :sha, shared_secret, signature_base_string) == decoded_signature + end, + "POST" + ) - assert {:ok, response} = OAuth.request(config, token, :post, "/info", [a: 1]) + assert {:ok, response} = OAuth.request(config, token, :post, "/info", a: 1) assert response.body == %{"success" => true} end end diff --git a/test/assent/strategies/oidc_test.exs b/test/assent/strategies/oidc_test.exs index 6a8731a..8981aff 100644 --- a/test/assent/strategies/oidc_test.exs +++ b/test/assent/strategies/oidc_test.exs @@ -1,29 +1,47 @@ defmodule Assent.Strategy.OIDCTest do use Assent.Test.OIDCTestCase - alias Assent.{JWTAdapter.AssentJWT, InvalidResponseError, RequestError, ServerUnreachableError, UnexpectedResponseError, Strategy.OIDC} + alias Assent.{ + InvalidResponseError, + JWTAdapter.AssentJWT, + RequestError, + ServerUnreachableError, + Strategy.OIDC, + UnexpectedResponseError + } describe "authorize_url/2" do test "generates url and state", %{config: config} do assert {:ok, %{url: url, session_params: %{state: state}}} = OIDC.authorize_url(config) refute is_nil(state) - assert url =~ TestServer.url("/oauth/authorize?client_id=id&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fcallback&response_type=code&scope=openid&state=#{state}") + + assert url =~ + TestServer.url( + "/oauth/authorize?client_id=id&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fcallback&response_type=code&scope=openid&state=#{state}" + ) end test "can add nonce", %{config: config} do assert {:ok, %{url: url, session_params: %{state: state, nonce: nonce}}} = - config - |> Keyword.put(:nonce, "n-0S6_WzA2Mj") - |> OIDC.authorize_url() + config + |> Keyword.put(:nonce, "n-0S6_WzA2Mj") + |> OIDC.authorize_url() assert nonce == "n-0S6_WzA2Mj" - assert url =~ TestServer.url("/oauth/authorize?client_id=id&nonce=n-0S6_WzA2Mj&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fcallback&response_type=code&scope=openid&state=#{state}") + + assert url =~ + TestServer.url( + "/oauth/authorize?client_id=id&nonce=n-0S6_WzA2Mj&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fcallback&response_type=code&scope=openid&state=#{state}" + ) end end describe "callback/2 with static OpenID configuration" do - test "with missing `token_endpoint` configuration options", %{config: config, callback_params: params} do + test "with missing `token_endpoint` configuration options", %{ + config: config, + callback_params: params + } do openid_config = config |> Keyword.get(:openid_configuration) @@ -31,44 +49,69 @@ defmodule Assent.Strategy.OIDCTest do config = Keyword.put(config, :openid_configuration, openid_config) - assert OIDC.callback(config, params) == {:error, "`token_endpoint` not found in OpenID configuration"} + assert OIDC.callback(config, params) == + {:error, "`token_endpoint` not found in OpenID configuration"} end test "with invalid id_token", %{config: config, callback_params: params} do - expect_oidc_access_token_request([id_token: "invalid"]) + expect_oidc_access_token_request(id_token: "invalid") assert OIDC.callback(config, params) == {:error, "The ID Token is not a valid JWT"} end - @user_claims %{sub: "1", name: "Dan Schultzer", email: "foo@example.com", email_verified: true, "http://localhost:4000/additional": "info"} - @user %{"email" => "foo@example.com", "name" => "Dan Schultzer", "sub" => "1", "email_verified" => true, "http://localhost:4000/additional" => "info"} + @user_claims %{ + sub: "1", + name: "Dan Schultzer", + email: "foo@example.com", + email_verified: true, + "http://localhost:4000/additional": "info" + } + @user %{ + "email" => "foo@example.com", + "name" => "Dan Schultzer", + "sub" => "1", + "email_verified" => true, + "http://localhost:4000/additional" => "info" + } test "with invalid authentication method", %{config: config, callback_params: params} do config = Keyword.put(config, :client_authentication_method, "invalid") - assert OIDC.callback(config, params) == {:error, "Invalid client authentication method: invalid"} + assert OIDC.callback(config, params) == + {:error, "Invalid client authentication method: invalid"} end test "with unsupported authentication method", %{config: config, callback_params: params} do openid_configuration = - Map.put(config[:openid_configuration], "token_endpoint_auth_methods_supported", ["private_key_jwt"]) + Map.put(config[:openid_configuration], "token_endpoint_auth_methods_supported", [ + "private_key_jwt" + ]) config = config |> Keyword.put(:client_authentication_method, "client_secret_basic") |> Keyword.put(:openid_configuration, openid_configuration) - assert OIDC.callback(config, params) == {:error, "Unsupported client authentication method: client_secret_basic"} + assert OIDC.callback(config, params) == + {:error, "Unsupported client authentication method: client_secret_basic"} end - test "with `client_secret_basic` authentication method", %{config: config, callback_params: params} do - expect_oidc_access_token_request([id_token_opts: [claims: @user_claims, iss: "http://localhost"]], fn conn, _params -> - assert ["Basic " <> token] = Plug.Conn.get_req_header(conn, "authorization") - assert [client_id, client_secret] = String.split(Base.url_decode64!(token, padding: false), ":") + test "with `client_secret_basic` authentication method", %{ + config: config, + callback_params: params + } do + expect_oidc_access_token_request( + [id_token_opts: [claims: @user_claims, iss: "http://localhost"]], + fn conn, _params -> + assert ["Basic " <> token] = Plug.Conn.get_req_header(conn, "authorization") - assert client_id == config[:client_id] - assert client_secret == config[:client_secret] - end) + assert [client_id, client_secret] = + String.split(Base.url_decode64!(token, padding: false), ":") + + assert client_id == config[:client_id] + assert client_secret == config[:client_secret] + end + ) expect_oidc_jwks_uri_request(count: 1) @@ -78,49 +121,54 @@ defmodule Assent.Strategy.OIDCTest do end @private_rsa_key """ - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw - kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr - m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi - NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV - 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 - QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs - kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go - amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM - +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 - D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC - 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y - lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ - hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp - bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X - +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B - BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC - 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx - QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz - 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 - Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 - NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j - 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma - 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K - y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB - jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= - -----END RSA PRIVATE KEY----- - """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw + kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr + m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi + NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV + 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 + QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs + kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go + amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM + +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 + D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC + 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y + lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ + hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp + bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X + +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B + BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC + 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx + QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz + 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 + Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 + NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j + 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma + 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K + y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB + jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= + -----END RSA PRIVATE KEY----- + """ @public_rsa_key """ - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv - vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc - aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy - tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 - e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb - V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 - MwIDAQAB - -----END PUBLIC KEY----- - """ - - test "with `private_key_jwt` authentication method", %{config: config, callback_params: params} do + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv + vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc + aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy + tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 + e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb + V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 + MwIDAQAB + -----END PUBLIC KEY----- + """ + + test "with `private_key_jwt` authentication method", %{ + config: config, + callback_params: params + } do openid_configuration = - Map.put(config[:openid_configuration], "token_endpoint_auth_methods_supported", ["private_key_jwt"]) + Map.put(config[:openid_configuration], "token_endpoint_auth_methods_supported", [ + "private_key_jwt" + ]) config = config @@ -131,16 +179,23 @@ defmodule Assent.Strategy.OIDCTest do url = TestServer.url() - expect_oidc_access_token_request([id_token_opts: [claims: @user_claims, iss: "http://localhost"]], fn _conn, params -> - assert {:ok, jwt} = AssentJWT.verify(params["client_assertion"], @public_rsa_key, json_library: Jason) - assert jwt.header["alg"] == "RS256" - assert jwt.header["typ"] == "JWT" - assert jwt.header["kid"] == "key_id" - assert jwt.claims["iss"] == "id" - assert jwt.claims["sub"] == "id" - assert jwt.claims["aud"] == url - assert jwt.claims["exp"] > DateTime.to_unix(DateTime.utc_now()) - end) + expect_oidc_access_token_request( + [id_token_opts: [claims: @user_claims, iss: "http://localhost"]], + fn _conn, params -> + assert {:ok, jwt} = + AssentJWT.verify(params["client_assertion"], @public_rsa_key, + json_library: Jason + ) + + assert jwt.header["alg"] == "RS256" + assert jwt.header["typ"] == "JWT" + assert jwt.header["kid"] == "key_id" + assert jwt.claims["iss"] == "id" + assert jwt.claims["sub"] == "id" + assert jwt.claims["aud"] == url + assert jwt.claims["exp"] > DateTime.to_unix(DateTime.utc_now()) + end + ) expect_oidc_jwks_uri_request(count: 1) @@ -174,20 +229,32 @@ defmodule Assent.Strategy.OIDCTest do assert {:failed_connect, _} = error.reason end - test "with unexpected openid config url response", %{config: config, openid_config: openid_config, callback_params: params} do + test "with unexpected openid config url response", %{ + config: config, + openid_config: openid_config, + callback_params: params + } do expect_openid_config_request(openid_config, status_code: 201) assert {:error, %UnexpectedResponseError{}} = OIDC.callback(config, params) end - test "with 404 openid config url", %{config: config, openid_config: openid_config, callback_params: params} do + test "with 404 openid config url", %{ + config: config, + openid_config: openid_config, + callback_params: params + } do expect_openid_config_request(openid_config, status_code: 404) assert {:error, %InvalidResponseError{} = error} = OIDC.callback(config, params) assert error.response.status == 404 end - test "with invalid id_token", %{config: config, openid_config: openid_config, callback_params: params} do + test "with invalid id_token", %{ + config: config, + openid_config: openid_config, + callback_params: params + } do expect_openid_config_request(openid_config) expect_oidc_access_token_request(uri: "/dynamic/token/path", id_token: "invalid") @@ -195,15 +262,28 @@ defmodule Assent.Strategy.OIDCTest do assert OIDC.callback(config, params) == {:error, "The ID Token is not a valid JWT"} end - test "with missing id_token", %{config: config, openid_config: openid_config, callback_params: params} do + test "with missing id_token", %{ + config: config, + openid_config: openid_config, + callback_params: params + } do expect_openid_config_request(openid_config) - expect_oidc_access_token_request(uri: "/dynamic/token/path", params: %{access_token: "access_token", renewal_token: "renewal_token"}) + expect_oidc_access_token_request( + uri: "/dynamic/token/path", + params: %{access_token: "access_token", renewal_token: "renewal_token"} + ) - assert OIDC.callback(config, params) == {:error, "The `id_token` key not found in token params, only found these keys: access_token, renewal_token"} + assert OIDC.callback(config, params) == + {:error, + "The `id_token` key not found in token params, only found these keys: access_token, renewal_token"} end - test "with valid id_token", %{config: config, openid_config: openid_config, callback_params: params} do + test "with valid id_token", %{ + config: config, + openid_config: openid_config, + callback_params: params + } do expect_openid_config_request(openid_config) expect_oidc_access_token_request(uri: "/dynamic/token/path") @@ -219,20 +299,21 @@ defmodule Assent.Strategy.OIDCTest do setup %{config: config} do JOSE.unsecured_signing(true) id_token = gen_id_token(alg: "none") - config = Keyword.put(config, :id_token_signed_response_alg, "none") + config = Keyword.put(config, :id_token_signed_response_alg, "none") {:ok, id_token: id_token, config: config} end test "fails", %{config: config, id_token: id_token} do - assert OIDC.validate_id_token(config, id_token) == {:error, "Invalid JWT signature for ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Invalid JWT signature for ID Token"} end end describe "validate_id_token/2 with `alg=HS256`" do setup %{config: config} do id_token = gen_id_token(alg: "HS256") - config = Keyword.put(config, :id_token_signed_response_alg, "HS256") + config = Keyword.put(config, :id_token_signed_response_alg, "HS256") {:ok, id_token: id_token, config: config} end @@ -248,25 +329,31 @@ defmodule Assent.Strategy.OIDCTest do test "with no `:client_id`", %{config: config, id_token: id_token} do config = Keyword.delete(config, :client_id) - assert {:error, %Assent.Config.MissingKeyError{} = error} = OIDC.validate_id_token(config, id_token) + assert {:error, %Assent.Config.MissingKeyError{} = error} = + OIDC.validate_id_token(config, id_token) + assert error.key == :client_id end test "with missing `issuer` in OpenID configuration", %{config: config, id_token: id_token} do openid_config = Map.delete(Keyword.get(config, :openid_configuration), "issuer") - config = Keyword.put(config, :openid_configuration, openid_config) + config = Keyword.put(config, :openid_configuration, openid_config) - assert OIDC.validate_id_token(config, id_token) == {:error, "`issuer` not found in OpenID configuration"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "`issuer` not found in OpenID configuration"} end test "with invalid id_token", %{config: config} do - assert OIDC.validate_id_token(config, "invalid") == {:error, "The ID Token is not a valid JWT"} + assert OIDC.validate_id_token(config, "invalid") == + {:error, "The ID Token is not a valid JWT"} end test "with no `:client_secret`", %{config: config, id_token: id_token} do config = Keyword.delete(config, :client_secret) - assert {:error, %Assent.Config.MissingKeyError{} = error} = OIDC.validate_id_token(config, id_token) + assert {:error, %Assent.Config.MissingKeyError{} = error} = + OIDC.validate_id_token(config, id_token) + assert error.key == :client_secret end @@ -274,35 +361,43 @@ defmodule Assent.Strategy.OIDCTest do test "with missing required #{key} keys in id_token", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{unquote(key) => nil}) - assert OIDC.validate_id_token(config, id_token) == {:error, "Missing `#{unquote(key)}` in ID Token claims"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Missing `#{unquote(key)}` in ID Token claims"} end end test "with invalid `issuer` in id_token", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"iss" => "invalid"}) - assert OIDC.validate_id_token(config, id_token) == {:error, "Invalid issuer \"invalid\" in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Invalid issuer \"invalid\" in ID Token"} end test "with unexpected `alg`", %{config: config, id_token: id_token} do - assert OIDC.validate_id_token(Keyword.delete(config, :id_token_signed_response_alg), id_token) == {:error, "Expected `alg` in ID Token to be \"RS256\", got \"HS256\""} + assert OIDC.validate_id_token( + Keyword.delete(config, :id_token_signed_response_alg), + id_token + ) == {:error, "Expected `alg` in ID Token to be \"RS256\", got \"HS256\""} JOSE.unsecured_signing(true) id_token = gen_id_token(alg: "none") - assert OIDC.validate_id_token(config, id_token) == {:error, "Expected `alg` in ID Token to be \"HS256\", got \"none\""} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Expected `alg` in ID Token to be \"HS256\", got \"none\""} end test "with invalid `aud` in id_token", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"aud" => "invalid"}) - assert OIDC.validate_id_token(config, id_token) == {:error, "`:client_id` not in audience [\"invalid\"] in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "`:client_id` not in audience [\"invalid\"] in ID Token"} end test "with case insensitive `aud` in id_token", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"aud" => "ID"}) - assert OIDC.validate_id_token(config, id_token) == {:error, "`:client_id` not in audience [\"ID\"] in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "`:client_id` not in audience [\"ID\"] in ID Token"} end test "with `aud` list with only client id in id_token", %{config: config} do @@ -314,21 +409,24 @@ defmodule Assent.Strategy.OIDCTest do test "with `aud` list in id_token no trusted_audiences config", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"aud" => ~w(id aud1 aud2)}) - assert OIDC.validate_id_token(config, id_token) == {:error, "Untrusted audience(s) [\"aud1\", \"aud2\"] in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Untrusted audience(s) [\"aud1\", \"aud2\"] in ID Token"} end test "with `aud` list with missing client id in id_token", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"aud" => ~w(aud1 aud2)}) config = Keyword.put(config, :trusted_audiences, ~w(aud1 aud2)) - assert OIDC.validate_id_token(config, id_token) == {:error, "`:client_id` not in audience [\"aud1\", \"aud2\"] in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "`:client_id` not in audience [\"aud1\", \"aud2\"] in ID Token"} end test "with `aud` list with untrusted audiences in id_token", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"aud" => ~w(id aud1 aud2)}) config = Keyword.put(config, :trusted_audiences, ~w(aud1)) - assert OIDC.validate_id_token(config, id_token) == {:error, "Untrusted audience(s) [\"aud2\"] in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Untrusted audience(s) [\"aud2\"] in ID Token"} end test "with `aud` list in id_token", %{config: config} do @@ -341,7 +439,8 @@ defmodule Assent.Strategy.OIDCTest do test "with invalid `azp`", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"azp" => "invalid"}) - assert OIDC.validate_id_token(config, id_token) == {:error, "Invalid authorized party \"invalid\" in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Invalid authorized party \"invalid\" in ID Token"} end test "with valid `azp`", %{config: config} do @@ -352,9 +451,10 @@ defmodule Assent.Strategy.OIDCTest do test "with invalid signature in id_token", %{config: config, id_token: id_token} do [header, payload, _signature] = String.split(id_token, ".") - id_token = "#{header}.#{payload}.invalid" + id_token = "#{header}.#{payload}.invalid" - assert OIDC.validate_id_token(config, id_token) == {:error, "Invalid JWT signature for ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Invalid JWT signature for ID Token"} end test "with expired id_token", %{config: config} do @@ -364,16 +464,19 @@ defmodule Assent.Strategy.OIDCTest do end test "with TTL reached for id_token", %{config: config} do - config = Keyword.put(config, :id_token_ttl_seconds, 60) + config = Keyword.put(config, :id_token_ttl_seconds, 60) id_token = gen_id_token(alg: "HS256", claims: %{"iat" => :os.system_time(:second) - 60}) - assert OIDC.validate_id_token(config, id_token) == {:error, "The ID Token was issued too long ago"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "The ID Token was issued too long ago"} end test "with missing `:session_params` config", %{config: config, id_token: id_token} do config = Keyword.delete(config, :session_params) - assert {:error, %Assent.Config.MissingKeyError{} = error} = OIDC.validate_id_token(config, id_token) + assert {:error, %Assent.Config.MissingKeyError{} = error} = + OIDC.validate_id_token(config, id_token) + assert error.key == :session_params end @@ -386,24 +489,32 @@ defmodule Assent.Strategy.OIDCTest do test "with unexpected `nonce` in id_token", %{config: config} do id_token = gen_id_token(alg: "HS256", claims: %{"nonce" => "a"}) - assert OIDC.validate_id_token(config, id_token) == {:error, "`nonce` included in ID Token but doesn't exist in session params"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "`nonce` included in ID Token but doesn't exist in session params"} end test "with missing `nonce` in id_token", %{config: config, id_token: id_token} do - config = Keyword.put(config, :session_params, Map.put(config[:session_params], :nonce, "n-0S6_WzA2Mj")) + config = + Keyword.put( + config, + :session_params, + Map.put(config[:session_params], :nonce, "n-0S6_WzA2Mj") + ) - assert OIDC.validate_id_token(config, id_token) == {:error, "`nonce` is not included in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "`nonce` is not included in ID Token"} end test "with invalid `nonce` in id_token", %{config: config} do - config = Keyword.put(config, :session_params, Map.put(config[:session_params], :nonce, "b")) + config = Keyword.put(config, :session_params, Map.put(config[:session_params], :nonce, "b")) id_token = gen_id_token(alg: "HS256", claims: %{"nonce" => "a"}) - assert OIDC.validate_id_token(config, id_token) == {:error, "Invalid `nonce` included in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "Invalid `nonce` included in ID Token"} end test "with valid nonce in id_token", %{config: config} do - config = Keyword.put(config, :session_params, Map.put(config[:session_params], :nonce, "a")) + config = Keyword.put(config, :session_params, Map.put(config[:session_params], :nonce, "a")) id_token = gen_id_token(alg: "HS256", claims: %{"nonce" => "a"}) assert {:ok, jwt} = OIDC.validate_id_token(config, id_token) @@ -423,14 +534,17 @@ defmodule Assent.Strategy.OIDCTest do openid_config = Map.delete(Keyword.get(config, :openid_configuration), "jwks_uri") config = Keyword.put(config, :openid_configuration, openid_config) - assert OIDC.validate_id_token(config, id_token) == {:error, "`jwks_uri` not found in OpenID configuration"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "`jwks_uri` not found in OpenID configuration"} end - test "with unreachable `jwk_uri` url", %{config: config, id_token: id_token} do + test "with unreachable `jwk_uri` url", %{config: config, id_token: id_token} do jwks_uri_url = TestServer.url("/jwks_uri.json") TestServer.stop() - assert {:error, %ServerUnreachableError{} = error} = OIDC.validate_id_token(config, id_token) + assert {:error, %ServerUnreachableError{} = error} = + OIDC.validate_id_token(config, id_token) + assert error.http_adapter == Assent.HTTPAdapter.Httpc assert error.request_url == jwks_uri_url assert {:failed_connect, _} = error.reason @@ -439,36 +553,53 @@ defmodule Assent.Strategy.OIDCTest do test "with unexpected `jwk_uri` url response", %{config: config, id_token: id_token} do expect_oidc_jwks_uri_request(status_code: 201) - assert {:error, %UnexpectedResponseError{} = error} = OIDC.validate_id_token(config, id_token) + assert {:error, %UnexpectedResponseError{} = error} = + OIDC.validate_id_token(config, id_token) + assert error.response.status == 201 end test "with 404 `jwks_uri` url", %{config: config, id_token: id_token} do - TestServer.add("/jwks_uri.json", via: :get, to: fn conn -> - Plug.Conn.send_resp(conn, 404, "") - end) + TestServer.add("/jwks_uri.json", + via: :get, + to: fn conn -> + Plug.Conn.send_resp(conn, 404, "") + end + ) assert {:error, %InvalidResponseError{} = error} = OIDC.validate_id_token(config, id_token) assert error.response.status == 404 end test "with missing keys in `jwks_uri` url", %{config: config, id_token: id_token} do - TestServer.add("/jwks_uri.json", via: :get, to: fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.send_resp(200, "{}") - end) - - assert OIDC.validate_id_token(config, id_token) == {:error, "No keys found in `jwks_uri` provider configuration"} - end - - test "with no `kid` in header and multiple keys fetched from `jwks_uri` url", %{config: config, id_token: id_token} do + TestServer.add("/jwks_uri.json", + via: :get, + to: fn conn -> + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp(200, "{}") + end + ) + + assert OIDC.validate_id_token(config, id_token) == + {:error, "No keys found in `jwks_uri` provider configuration"} + end + + test "with no `kid` in header and multiple keys fetched from `jwks_uri` url", %{ + config: config, + id_token: id_token + } do expect_oidc_jwks_uri_request() - assert OIDC.validate_id_token(config, id_token) == {:error, "Multiple public keys found in provider configuration and no `kid` value in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, + "Multiple public keys found in provider configuration and no `kid` value in ID Token"} end - test "with no `kid` in header and single key fetched from `jwks_uri` url", %{config: config, id_token: id_token} do + test "with no `kid` in header and single key fetched from `jwks_uri` url", %{ + config: config, + id_token: id_token + } do expect_oidc_jwks_uri_request(count: 1) assert {:ok, jwt} = OIDC.validate_id_token(config, id_token) @@ -481,7 +612,8 @@ defmodule Assent.Strategy.OIDCTest do expect_oidc_jwks_uri_request() - assert OIDC.validate_id_token(config, id_token) == {:error, "No keys found for the `kid` value \"invalid\" provided in ID Token"} + assert OIDC.validate_id_token(config, id_token) == + {:error, "No keys found for the `kid` value \"invalid\" provided in ID Token"} end test "with matching `kid` in keys fetched from `jwks_uri` url", %{config: config} do @@ -497,8 +629,8 @@ defmodule Assent.Strategy.OIDCTest do describe "fetch_userinfo/2" do setup %{config: config} do - id_token = gen_id_token(alg: "HS256") - config = Keyword.put(config, :id_token_signed_response_alg, "HS256") + id_token = gen_id_token(alg: "HS256") + config = Keyword.put(config, :id_token_signed_response_alg, "HS256") access_token = %{"access_token" => "access_token", "id_token" => id_token} {:ok, config: config, access_token: access_token} @@ -511,24 +643,39 @@ defmodule Assent.Strategy.OIDCTest do assert {:error, %InvalidResponseError{}} = OIDC.fetch_userinfo(config, access_token) end - test "with missing `userinfo_endpoint` in OpenID configuration", %{config: config, access_token: access_token} do + test "with missing `userinfo_endpoint` in OpenID configuration", %{ + config: config, + access_token: access_token + } do openid_configuration = Map.delete(config[:openid_configuration], "userinfo_endpoint") - config = Keyword.put(config, :openid_configuration, openid_configuration) + config = Keyword.put(config, :openid_configuration, openid_configuration) - assert OIDC.fetch_userinfo(config, access_token) == {:error, "`userinfo_endpoint` not found in OpenID configuration"} + assert OIDC.fetch_userinfo(config, access_token) == + {:error, "`userinfo_endpoint` not found in OpenID configuration"} end test "with unreachable `userinfo_endpoint`", %{config: config, access_token: access_token} do - openid_configuration = Map.put(config[:openid_configuration], "userinfo_endpoint", "http://localhost:8888/userinfo") - config = Keyword.put(config, :openid_configuration, openid_configuration) + openid_configuration = + Map.put( + config[:openid_configuration], + "userinfo_endpoint", + "http://localhost:8888/userinfo" + ) + + config = Keyword.put(config, :openid_configuration, openid_configuration) + + assert {:error, %ServerUnreachableError{} = error} = + OIDC.fetch_userinfo(config, access_token) - assert {:error, %ServerUnreachableError{} = error} = OIDC.fetch_userinfo(config, access_token) assert error.http_adapter == Assent.HTTPAdapter.Httpc assert error.request_url == "http://localhost:8888/userinfo" assert {:failed_connect, _} = error.reason end - test "with unexpected `userinfo_endpoint` url response", %{config: config, access_token: access_token} do + test "with unexpected `userinfo_endpoint` url response", %{ + config: config, + access_token: access_token + } do expect_oidc_userinfo_request(gen_id_token(alg: "HS256"), status_code: 201) assert {:error, %UnexpectedResponseError{}} = OIDC.fetch_userinfo(config, access_token) @@ -546,7 +693,8 @@ defmodule Assent.Strategy.OIDCTest do [header, payload, _signature] = String.split(gen_id_token(alg: "HS256"), ".") expect_oidc_userinfo_request("#{header}.#{payload}.invalid") - assert OIDC.fetch_userinfo(config, access_token) == {:error, "Invalid JWT signature for ID Token"} + assert OIDC.fetch_userinfo(config, access_token) == + {:error, "Invalid JWT signature for ID Token"} end test "with jwt response", %{config: config, access_token: access_token} do @@ -558,13 +706,15 @@ defmodule Assent.Strategy.OIDCTest do test "with missing `sub` in userinfo claims", %{config: config, access_token: access_token} do expect_oidc_userinfo_request(Map.delete(@user_claims, :sub)) - assert OIDC.fetch_userinfo(config, access_token) == {:error, "`sub` not in userinfo response"} + assert OIDC.fetch_userinfo(config, access_token) == + {:error, "`sub` not in userinfo response"} end test "with different `sub` in userinfo claims", %{config: config, access_token: access_token} do expect_oidc_userinfo_request(Map.put(@user_claims, :sub, "2")) - assert OIDC.fetch_userinfo(config, access_token) == {:error, "`sub` in userinfo response not the same as in ID Token"} + assert OIDC.fetch_userinfo(config, access_token) == + {:error, "`sub` in userinfo response not the same as in ID Token"} end end end diff --git a/test/assent/strategies/slack_test.exs b/test/assent/strategies/slack_test.exs index 3110369..e348c3f 100644 --- a/test/assent/strategies/slack_test.exs +++ b/test/assent/strategies/slack_test.exs @@ -61,7 +61,8 @@ defmodule Assent.Strategy.SlackTest do } setup %{config: config} do - openid_configuration = Map.merge(@openid_config, Map.delete(config[:openid_configuration], "issuer")) + openid_configuration = + Map.merge(@openid_config, Map.delete(config[:openid_configuration], "issuer")) config = Keyword.put(config, :openid_configuration, openid_configuration) diff --git a/test/assent/strategies/stripe_test.exs b/test/assent/strategies/stripe_test.exs index 9ef2b98..d89f7ae 100644 --- a/test/assent/strategies/stripe_test.exs +++ b/test/assent/strategies/stripe_test.exs @@ -100,7 +100,7 @@ defmodule Assent.Strategy.StripeTest do "type" => "custom" } - @user %{ + @user %{ "email" => "site@stripe.com", "sub" => "acct_1032D82eZvKYlo2C" } @@ -120,6 +120,7 @@ defmodule Assent.Strategy.StripeTest do expect_oauth2_access_token_request([], fn _conn, params -> assert params["client_secret"] == config[:client_secret] end) + expect_oauth2_user_request(@user_response, uri: "/v1/account") assert {:ok, %{user: user}} = Stripe.callback(config, params) diff --git a/test/assent/strategies/twitter_test.exs b/test/assent/strategies/twitter_test.exs index efb6419..1d4841e 100644 --- a/test/assent/strategies/twitter_test.exs +++ b/test/assent/strategies/twitter_test.exs @@ -10,7 +10,8 @@ defmodule Assent.Strategy.TwitterTest do "name" => "Twitter Dev", "screen_name" => "TwitterDev", "location" => "Internet", - "description" => "Your official source for Twitter Platform news, updates & events. Need technical help? Visit https://t.co/mGHnxZU8c1 ⌨️ #TapIntoTwitter", + "description" => + "Your official source for Twitter Platform news, updates & events. Need technical help? Visit https://t.co/mGHnxZU8c1 ⌨️ #TapIntoTwitter", "url" => "https://t.co/FGl7VOULyL", "entities" => %{ "url" => %{ @@ -56,7 +57,8 @@ defmodule Assent.Strategy.TwitterTest do "created_at" => "Tue May 14 17:54:29 +0000 2019", "id" => 1_128_357_932_238_823_424, "id_str" => "1128357932238823424", - "text" => "We’ll release the first Labs endpoints to all eligible developers in the coming weeks. If you want to participate,… https://t.co/8q8sj87D5a", + "text" => + "We’ll release the first Labs endpoints to all eligible developers in the coming weeks. If you want to participate,… https://t.co/8q8sj87D5a", "truncated" => true, "entities" => %{ "hashtags" => [], @@ -100,7 +102,8 @@ defmodule Assent.Strategy.TwitterTest do "profile_background_image_url_https" => "null", "profile_background_tile" => nil, "profile_image_url" => "null", - "profile_image_url_https" => "https://pbs.twimg.com/profile_images/880136122604507136/xHrnqf1T_normal.jpg", + "profile_image_url_https" => + "https://pbs.twimg.com/profile_images/880136122604507136/xHrnqf1T_normal.jpg", "profile_banner_url" => "https://pbs.twimg.com/profile_banners/2244994945/1498675817", "profile_link_color" => "null", "profile_sidebar_border_color" => "null", @@ -127,34 +130,66 @@ defmodule Assent.Strategy.TwitterTest do setup %{config: config, callback_params: callback_params} do config = Keyword.merge(config, consumer_key: "cChZNFj6T5R0TigYB9yd1w") - callback_params = Map.merge(callback_params, %{"oauth_token" => "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0", "oauth_verifier" => "uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY"}) + + callback_params = + Map.merge(callback_params, %{ + "oauth_token" => "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0", + "oauth_verifier" => "uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY" + }) {:ok, config: config, callback_params: callback_params} end test "authorize_url/2", %{config: config} do - expect_oauth_request_token_request(uri: "/oauth/request_token", params: %{oauth_token: "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0", oauth_token_secret: "veNRnAWe6inFuo8o2u8SLLZLjolYDmDP7SzL0YfYI", oauth_callback_confirmed: true}) + expect_oauth_request_token_request( + uri: "/oauth/request_token", + params: %{ + oauth_token: "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0", + oauth_token_secret: "veNRnAWe6inFuo8o2u8SLLZLjolYDmDP7SzL0YfYI", + oauth_callback_confirmed: true + } + ) + + assert {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} = + Twitter.authorize_url(config) + + assert url == + TestServer.url( + "/oauth/authenticate?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0" + ) - assert {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} = Twitter.authorize_url(config) - assert url == TestServer.url("/oauth/authenticate?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0") refute is_nil(oauth_token_secret) end test "callback/2", %{config: config, callback_params: params} do - expect_oauth_access_token_request([uri: "/oauth/access_token", params: %{oauth_token: "7588892-kagSNqWge8gB1WwE3plnFsJHAZVfxWD7Vb57p0b4", oauth_token_secret: "PbKfYqSryyeKDWz4ebtY3o5ogNLG11WJuZBc9fQrQo"}], fn _conn, oauth_params -> - assert oauth_params["oauth_consumer_key"] == "cChZNFj6T5R0TigYB9yd1w" - assert oauth_params["oauth_token"] == "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0" - assert oauth_params["oauth_verifier"] == "uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY" - end) + expect_oauth_access_token_request( + [ + uri: "/oauth/access_token", + params: %{ + oauth_token: "7588892-kagSNqWge8gB1WwE3plnFsJHAZVfxWD7Vb57p0b4", + oauth_token_secret: "PbKfYqSryyeKDWz4ebtY3o5ogNLG11WJuZBc9fQrQo" + } + ], + fn _conn, oauth_params -> + assert oauth_params["oauth_consumer_key"] == "cChZNFj6T5R0TigYB9yd1w" + assert oauth_params["oauth_token"] == "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0" + assert oauth_params["oauth_verifier"] == "uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY" + end + ) - expect_oauth_user_request(@user_response, uri: "/1.1/account/verify_credentials.json", params: [include_entities: false, skip_status: true, include_email: true]) + expect_oauth_user_request(@user_response, + uri: "/1.1/account/verify_credentials.json", + params: [include_entities: false, skip_status: true, include_email: true] + ) assert {:ok, %{user: user}} = Twitter.callback(config, params) assert user == @user end test "callback/2 when user denies", %{config: config, callback_params: params} do - assert {:error, %CallbackError{} = error} = Twitter.callback(config, %{"denied" => params["oauth_token"]}) + assert {:error, %CallbackError{} = error} = + Twitter.callback(config, %{"denied" => params["oauth_token"]}) + assert error.message == "The user denied the authorization request" refute error.error refute error.error_uri diff --git a/test/assent/strategies/vk_test.exs b/test/assent/strategies/vk_test.exs index 17c0864..e6933d2 100644 --- a/test/assent/strategies/vk_test.exs +++ b/test/assent/strategies/vk_test.exs @@ -38,14 +38,18 @@ defmodule Assent.Strategy.VKTest do test "normalizes data", %{config: config, callback_params: params} do expect_oauth2_access_token_request(uri: "/access_token", params: @token_response) - expect_oauth2_user_request(%{"response" => @users_response}, [uri: "/method/users.get"], fn conn -> - conn = Plug.Conn.fetch_query_params(conn) + expect_oauth2_user_request( + %{"response" => @users_response}, + [uri: "/method/users.get"], + fn conn -> + conn = Plug.Conn.fetch_query_params(conn) - assert conn.params["access_token"] == "access_token" - assert conn.params["fields"] == "uid,first_name,last_name,photo_200,screen_name" - assert conn.params["v"] == "5.69" - assert conn.params["access_token"] == "access_token" - end) + assert conn.params["access_token"] == "access_token" + assert conn.params["fields"] == "uid,first_name,last_name,photo_200,screen_name" + assert conn.params["v"] == "5.69" + assert conn.params["access_token"] == "access_token" + end + ) assert {:ok, %{user: user}} = VK.callback(config, params) assert user == @user @@ -53,7 +57,7 @@ defmodule Assent.Strategy.VKTest do test "handles invalid user response", %{config: config, callback_params: params} do expect_oauth2_access_token_request(uri: "/access_token", params: @token_response) - expect_oauth2_user_request(%{"a" => 1}, [uri: "/method/users.get"]) + expect_oauth2_user_request(%{"a" => 1}, uri: "/method/users.get") assert {:error, %RuntimeError{} = error} = VK.callback(config, params) assert error.message =~ "Retrieved an invalid response fetching VK user" diff --git a/test/assent/strategy_test.exs b/test/assent/strategy_test.exs index 35a2150..cd5f016 100644 --- a/test/assent/strategy_test.exs +++ b/test/assent/strategy_test.exs @@ -9,21 +9,33 @@ defmodule Assent.StrategyTest do headers = [{"content-type", "application/json"}] body = Jason.encode!(expected) - assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == {:ok, %{body: expected, headers: headers}} - assert Strategy.decode_response({:error, %{body: body, headers: headers}}, []) == {:error, %{body: expected, headers: headers}} + + assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == + {:ok, %{body: expected, headers: headers}} + + assert Strategy.decode_response({:error, %{body: body, headers: headers}}, []) == + {:error, %{body: expected, headers: headers}} headers = [{"content-type", "application/json; charset=utf-8"}] - assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == {:ok, %{body: expected, headers: headers}} + + assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == + {:ok, %{body: expected, headers: headers}} headers = [{"content-type", "text/javascript"}] - assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == {:ok, %{body: expected, headers: headers}} + + assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == + {:ok, %{body: expected, headers: headers}} headers = [{"content-type", "application/x-www-form-urlencoded"}] body = URI.encode_query(expected) - assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == {:ok, %{body: expected, headers: headers}} + + assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == + {:ok, %{body: expected, headers: headers}} headers = [{"content-type", "application/x-www-form-urlencoded; charset=utf-8"}] - assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == {:ok, %{body: expected, headers: headers}} + + assert Strategy.decode_response({:ok, %{body: body, headers: headers}}, []) == + {:ok, %{body: expected, headers: headers}} assert Strategy.decode_response({:error, "error reason"}, []) == {:error, "error reason"} end @@ -47,19 +59,35 @@ defmodule Assent.StrategyTest do end def request(:get, "json-encoded-body", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: Jason.encode!(%{"a" => 1})}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/json"}], + body: Jason.encode!(%{"a" => 1}) + }} end def request(:get, "json-encoded-body-already-decoded", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: %{"a" => 1}}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/json"}], + body: %{"a" => 1} + }} end def request(:get, "json-encoded-body-text/javascript-header", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "text/javascript"}], body: Jason.encode!(%{"a" => 1})}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "text/javascript"}], + body: Jason.encode!(%{"a" => 1}) + }} end def request(:get, "invalid-json-body", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: "%"}} + {:ok, + %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: "%"}} end def request(:get, "json-no-headers", nil, [], nil) do @@ -67,41 +95,115 @@ defmodule Assent.StrategyTest do end def request(:get, "form-data-body", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: URI.encode_query(%{"a" => 1})}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/x-www-form-urlencoded"}], + body: URI.encode_query(%{"a" => 1}) + }} end def request(:get, "form-data-body-already-decoded", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: %{"a" => 1}}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/x-www-form-urlencoded"}], + body: %{"a" => 1} + }} end end test "request/5" do assert Strategy.request(:get, "http-adapter", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [], body: nil, http_adapter: HTTPMock, request_url: "http-adapter"}} - - assert Strategy.request(:get, "http-adapter-with-opts", nil, [], http_adapter: {HTTPMock, a: 1}) == - {:ok, %HTTPResponse{status: 200, headers: [], body: [a: 1], http_adapter: HTTPMock, request_url: "http-adapter-with-opts"}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [], + body: nil, + http_adapter: HTTPMock, + request_url: "http-adapter" + }} + + assert Strategy.request(:get, "http-adapter-with-opts", nil, [], + http_adapter: {HTTPMock, a: 1} + ) == + {:ok, + %HTTPResponse{ + status: 200, + headers: [], + body: [a: 1], + http_adapter: HTTPMock, + request_url: "http-adapter-with-opts" + }} assert Strategy.request(:get, "json-encoded-body", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "json-encoded-body"}} - - assert Strategy.request(:get, "json-encoded-body-already-decoded", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "json-encoded-body-already-decoded"}} - - assert Strategy.request(:get, "json-encoded-body-text/javascript-header", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "text/javascript"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "json-encoded-body-text/javascript-header"}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/json"}], + body: %{"a" => 1}, + http_adapter: HTTPMock, + request_url: "json-encoded-body" + }} + + assert Strategy.request(:get, "json-encoded-body-already-decoded", nil, [], + http_adapter: HTTPMock + ) == + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/json"}], + body: %{"a" => 1}, + http_adapter: HTTPMock, + request_url: "json-encoded-body-already-decoded" + }} + + assert Strategy.request(:get, "json-encoded-body-text/javascript-header", nil, [], + http_adapter: HTTPMock + ) == + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "text/javascript"}], + body: %{"a" => 1}, + http_adapter: HTTPMock, + request_url: "json-encoded-body-text/javascript-header" + }} assert {:error, %Jason.DecodeError{}} = - Strategy.request(:get, "invalid-json-body", nil, [], http_adapter: HTTPMock) + Strategy.request(:get, "invalid-json-body", nil, [], http_adapter: HTTPMock) assert Strategy.request(:get, "json-no-headers", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [], body: Jason.encode!(%{"a" => 1}), http_adapter: HTTPMock, request_url: "json-no-headers"}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [], + body: Jason.encode!(%{"a" => 1}), + http_adapter: HTTPMock, + request_url: "json-no-headers" + }} assert Strategy.request(:get, "form-data-body", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: %{"a" => "1"}, http_adapter: HTTPMock, request_url: "form-data-body"}} - - assert Strategy.request(:get, "form-data-body-already-decoded", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "form-data-body-already-decoded"}} + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/x-www-form-urlencoded"}], + body: %{"a" => "1"}, + http_adapter: HTTPMock, + request_url: "form-data-body" + }} + + assert Strategy.request(:get, "form-data-body-already-decoded", nil, [], + http_adapter: HTTPMock + ) == + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/x-www-form-urlencoded"}], + body: %{"a" => 1}, + http_adapter: HTTPMock, + request_url: "form-data-body-already-decoded" + }} end defmodule CustomJWTAdapter do @@ -132,22 +234,26 @@ defmodule Assent.StrategyTest do assert Strategy.sign_jwt(@token, @alg, @secret, jwt_adapter: CustomJWTAdapter) == :signed - assert {:ok, @empty_encoding <> "." <> _rest} = Strategy.sign_jwt(@token, @alg, @secret, json_library: CustomJSONLibrary) + assert {:ok, @empty_encoding <> "." <> _rest} = + Strategy.sign_jwt(@token, @alg, @secret, json_library: CustomJSONLibrary) end test "verify_jwt/2" do assert {:ok, jwt} = Strategy.verify_jwt(@token, @secret, []) assert jwt.verified? assert Strategy.verify_jwt(@token, @secret, jwt_adapter: CustomJWTAdapter) == :verified - assert {:ok, %{header: %{"custom_json" => true}}} = Strategy.verify_jwt(@token, @secret, json_library: CustomJSONLibrary) + + assert {:ok, %{header: %{"custom_json" => true}}} = + Strategy.verify_jwt(@token, @secret, json_library: CustomJSONLibrary) end test "to_url/3" do - assert Strategy.to_url("http://localhost", "/path", [a: 1, b: [c: 2, d: [e: 3]], f: [4, 5]]) == "http://localhost/path?a=1&b[c]=2&b[d][e]=3&f[]=4&f[]=5" + assert Strategy.to_url("http://localhost", "/path", a: 1, b: [c: 2, d: [e: 3]], f: [4, 5]) == + "http://localhost/path?a=1&b[c]=2&b[d][e]=3&f[]=4&f[]=5" end test "normalize_userinfo/2" do - user = %{"email" => "foo@example.com", "name" => nil, "nickname" => "foo"} + user = %{"email" => "foo@example.com", "name" => nil, "nickname" => "foo"} extra = %{"a" => "1"} expected = %{"email" => "foo@example.com", "nickname" => "foo", "a" => "1"} @@ -155,7 +261,7 @@ defmodule Assent.StrategyTest do end test "prune/1" do - map = %{a: :ok, b: nil, c: "", d: %{a: :ok, b: nil}} + map = %{a: :ok, b: nil, c: "", d: %{a: :ok, b: nil}} expected = %{a: :ok, c: "", d: %{a: :ok}} assert Strategy.prune(map) == expected diff --git a/test/support/strategies/oauth2_test_case.ex b/test/support/strategies/oauth2_test_case.ex index 271bbf2..8273501 100644 --- a/test/support/strategies/oauth2_test_case.ex +++ b/test/support/strategies/oauth2_test_case.ex @@ -6,7 +6,14 @@ defmodule Assent.Test.OAuth2TestCase do TestServer.start() params = %{"code" => "code_test_value", "state" => "state_test_value"} - config = [client_id: "id", client_secret: "secret", base_url: TestServer.url(), redirect_uri: "http://localhost:4000/auth/callback", session_params: %{state: "state_test_value"}] + + config = [ + client_id: "id", + client_secret: "secret", + base_url: TestServer.url(), + redirect_uri: "http://localhost:4000/auth/callback", + session_params: %{state: "state_test_value"} + ] {:ok, callback_params: params, config: config} end @@ -25,17 +32,20 @@ defmodule Assent.Test.OAuth2TestCase do def expect_oauth2_access_token_request(opts \\ [], assert_fn \\ nil) do access_token = Keyword.get(opts, :access_token, "access_token") token_params = Keyword.get(opts, :params, %{access_token: access_token}) - uri = Keyword.get(opts, :uri, "/oauth/token") - status_code = Keyword.get(opts, :status_code, 200) + uri = Keyword.get(opts, :uri, "/oauth/token") + status_code = Keyword.get(opts, :status_code, 200) - TestServer.add(uri, via: :post, to: fn conn -> - {:ok, body, _conn} = Plug.Conn.read_body(conn, []) - params = URI.decode_query(body) + TestServer.add(uri, + via: :post, + to: fn conn -> + {:ok, body, _conn} = Plug.Conn.read_body(conn, []) + params = URI.decode_query(body) - if assert_fn, do: assert_fn.(conn, params) + if assert_fn, do: assert_fn.(conn, params) - send_json_resp(conn, token_params, status_code) - end) + send_json_resp(conn, token_params, status_code) + end + ) end @spec expect_oauth2_user_request(map(), Keyword.t(), function() | nil) :: :ok @@ -48,15 +58,18 @@ defmodule Assent.Test.OAuth2TestCase do @spec expect_oauth2_api_request(binary(), map(), Keyword.t(), function() | nil) :: :ok def expect_oauth2_api_request(uri, response, opts \\ [], assert_fn \\ nil, method \\ :get) do access_token = Keyword.get(opts, :access_token, "access_token") - status_code = Keyword.get(opts, :status_code, 200) + status_code = Keyword.get(opts, :status_code, 200) - TestServer.add(uri, via: method, to: fn conn -> - if assert_fn, do: assert_fn.(conn) + TestServer.add(uri, + via: method, + to: fn conn -> + if assert_fn, do: assert_fn.(conn) - assert_bearer_token_in_header(conn, access_token) + assert_bearer_token_in_header(conn, access_token) - send_json_resp(conn, response, status_code) - end) + send_json_resp(conn, response, status_code) + end + ) end defp assert_bearer_token_in_header(conn, token) do diff --git a/test/support/strategies/oauth_test_case.ex b/test/support/strategies/oauth_test_case.ex index 99d7f83..45afd64 100644 --- a/test/support/strategies/oauth_test_case.ex +++ b/test/support/strategies/oauth_test_case.ex @@ -6,7 +6,14 @@ defmodule Assent.Test.OAuthTestCase do TestServer.start() params = %{"oauth_token" => "hh5s93j4hdidpola", "oauth_verifier" => "hfdp7dh39dks9884"} - config = [consumer_key: "dpf43f3p2l4k3l03", consumer_secret: "kd94hf93k423kf44", base_url: TestServer.url(), redirect_uri: "http://localhost:4000/auth/callback", session_params: %{oauth_token_secret: "request_token_secret"}] + + config = [ + consumer_key: "dpf43f3p2l4k3l03", + consumer_secret: "kd94hf93k423kf44", + base_url: TestServer.url(), + redirect_uri: "http://localhost:4000/auth/callback", + session_params: %{oauth_token_secret: "request_token_secret"} + ] {:ok, callback_params: params, config: config} end @@ -23,36 +30,46 @@ defmodule Assent.Test.OAuthTestCase do @spec expect_oauth_request_token_request(Keyword.t(), function() | nil) :: :ok def expect_oauth_request_token_request(opts \\ [], assert_fn \\ nil) do - response = Keyword.get(opts, :params, %{oauth_token: "hh5s93j4hdidpola", oauth_token_secret: "hdhd0244k9j7ao03"}) - uri = Keyword.get(opts, :uri, "/request_token") + response = + Keyword.get(opts, :params, %{ + oauth_token: "hh5s93j4hdidpola", + oauth_token_secret: "hdhd0244k9j7ao03" + }) + + uri = Keyword.get(opts, :uri, "/request_token") expect_oauth_request("POST", uri, opts, response, assert_fn) end defp expect_oauth_request(method, uri, opts, response, assert_fn) do - status_code = Keyword.get(opts, :status_code, 200) - content_type = Keyword.get(opts, :content_type, "application/x-www-form-urlencoded") + status_code = Keyword.get(opts, :status_code, 200) + content_type = Keyword.get(opts, :content_type, "application/x-www-form-urlencoded") response = case content_type do "application/x-www-form-urlencoded" -> URI.encode_query(response) - "application/json" -> Jason.encode!(response) - _any -> response + "application/json" -> Jason.encode!(response) + _any -> response end - TestServer.add(uri, via: method, to: fn conn -> - if assert_fn, do: assert_fn.(conn, parse_auth_header(conn)) + TestServer.add(uri, + via: method, + to: fn conn -> + if assert_fn, do: assert_fn.(conn, parse_auth_header(conn)) - conn - |> Conn.put_resp_content_type(content_type) - |> Conn.resp(status_code, response) - end) + conn + |> Conn.put_resp_content_type(content_type) + |> Conn.resp(status_code, response) + end + ) end @spec expect_oauth_access_token_request(Keyword.t()) :: :ok def expect_oauth_access_token_request(opts \\ [], assert_fn \\ nil) do - params = Keyword.get(opts, :params, %{oauth_token: "token", oauth_token_secret: "token_secret"}) - uri = Keyword.get(opts, :uri, "/access_token") + params = + Keyword.get(opts, :params, %{oauth_token: "token", oauth_token_secret: "token_secret"}) + + uri = Keyword.get(opts, :uri, "/access_token") expect_oauth_request("POST", uri, opts, params, assert_fn) end @@ -72,7 +89,7 @@ defmodule Assent.Test.OAuthTestCase do @spec expect_oauth_user_request(map(), Keyword.t()) :: :ok def expect_oauth_user_request(user_params, opts \\ [], assert_fn \\ nil) do - uri = Keyword.get(opts, :uri, "/api/user") + uri = Keyword.get(opts, :uri, "/api/user") opts = Keyword.put(opts, :content_type, "application/json") expect_oauth_request("GET", uri, opts, user_params, assert_fn) diff --git a/test/support/strategies/oidc_test_case.ex b/test/support/strategies/oidc_test_case.ex index cf681a2..c295313 100644 --- a/test/support/strategies/oidc_test_case.ex +++ b/test/support/strategies/oidc_test_case.ex @@ -6,45 +6,45 @@ defmodule Assent.Test.OIDCTestCase do alias Plug.Conn @private_key """ - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw - kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr - m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi - NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV - 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 - QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs - kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go - amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM - +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 - D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC - 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y - lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ - hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp - bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X - +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B - BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC - 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx - QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz - 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 - Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 - NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j - 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma - 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K - y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB - jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= - -----END RSA PRIVATE KEY----- - """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw + kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr + m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi + NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV + 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 + QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs + kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go + amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM + +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 + D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC + 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y + lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ + hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp + bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X + +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B + BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC + 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx + QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz + 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 + Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 + NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j + 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma + 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K + y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB + jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= + -----END RSA PRIVATE KEY----- + """ @public_key """ - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv - vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc - aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy - tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 - e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb - V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 - MwIDAQAB - -----END PUBLIC KEY----- - """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv + vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc + aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy + tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 + e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb + V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 + MwIDAQAB + -----END PUBLIC KEY----- + """ @client_id "id" @client_secret "secret" @claims %{ @@ -58,6 +58,7 @@ defmodule Assent.Test.OIDCTestCase do TestServer.start() params = %{"code" => "test_code_value", "state" => "test_state_value"} + config = [ client_id: @client_id, openid_configuration: %{ @@ -70,7 +71,8 @@ defmodule Assent.Test.OIDCTestCase do client_secret: @client_secret, base_url: TestServer.url(), redirect_uri: "http://localhost:4000/auth/callback", - session_params: %{state: "test_state_value"}] + session_params: %{state: "test_state_value"} + ] {:ok, callback_params: params, config: config} end @@ -86,22 +88,26 @@ defmodule Assent.Test.OIDCTestCase do @spec expect_openid_config_request(map()) :: :ok def expect_openid_config_request(openid_config, opts \\ []) do - uri = Keyword.get(opts, :uri, "/.well-known/openid-configuration") - status_code = Keyword.get(opts, :status_code, 200) + uri = Keyword.get(opts, :uri, "/.well-known/openid-configuration") + status_code = Keyword.get(opts, :status_code, 200) - TestServer.add(uri, via: :get, to: fn conn -> - send_json_resp(conn, openid_config, status_code) - end) + TestServer.add(uri, + via: :get, + to: fn conn -> + send_json_resp(conn, openid_config, status_code) + end + ) end @spec expect_oidc_access_token_request(Keyword.t(), function() | nil) :: :ok def expect_oidc_access_token_request(opts \\ [], assert_fn \\ nil) do - id_token = Keyword.get_lazy(opts, :id_token, fn -> - opts - |> Keyword.get(:id_token_opts, []) - |> Keyword.put_new(:iss, TestServer.url()) - |> gen_id_token() - end) + id_token = + Keyword.get_lazy(opts, :id_token, fn -> + opts + |> Keyword.get(:id_token_opts, []) + |> Keyword.put_new(:iss, TestServer.url()) + |> gen_id_token() + end) params = Keyword.get(opts, :params, %{access_token: "access_token", id_token: id_token}) @@ -115,38 +121,46 @@ defmodule Assent.Test.OIDCTestCase do @spec expect_oidc_jwks_uri_request(Keyword.t()) :: :ok def expect_oidc_jwks_uri_request(opts \\ []) do - uri = Keyword.get(opts, :uri, "/jwks_uri.json") - keys = opts[:keys] || gen_keys(opts) + uri = Keyword.get(opts, :uri, "/jwks_uri.json") + keys = opts[:keys] || gen_keys(opts) status_code = Keyword.get(opts, :status_code, 200) - TestServer.add(uri, via: :get, to: fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.send_resp(status_code, Jason.encode!(%{"keys" => keys})) - end) + TestServer.add(uri, + via: :get, + to: fn conn -> + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp(status_code, Jason.encode!(%{"keys" => keys})) + end + ) keys end @spec expect_oidc_userinfo_request(map() | binary(), Keyword.t()) :: :ok def expect_oidc_userinfo_request(claims_or_jwt, opts \\ []) + def expect_oidc_userinfo_request(claims, opts) when is_map(claims) do opts = Keyword.put_new(opts, :uri, "/userinfo") OAuth2TestCase.expect_oauth2_user_request(claims, opts) end + def expect_oidc_userinfo_request(jwt, opts) when is_binary(jwt) do - uri = Keyword.get(opts, :uri, "/userinfo") + uri = Keyword.get(opts, :uri, "/userinfo") access_token = Keyword.get(opts, :access_token, "access_token") - status_code = Keyword.get(opts, :status_code, 200) + status_code = Keyword.get(opts, :status_code, 200) - TestServer.add(uri, via: :get, to: fn conn -> - assert {"authorization", "Bearer #{access_token}"} in conn.req_headers + TestServer.add(uri, + via: :get, + to: fn conn -> + assert {"authorization", "Bearer #{access_token}"} in conn.req_headers - conn - |> Conn.put_resp_content_type("application/jwt") - |> Conn.send_resp(status_code, jwt) - end) + conn + |> Conn.put_resp_content_type("application/jwt") + |> Conn.send_resp(status_code, jwt) + end + ) end defp gen_keys(opts) do @@ -161,9 +175,9 @@ defmodule Assent.Test.OIDCTestCase do @spec gen_id_token(Keyword.t()) :: binary() def gen_id_token(opts \\ []) do - iss = Keyword.get(opts, :iss, "http://localhost") + iss = Keyword.get(opts, :iss, "http://localhost") claims = Keyword.get(opts, :claims, %{}) - alg = Keyword.get(opts, :alg, "RS256") + alg = Keyword.get(opts, :alg, "RS256") claims = @claims @@ -185,11 +199,13 @@ defmodule Assent.Test.OIDCTestCase do |> JOSE.JWK.from_oct() |> JOSE.JWT.sign(%{"alg" => "HS256"}, claims) end + defp signed_jwt("RS256", claims, opts) do @private_key |> JOSE.JWK.from_pem() |> JOSE.JWT.sign(add_kid(%{"alg" => "RS256"}, opts), claims) end + defp signed_jwt("none", claims, _opts) do 16 |> JOSE.JWK.generate_key()