diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1c17b..d3fb35c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ * `Assent.Strategy.OAuth2` now supports PKCE * `Assent.Strategy.OAuth2.Base.authorize_url/2` incomplete typespec fixed * `Assent.Strategy.decode_response/2` deprecated accepting result tuples and now accepts `Assent.HTTPAdapter.HTTPResponse` structs +* `Assent.Strategy.request/5` deprecated in favor of `Assent.Strategy.http_request/5` +* `Assent.Strategy.decode_response/2` deprecated in favor of `Assent.HTTPAdapter.decode_response/2` +* `Assent.Config.get/3` deprecated in favor of `Keyword.get/3` +* `Assent.Config.put/3` deprecated in favor of `Keyword.put/3` +* `Assent.Config.merge/2` deprecated in favor of `Keyword.merge/2` +* `Assent.Config.t()` type deprecated in favor of `Keyword.t()` type +* `Assent.Config.fetch/2` deprecated in favor of `Assent.fetch_config/2` ## v0.2.10 (2024-04-11) diff --git a/README.md b/README.md index fbf463e..dea88c7 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ defmodule ProviderAuth do @config # Session params should be added to the config so the strategy can use them - |> Config.put(:session_params, session_params) + |> Keyword.put(:session_params, session_params) |> Github.callback(params) |> case do {:ok, %{user: user, token: token}} -> @@ -166,8 +166,6 @@ config :my_app, :strategies, ```elixir defmodule MultiProviderAuth do - alias Assent.Config - @spec request(atom()) :: {:ok, map()} | {:error, term()} def request(provider) do config = config!(provider) @@ -180,7 +178,7 @@ defmodule MultiProviderAuth do config = config!(provider) config - |> Assent.Config.put(:session_params, session_params) + |> Keyword.put(:session_params, session_params) |> config[:strategy].callback(params) end @@ -189,7 +187,7 @@ defmodule MultiProviderAuth do Application.get_env(:my_app, :strategies)[provider] || raise "No provider configuration for #{provider}" - Config.put(config, :redirect_uri, "http://localhost:4000/oauth/#{provider}/callback") + Keyword.put(config, :redirect_uri, "http://localhost:4000/oauth/#{provider}/callback") end end ``` diff --git a/integration/lib/router.ex b/integration/lib/router.ex index 893ddff..67cea67 100644 --- a/integration/lib/router.ex +++ b/integration/lib/router.ex @@ -75,7 +75,7 @@ defmodule IntegrationServer.Router do unquote(path) |> config!() - |> Assent.Config.put(:session_params, get_session(conn, :session_params)) + |> Keyword.put(:session_params, get_session(conn, :session_params)) |> unquote(module).callback(conn.params) |> case do {:ok, %{user: user, token: token}} -> diff --git a/lib/assent.ex b/lib/assent.ex index ae5c232..4c7c948 100644 --- a/lib/assent.ex +++ b/lib/assent.ex @@ -1,6 +1,22 @@ defmodule Assent do @moduledoc false + defmodule MissingConfigError do + defexception [:key, :config] + + @type t :: %__MODULE__{ + key: atom(), + config: Keyword.t() + } + + def message(exception) do + key = inspect(exception.key) + config_keys = inspect(Keyword.keys(exception.config)) + + "Expected #{key} in config, got: #{config_keys}" + end + end + defmodule CallbackError do defexception [:message, :error, :error_uri] end @@ -18,18 +34,33 @@ defmodule Assent do end defmodule MissingParamError do - defexception [:expected_key, :params] + defexception [:key, :params] @type t :: %__MODULE__{ - expected_key: binary(), + key: binary(), params: map() } + # TODO: Deprecated, remove in 0.3 + def exception(opts) do + opts = + case Keyword.fetch(opts, :expected_key) do + {:ok, key} -> + IO.warn("The `expected_key` option is deprecated. Please use `key` instead.") + [key: key, params: opts[:params]] + + :error -> + opts + end + + struct!(__MODULE__, opts) + end + def message(exception) do - expected_key = inspect(exception.expected_key) - params = inspect(Map.keys(exception.params)) + key = inspect(exception.key) + param_keys = exception.params |> Map.keys() |> Enum.sort() |> inspect() - "Expected #{expected_key} in params, got: #{params}" + "Expected #{key} in params, got: #{param_keys}" end end @@ -112,6 +143,49 @@ defmodule Assent do end end + @doc """ + Fetches the key value from the configuration. + + Returns a `Assent.MissingConfigError` if the key is not found. + """ + @spec fetch_config(Keyword.t(), atom()) :: {:ok, any()} | {:error, MissingConfigError.t()} + def fetch_config(config, key) when is_list(config) and is_atom(key) do + case Keyword.fetch(config, key) do + {:ok, value} -> {:ok, value} + :error -> {:error, MissingConfigError.exception(key: key, config: config)} + end + end + + @doc """ + Fetches the key value from the params. + + Returns a `Assent.MissingParamError` if the key is not found. + """ + @spec fetch_param(map(), binary()) :: {:ok, any()} | {:error, MissingParamError.t()} + def fetch_param(params, key) when is_map(params) and is_binary(key) do + case Map.fetch(params, key) do + {:ok, value} -> {:ok, value} + :error -> {:error, MissingParamError.exception(key: key, params: params)} + end + end + + @default_json_library (Code.ensure_loaded?(JSON) && JSON) || Jason + + @doc """ + Fetches the JSON library in config. + + If not found in provided config, this will attempt to load the JSON library + from global application environment for `:assent`. Defaults to + `#{inspect(@default_json_library)}`. + """ + @spec json_library(Keyword.t()) :: module() + def json_library(config) do + case Keyword.fetch(config, :json_library) do + :error -> Application.get_env(:assent, :json_library, @default_json_library) + {:ok, json_library} -> json_library + end + end + import Bitwise @doc false diff --git a/lib/assent/config.ex b/lib/assent/config.ex index 8d85417..961f245 100644 --- a/lib/assent/config.ex +++ b/lib/assent/config.ex @@ -1,9 +1,8 @@ +# TODO: Deprecated, remove in 0.3 defmodule Assent.Config do - @moduledoc """ - Methods to handle configurations. - """ + @moduledoc false - defmodule MissingKeyError do + defmodule MissingConfigError do @type t :: %__MODULE__{} defexception [:key] @@ -15,51 +14,29 @@ defmodule Assent.Config do @type t :: Keyword.t() - @doc """ - Fetches the key value from the configuration. - """ - @spec fetch(t(), atom()) :: {:ok, any()} | {:error, MissingKeyError.t()} - def fetch(config, key) do - case Keyword.fetch(config, key) do - {:ok, value} -> {:ok, value} - :error -> {:error, MissingKeyError.exception(key: key)} - end - end + @doc false + @deprecated "Use Assent.fetch_config/2 instead" + def fetch(config, key), do: Assent.fetch_config(config, key) + @deprecated "Use Keyword.get/3 instead" defdelegate get(config, key, default), to: Keyword + @deprecated "Use Keyword.put/3 instead" defdelegate put(config, key, value), to: Keyword + @deprecated "Use Keyword.merge/2 instead" defdelegate merge(config_a, config_b), to: Keyword - @default_json_library (Code.ensure_loaded?(JSON) && JSON) || Jason - - @doc """ - Fetches the JSON library in config. - - If not found in provided config, this will attempt to load the JSON library - from global application environment for `:assent`. Defaults to - `#{@default_json_library}`. - """ - @spec json_library(t()) :: module() - def json_library(config) do - case get(config, :json_library, nil) do - nil -> - Application.get_env(:assent, :json_library, @default_json_library) - - json_library -> - json_library - end - end + @deprecated "Use Assent.json_library/1 instead" + def json_library(config), do: Assent.json_library(config) - # TODO: Remove in next major version def __base_url__(config) do - case fetch(config, :base_url) do + case Assent.fetch_config(config, :base_url) do {:ok, base_url} -> {:ok, base_url} {:error, error} -> - case fetch(config, :site) do + case Assent.fetch_config(config, :site) do {:ok, base_url} -> IO.warn("The `:site` configuration key is deprecated, use `:base_url` instead") {:ok, base_url} diff --git a/lib/assent/http_adapter.ex b/lib/assent/http_adapter.ex index a486c3e..e10b75c 100644 --- a/lib/assent/http_adapter.ex +++ b/lib/assent/http_adapter.ex @@ -26,6 +26,7 @@ defmodule Assent.HTTPAdapter do end end """ + alias Assent.{InvalidResponseError, ServerUnreachableError} defmodule HTTPResponse do @moduledoc """ @@ -69,7 +70,7 @@ defmodule Assent.HTTPAdapter do {:ok, map()} | {:error, any()} @doc """ - Sets a user agent header + Sets a user agent header. The header value will be `Assent-VERSION` with VERSION being the `:vsn` of `:assent` app. @@ -80,4 +81,99 @@ defmodule Assent.HTTPAdapter do {"User-Agent", "Assent-#{version}"} end + + @default_http_client Enum.find_value( + [ + {Req, Assent.HTTPAdapter.Req}, + {:httpc, Assent.HTTPAdapter.Httpc} + ], + fn {dep, module} -> + Code.ensure_loaded?(dep) && {module, []} + end + ) + + @doc """ + Makes a HTTP request. + + ## Options + + - `:http_adapter` - The HTTP adapter to use, defaults to + `#{inspect(elem(@default_http_client, 0))}`. + - `:json_library` - The JSON library to use, see + `Assent.json_library/1`. + """ + @spec request(atom(), binary(), binary() | nil, list(), Keyword.t()) :: + {:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()} + def request(method, url, body, headers, opts) do + {http_adapter, http_adapter_opts} = get_adapter(opts) + + method + |> http_adapter.request(url, body, headers, http_adapter_opts) + |> case do + {:ok, response} -> + decode_response(response, opts) + + {:error, error} -> + {:error, + ServerUnreachableError.exception( + reason: error, + http_adapter: http_adapter, + request_url: url + )} + end + |> case do + {:ok, %{status: status} = resp} when status in 200..399 -> + {:ok, %{resp | http_adapter: http_adapter, request_url: url}} + + {:ok, %{status: status} = resp} when status in 400..599 -> + {:error, %{resp | http_adapter: http_adapter, request_url: url}} + + {:error, error} -> + {:error, error} + end + end + + defp get_adapter(opts) do + default_http_adapter = Application.get_env(:assent, :http_adapter, @default_http_client) + + case Keyword.get(opts, :http_adapter, default_http_adapter) do + {http_adapter, opts} -> {http_adapter, opts} + http_adapter when is_atom(http_adapter) -> {http_adapter, nil} + end + end + + @doc """ + Decodes request response body. + + ## Options + + - `:json_library` - The JSON library to use, see + `Assent.json_library/1` + """ + @spec decode_response(HTTPResponse.t(), Keyword.t()) :: + {:ok, HTTPResponse.t()} | {:error, InvalidResponseError.t()} + def decode_response(%HTTPResponse{} = response, opts) do + case decode(response.headers, response.body, opts) do + {:ok, body} -> {:ok, %{response | body: body}} + {:error, _error} -> {:error, InvalidResponseError.exception(response: response)} + end + end + + defp decode(headers, body, opts) when is_binary(body) do + case List.keyfind(headers, "content-type", 0) do + {"content-type", "application/json" <> _rest} -> + Assent.json_library(opts).decode(body) + + {"content-type", "text/javascript" <> _rest} -> + Assent.json_library(opts).decode(body) + + {"content-type", "application/x-www-form-urlencoded" <> _reset} -> + {:ok, URI.decode_query(body)} + + _any -> + {:ok, body} + end + end + + defp decode(_headers, body, _opts), do: {:ok, body} end diff --git a/lib/assent/jwt_adapter.ex b/lib/assent/jwt_adapter.ex index 163952f..f04f67b 100644 --- a/lib/assent/jwt_adapter.ex +++ b/lib/assent/jwt_adapter.ex @@ -30,15 +30,21 @@ defmodule Assent.JWTAdapter do end end """ - - 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()} + @default_jwt_adapter Assent.JWTAdapter.AssentJWT + @doc """ - Generates a signed JSON Web Token signature + Generates a signed JSON Web Token signature. + + ## Options + + - `:json_library` - The JSON library to use, optional, see + `Assent.json_library/1`. + - `:jwt_adapter` - The JWT adapter module to use, optional, defaults to + `#{inspect(@default_jwt_adapter)}` """ @spec sign(map(), binary(), binary(), Keyword.t()) :: {:ok, binary()} | {:error, term()} def sign(claims, alg, secret, opts \\ []) do @@ -47,7 +53,14 @@ defmodule Assent.JWTAdapter do end @doc """ - Verifies the JSON Web Token signature + Verifies the JSON Web Token signature. + + ## Options + + - `:json_library` - The JSON library to use, optional, see + `Assent.json_library/1`. + - `:jwt_adapter` - The JWT adapter module to use, optional, defaults to + `#{inspect(@default_jwt_adapter)}` """ @spec verify(binary(), binary() | map() | nil, Keyword.t()) :: {:ok, map()} | {:error, any()} def verify(token, secret, opts \\ []) do @@ -56,8 +69,8 @@ defmodule Assent.JWTAdapter do end defp get_adapter(opts) do - default_opts = Keyword.put(opts, :json_library, Config.json_library(opts)) - default_jwt_adapter = Application.get_env(:assent, :jwt_adapter, Assent.JWTAdapter.AssentJWT) + default_opts = Keyword.put(opts, :json_library, Assent.json_library(opts)) + default_jwt_adapter = Application.get_env(:assent, :jwt_adapter, @default_jwt_adapter) case Keyword.get(opts, :jwt_adapter, default_jwt_adapter) do {adapter, opts} -> {adapter, Keyword.merge(default_opts, opts)} @@ -66,13 +79,18 @@ defmodule Assent.JWTAdapter do end @doc """ - Loads a private key from the provided configuration + Loads a private key from the provided configuration. + + ## Options + + - `:private_key_path` - The path to the private key file, optional. + - `:private_key` - The private key, required if `:private_key_path` is not set. """ - @spec load_private_key(Config.t()) :: {:ok, binary()} | {:error, term()} + @spec load_private_key(Keyword.t()) :: {:ok, binary()} | {:error, term()} def load_private_key(config) do - case Config.fetch(config, :private_key_path) do + case Assent.fetch_config(config, :private_key_path) do {:ok, path} -> read(path) - {:error, _any} -> Config.fetch(config, :private_key) + {:error, _any} -> Assent.fetch_config(config, :private_key) end end diff --git a/lib/assent/jwt_adapter/assent_jwt.ex b/lib/assent/jwt_adapter/assent_jwt.ex index 0e5459b..108313c 100644 --- a/lib/assent/jwt_adapter/assent_jwt.ex +++ b/lib/assent/jwt_adapter/assent_jwt.ex @@ -8,7 +8,7 @@ defmodule Assent.JWTAdapter.AssentJWT do See `Assent.JWTAdapter` for more. """ - alias Assent.{Config, JWTAdapter} + alias Assent.JWTAdapter @behaviour Assent.JWTAdapter @@ -26,9 +26,9 @@ defmodule Assent.JWTAdapter.AssentJWT do defp encode_header(alg, opts) do header = - case Keyword.has_key?(opts, :private_key_id) do - false -> %{"typ" => "JWT", "alg" => alg} - true -> %{"typ" => "JWT", "alg" => alg, "kid" => Keyword.get(opts, :private_key_id)} + case Keyword.fetch(opts, :private_key_id) do + :error -> %{"typ" => "JWT", "alg" => alg} + {:ok, private_key_id} -> %{"typ" => "JWT", "alg" => alg, "kid" => private_key_id} end case encode_json_base64(header, opts) do @@ -41,7 +41,7 @@ defmodule Assent.JWTAdapter.AssentJWT do end defp encode_json_base64(map, opts) do - with {:ok, json_library} <- Config.fetch(opts, :json_library), + with {:ok, json_library} <- Assent.fetch_config(opts, :json_library), {:ok, json} <- json_encode(json_library, map) do {:ok, Base.url_encode64(json, padding: false)} end @@ -173,7 +173,7 @@ defmodule Assent.JWTAdapter.AssentJWT do end defp decode_header(header, opts) do - with {:ok, json_library} <- Config.fetch(opts, :json_library), + with {:ok, json_library} <- Assent.fetch_config(opts, :json_library), {:ok, header} <- decode_base64_url(header), {:ok, header} <- decode_json(header, json_library), {:ok, alg} <- fetch_alg(header) do @@ -202,7 +202,7 @@ defmodule Assent.JWTAdapter.AssentJWT do defp fetch_alg(_header), do: {:error, "No \"alg\" found in header"} defp decode_claims(claims, opts) do - with {:ok, json_library} <- Config.fetch(opts, :json_library), + with {:ok, json_library} <- Assent.fetch_config(opts, :json_library), {:ok, claims} <- decode_base64_url(claims), {:ok, claims} <- decode_json(claims, json_library) do {:ok, claims} diff --git a/lib/assent/jwt_adapter/jose.ex b/lib/assent/jwt_adapter/jose.ex index 31efc24..180dae3 100644 --- a/lib/assent/jwt_adapter/jose.ex +++ b/lib/assent/jwt_adapter/jose.ex @@ -32,9 +32,9 @@ defmodule Assent.JWTAdapter.JOSE do defp jws(alg, opts) do jws = %{"alg" => alg} - case Keyword.get(opts, :private_key_id) do - nil -> jws - kid -> Map.put(jws, "kid", kid) + case Keyword.fetch(opts, :private_key_id) do + :error -> jws + {:ok, kid} -> Map.put(jws, "kid", kid) end end diff --git a/lib/assent/strategies/apple.ex b/lib/assent/strategies/apple.ex index be1f3f8..98cfaab 100644 --- a/lib/assent/strategies/apple.ex +++ b/lib/assent/strategies/apple.ex @@ -48,11 +48,11 @@ defmodule Assent.Strategy.Apple do """ use Assent.Strategy.OIDC.Base - alias Assent.{Config, JWTAdapter, Strategy.OIDC, Strategy.OIDC.Base} + alias Assent.{JWTAdapter, Strategy.OIDC, Strategy.OIDC.Base} @impl true def default_config(config) do - base_url = Config.get(config, :base_url, "https://appleid.apple.com") + base_url = Keyword.get(config, :base_url, "https://appleid.apple.com") [ base_url: base_url, @@ -65,7 +65,7 @@ defmodule Assent.Strategy.Apple do }, authorization_params: [scope: "email", response_mode: "form_post"], client_authentication_method: "client_secret_post", - openid_default_scope: "" + openid_default_scope: nil ] end @@ -75,8 +75,8 @@ defmodule Assent.Strategy.Apple do with {:ok, client_secret} <- gen_client_secret(config), {:ok, user_info} <- decode_user_params(config, params) do config - |> Config.put(:client_secret, client_secret) - |> Config.put(:user, user_info) + |> Keyword.put(:client_secret, client_secret) + |> Keyword.put(:user, user_info) |> Base.callback(params, __MODULE__) end end @@ -91,9 +91,9 @@ defmodule Assent.Strategy.Apple do |> 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), + with {:ok, base_url} <- Assent.fetch_config(config, :base_url), + {:ok, client_id} <- Assent.fetch_config(config, :client_id), + {:ok, team_id} <- Assent.fetch_config(config, :team_id), :ok <- ensure_private_key_id(config), {:ok, private_key} <- JWTAdapter.load_private_key(config) do claims = %{ @@ -109,7 +109,7 @@ defmodule Assent.Strategy.Apple do end defp ensure_private_key_id(config) do - with {:ok, _private_key_id} <- Config.fetch(config, :private_key_id) do + with {:ok, _private_key_id} <- Assent.fetch_config(config, :private_key_id) do :ok end end @@ -120,7 +120,7 @@ defmodule Assent.Strategy.Apple do @impl true def fetch_user(config, token) do with {:ok, user} <- OIDC.fetch_user(config, token), - {:ok, user_info} <- Config.fetch(config, :user) do + {:ok, user_info} <- Assent.fetch_config(config, :user) do {:ok, Map.merge(user, user_info)} end end diff --git a/lib/assent/strategies/auth0.ex b/lib/assent/strategies/auth0.ex index f95e941..c8c5ff6 100644 --- a/lib/assent/strategies/auth0.ex +++ b/lib/assent/strategies/auth0.ex @@ -15,8 +15,6 @@ defmodule Assent.Strategy.Auth0 do """ use Assent.Strategy.OAuth2.Base - alias Assent.Config - @impl true def default_config(config) do append_domain_config(config, @@ -29,8 +27,8 @@ defmodule Assent.Strategy.Auth0 do end defp append_domain_config(config, default) do - case Config.fetch(config, :domain) do - {:ok, domain} -> Config.put(default, :base_url, prepend_scheme(domain)) + case Assent.fetch_config(config, :domain) do + {:ok, domain} -> Keyword.put(default, :base_url, prepend_scheme(domain)) _error -> default end end diff --git a/lib/assent/strategies/azure_ad.ex b/lib/assent/strategies/azure_ad.ex index 18fbd51..690f558 100644 --- a/lib/assent/strategies/azure_ad.ex +++ b/lib/assent/strategies/azure_ad.ex @@ -36,11 +36,9 @@ defmodule Assent.Strategy.AzureAD do """ use Assent.Strategy.OIDC.Base - alias Assent.Config - @impl true def default_config(config) do - tenant_id = Config.get(config, :tenant_id, "common") + tenant_id = Keyword.get(config, :tenant_id, "common") [ base_url: "https://login.microsoftonline.com/#{tenant_id}/v2.0", diff --git a/lib/assent/strategies/bitbucket.ex b/lib/assent/strategies/bitbucket.ex index 02145ba..7c2e9ce 100644 --- a/lib/assent/strategies/bitbucket.ex +++ b/lib/assent/strategies/bitbucket.ex @@ -18,7 +18,7 @@ defmodule Assent.Strategy.Bitbucket do """ use Assent.Strategy.OAuth2.Base - alias Assent.{Config, Strategy.OAuth2} + alias Assent.Strategy.OAuth2 @impl true def default_config(_config) do @@ -51,7 +51,7 @@ defmodule Assent.Strategy.Bitbucket do @impl true def fetch_user(config, access_token) do - with {:ok, user_emails_url} <- Config.fetch(config, :user_emails_url), + with {:ok, user_emails_url} <- Assent.fetch_config(config, :user_emails_url), {:ok, user} <- OAuth2.fetch_user(config, access_token) do fetch_email(config, access_token, user, user_emails_url) end diff --git a/lib/assent/strategies/digital_ocean.ex b/lib/assent/strategies/digital_ocean.ex index 5eaffb9..55797d1 100644 --- a/lib/assent/strategies/digital_ocean.ex +++ b/lib/assent/strategies/digital_ocean.ex @@ -14,7 +14,6 @@ defmodule Assent.Strategy.DigitalOcean do """ use Assent.Strategy.OAuth2.Base - alias Assent.Config @impl true def default_config(config) do @@ -24,7 +23,7 @@ defmodule Assent.Strategy.DigitalOcean do token_url: "https://cloud.digitalocean.com/v1/oauth/token", user_url: "/v2/account", authorization_params: [ - prompt: Config.get(config, :prompt, "select_account"), + prompt: Keyword.get(config, :prompt, "select_account"), scope: "read write", response_type: "code" ], diff --git a/lib/assent/strategies/facebook.ex b/lib/assent/strategies/facebook.ex index 9d0a0af..fa9c3cb 100644 --- a/lib/assent/strategies/facebook.ex +++ b/lib/assent/strategies/facebook.ex @@ -62,12 +62,12 @@ defmodule Assent.Strategy.Facebook do {:ok, %{user: user, token: token}} = config - |> Assent.Config.put(:redirect_uri, "") + |> Keyword.put(:redirect_uri, "") |> Assent.Strategy.Facebook.callback(params) """ use Assent.Strategy.OAuth2.Base - alias Assent.{Config, Strategy.OAuth2} + alias Assent.Strategy.OAuth2 @api_version "4.0" @@ -86,7 +86,7 @@ defmodule Assent.Strategy.Facebook do @impl true def normalize(config, user) do - with {:ok, base_url} <- Config.fetch(config, :base_url) do + with {:ok, base_url} <- Assent.fetch_config(config, :base_url) do {:ok, %{ "sub" => user["id"], @@ -107,8 +107,8 @@ defmodule Assent.Strategy.Facebook do @impl true def fetch_user(config, access_token) do - with {:ok, fields} <- Config.fetch(config, :user_url_request_fields), - {:ok, client_secret} <- Config.fetch(config, :client_secret) do + with {:ok, fields} <- Assent.fetch_config(config, :user_url_request_fields), + {:ok, client_secret} <- Assent.fetch_config(config, :client_secret) do params = [ appsecret_proof: appsecret_proof(access_token, client_secret), fields: fields, diff --git a/lib/assent/strategies/github.ex b/lib/assent/strategies/github.ex index d11a9ea..6847ae2 100644 --- a/lib/assent/strategies/github.ex +++ b/lib/assent/strategies/github.ex @@ -20,7 +20,7 @@ defmodule Assent.Strategy.Github do """ use Assent.Strategy.OAuth2.Base - alias Assent.{Config, Strategy.OAuth2} + alias Assent.Strategy.OAuth2 @impl true def default_config(_config) do @@ -51,7 +51,7 @@ defmodule Assent.Strategy.Github do @impl true def fetch_user(config, access_token) do - with {:ok, user_emails_url} <- Config.fetch(config, :user_emails_url), + with {:ok, user_emails_url} <- Assent.fetch_config(config, :user_emails_url), {:ok, user} <- OAuth2.fetch_user(config, access_token) do fetch_email(config, access_token, user, user_emails_url) end diff --git a/lib/assent/strategies/instagram.ex b/lib/assent/strategies/instagram.ex index d7674b6..babf36f 100644 --- a/lib/assent/strategies/instagram.ex +++ b/lib/assent/strategies/instagram.ex @@ -17,7 +17,7 @@ defmodule Assent.Strategy.Instagram do """ use Assent.Strategy.OAuth2.Base - alias Assent.{Config, Strategy.OAuth2} + alias Assent.Strategy.OAuth2 @impl true def default_config(_config) do @@ -34,7 +34,7 @@ defmodule Assent.Strategy.Instagram do @impl true def fetch_user(config, access_token) do - with {:ok, fields} <- Config.fetch(config, :user_url_request_fields) do + with {:ok, fields} <- Assent.fetch_config(config, :user_url_request_fields) do params = [ fields: fields, access_token: access_token["access_token"] diff --git a/lib/assent/strategies/oauth.ex b/lib/assent/strategies/oauth.ex index b3186f6..e7d1bc7 100644 --- a/lib/assent/strategies/oauth.ex +++ b/lib/assent/strategies/oauth.ex @@ -35,12 +35,12 @@ defmodule Assent.Strategy.OAuth do {:ok, {url: url, session_params: session_params}} = config - |> Assent.Config.put(:redirect_uri, "http://localhost:4000/auth/callback") + |> Keyword.put(:redirect_uri, "http://localhost:4000/auth/callback") |> OAuth.authorize_url() {:ok, %{user: user, token: token}} = config - |> Assent.Config.put(:session_params, session_params) + |> Keyword.put(:session_params, session_params) |> OAuth.callback(params) """ @behaviour Assent.Strategy @@ -52,7 +52,6 @@ defmodule Assent.Strategy.OAuth do HTTPAdapter.HTTPResponse, InvalidResponseError, JWTAdapter, - MissingParamError, RequestError, UnexpectedResponseError } @@ -68,7 +67,7 @@ defmodule Assent.Strategy.OAuth do @doc """ Generate authorization URL for request phase. - ## Configuration + ## Options - `:redirect_uri` - The URI that the server redirects the user to after authentication, required @@ -79,9 +78,9 @@ defmodule Assent.Strategy.OAuth do - `:authorization_params` - The authorization parameters, defaults to `[]` """ @impl true - @spec authorize_url(Config.t()) :: on_authorize_url() + @spec authorize_url(Keyword.t()) :: on_authorize_url() def authorize_url(config) do - with {:ok, redirect_uri} <- Config.fetch(config, :redirect_uri), + with {:ok, redirect_uri} <- Assent.fetch_config(config, :redirect_uri), {:ok, token} <- fetch_request_token(config, [{"oauth_callback", redirect_uri}]), {:ok, url, oauth_token_secret} <- gen_authorize_url(config, token) do {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} @@ -90,7 +89,7 @@ defmodule Assent.Strategy.OAuth do defp fetch_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") + request_token_url = Keyword.get(config, :request_token_url, "/request_token") url = process_url(base_url, request_token_url) config @@ -122,7 +121,7 @@ defmodule Assent.Strategy.OAuth do |> Enum.to_list() |> Enum.map(fn {key, value} -> {to_string(key), value} end) - signature_method = Config.get(config, :signature_method, :hmac_sha1) + signature_method = Keyword.get(config, :signature_method, :hmac_sha1) with {:ok, oauth_params} <- gen_oauth_params(config, signature_method, oauth_params), {:ok, signed_header} <- @@ -140,12 +139,12 @@ defmodule Assent.Strategy.OAuth do query_params = url_params(method, params) url = Helpers.to_url(base_url, url, query_params) - Helpers.request(method, url, req_body, req_headers, config) + Helpers.http_request(method, url, req_body, req_headers, config) end end defp gen_oauth_params(config, signature_method, oauth_params) do - with {:ok, consumer_key} <- Config.fetch(config, :consumer_key) do + with {:ok, consumer_key} <- Assent.fetch_config(config, :consumer_key) do nonce = gen_nonce() signature_method = signature_method_value(signature_method) timestamp = to_string(:os.system_time(:second)) @@ -220,7 +219,7 @@ defmodule Assent.Strategy.OAuth do do: encoded_shared_secret(config, token_secret) defp encoded_shared_secret(config, token_secret) do - with {:ok, consumer_secret} <- Config.fetch(config, :consumer_secret) do + with {:ok, consumer_secret} <- Assent.fetch_config(config, :consumer_secret) do shared_secret = Enum.map_join([consumer_secret, token_secret || ""], "&", &percent_encode/1) {:ok, shared_secret} @@ -302,7 +301,7 @@ defmodule Assent.Strategy.OAuth 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 - authorization_url = Config.get(config, :authorize_url, "/authorize") + authorization_url = Keyword.get(config, :authorize_url, "/authorize") params = authorization_params(config, oauth_token: oauth_token) url = Helpers.to_url(base_url, authorization_url, params) @@ -319,15 +318,15 @@ defmodule Assent.Strategy.OAuth do defp authorization_params(config, params) do config - |> Config.get(:authorization_params, []) - |> Config.merge(params) + |> Keyword.get(:authorization_params, []) + |> Keyword.merge(params) |> List.keysort(0) end @doc """ Callback phase for generating access token and fetch user data. - ## Configuration + ## Options - `:access_token_url` - The path or URL to fetch the access token from, optional, defaults to `/oauth/access_token` @@ -336,26 +335,19 @@ defmodule Assent.Strategy.OAuth do `authorize_url/1`, optional """ @impl true - @spec callback(Config.t(), map(), atom()) :: on_callback() + @spec callback(Keyword.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do - with {:ok, oauth_token} <- fetch_param(params, "oauth_token"), - {:ok, oauth_verifier} <- fetch_param(params, "oauth_verifier"), + with {:ok, oauth_token} <- Assent.fetch_param(params, "oauth_token"), + {:ok, oauth_verifier} <- Assent.fetch_param(params, "oauth_verifier"), {:ok, token} <- fetch_access_token(config, oauth_token, oauth_verifier), {:ok, user} <- strategy.fetch_user(config, token) do {:ok, %{user: user, token: token}} end end - defp fetch_param(params, key) do - case Map.fetch(params, key) do - {:ok, value} -> {:ok, value} - :error -> {:error, MissingParamError.exception(expected_key: key, params: params)} - end - end - defp fetch_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") + access_token_url = Keyword.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]) @@ -376,7 +368,7 @@ 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()}]) :: + @spec request(Keyword.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), @@ -398,9 +390,9 @@ defmodule Assent.Strategy.OAuth do end @doc false - @spec fetch_user(Config.t(), map()) :: {:ok, map()} | {:error, term()} + @spec fetch_user(Keyword.t(), map()) :: {:ok, map()} | {:error, term()} def fetch_user(config, token) do - with {:ok, url} <- Config.fetch(config, :user_url) do + with {:ok, url} <- Assent.fetch_config(config, :user_url) do case request(config, token, :get, url) do {:ok, %HTTPResponse{status: 200, body: user}} when is_map(user) -> {:ok, user} diff --git a/lib/assent/strategies/oauth2.ex b/lib/assent/strategies/oauth2.ex index 63a58fd..b4fc885 100644 --- a/lib/assent/strategies/oauth2.ex +++ b/lib/assent/strategies/oauth2.ex @@ -64,13 +64,13 @@ defmodule Assent.Strategy.OAuth2 do {:ok, %{url: url, session_params: session_params}} = config - |> Assent.Config.put(:redirect_uri, "http://localhost:4000/auth/callback") + |> Keyword.put(:redirect_uri, "http://localhost:4000/auth/callback") |> Assent.Strategy.OAuth2.authorize_url() {:ok, %{user: user, token: token}} = config - |> Assent.Config.put(:redirect_uri, "http://localhost:4000/auth/callback") - |> Assent.Config.put(:session_params, session_params) + |> Keyword.put(:redirect_uri, "http://localhost:4000/auth/callback") + |> Keyword.put(:session_params, session_params) |> Assent.Strategy.OAuth2.callback(params) """ @behaviour Assent.Strategy @@ -84,7 +84,6 @@ defmodule Assent.Strategy.OAuth2 do HTTPAdapter.HTTPResponse, InvalidResponseError, JWTAdapter, - MissingParamError, RequestError, UnexpectedResponseError } @@ -103,7 +102,7 @@ defmodule Assent.Strategy.OAuth2 do @doc """ Generate authorization URL for request phase. - ## Configuration + ## Options - `:redirect_uri` - The URI that the server redirects the user to after authentication, required @@ -112,17 +111,17 @@ defmodule Assent.Strategy.OAuth2 do - `:authorization_params` - The authorization parameters, defaults to `[]` """ @impl true - @spec authorize_url(Config.t()) :: on_authorize_url() + @spec authorize_url(Keyword.t()) :: on_authorize_url() def authorize_url(config) do config = deprecated_state_handling(config) - with {:ok, redirect_uri} <- Config.fetch(config, :redirect_uri), + with {:ok, redirect_uri} <- Assent.fetch_config(config, :redirect_uri), {:ok, base_url} <- Config.__base_url__(config), - {:ok, client_id} <- Config.fetch(config, :client_id) do + {:ok, client_id} <- Assent.fetch_config(config, :client_id) do session_params = session_params(config) url_params = authorization_params(config, client_id, redirect_uri, session_params) - authorize_url = Config.get(config, :authorize_url, "/oauth/authorize") + authorize_url = Keyword.get(config, :authorize_url, "/oauth/authorize") url = Helpers.to_url(base_url, authorize_url, url_params) {:ok, %{url: url, session_params: Enum.into(session_params, %{})}} @@ -132,7 +131,7 @@ defmodule Assent.Strategy.OAuth2 do # TODO: Remove in >= 0.3 defp deprecated_state_handling(config) do config - |> Config.get(:authorization_params, []) + |> Keyword.get(:authorization_params, []) |> Keyword.get(:state) |> case do nil -> @@ -152,7 +151,7 @@ defmodule Assent.Strategy.OAuth2 do end defp state_params(config) do - case Config.get(config, :state, true) do + case Keyword.get(config, :state, true) do state when is_binary(state) -> [state: state] true -> [state: gen_url_encoded_base64(32)] false -> [] @@ -168,7 +167,7 @@ defmodule Assent.Strategy.OAuth2 do end defp code_verifier_params(config) do - case Config.get(config, :code_verifier, false) do + case Keyword.get(config, :code_verifier, false) do true -> code_verifier = gen_url_encoded_base64(128) @@ -184,7 +183,7 @@ defmodule Assent.Strategy.OAuth2 do end defp authorization_params(config, client_id, redirect_uri, session_params) do - params = Config.get(config, :authorization_params, []) + params = Keyword.get(config, :authorization_params, []) [ response_type: "code", @@ -203,7 +202,7 @@ defmodule Assent.Strategy.OAuth2 do user data. Returns a map with access token in `:token` and user data in `:user`. - ## Configuration + ## Options - `:token_url` - The path or URL to fetch the token from, optional, defaults to `/oauth/token` @@ -212,9 +211,9 @@ defmodule Assent.Strategy.OAuth2 do `authorize_url/1`, optional """ @impl true - @spec callback(Config.t(), map(), atom()) :: on_callback() + @spec callback(Keyword.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do - with {:ok, session_params} <- Config.fetch(config, :session_params), + with {:ok, session_params} <- Assent.fetch_config(config, :session_params), :ok <- check_error_params(params), :ok <- verify_state(config, session_params, params), {:ok, grant_params} <- fetch_grant_access_token_params(config, params, session_params), @@ -234,38 +233,31 @@ defmodule Assent.Strategy.OAuth2 do defp check_error_params(_params), do: :ok defp verify_state(config, session_params, params) do - case Config.get(config, :state, true) do + case Keyword.get(config, :state, true) do false -> :ok _true -> verify_state(session_params.state, params) end end - defp verify_state(stored_state, %{"state" => provided_state}) do - case Assent.constant_time_compare(stored_state, provided_state) do - true -> :ok - false -> {:error, CallbackCSRFError.exception(key: "state")} + defp verify_state(stored_state, params) do + with {:ok, provided_state} <- Assent.fetch_param(params, "state") do + case Assent.constant_time_compare(stored_state, provided_state) do + true -> :ok + false -> {:error, CallbackCSRFError.exception(key: "state")} + end end end - defp verify_state(_stored_state, params) do - {:error, MissingParamError.exception(expected_key: "state", params: params)} - end - defp fetch_grant_access_token_params(config, params, session_params) do - with {:ok, code} <- fetch_code_param(params), - {:ok, redirect_uri} <- Config.fetch(config, :redirect_uri), + with {:ok, code} <- Assent.fetch_param(params, "code"), + {:ok, redirect_uri} <- Assent.fetch_config(config, :redirect_uri), {:ok, code_verifier_params} <- fetch_code_verifer_params(config, session_params) do {:ok, [code: code, redirect_uri: redirect_uri] ++ code_verifier_params} end end - 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_verifer_params(config, session_params) do - case Config.get(config, :code_verifier, false) do + case Keyword.get(config, :code_verifier, false) do true -> {:ok, [code_verifier: Map.fetch!(session_params, :code_verifier)]} false -> {:ok, []} end @@ -274,10 +266,10 @@ defmodule Assent.Strategy.OAuth2 do @doc """ Grants an access token. """ - @spec grant_access_token(Config.t(), binary(), Keyword.t()) :: {:ok, map()} | {:error, term()} + @spec grant_access_token(Keyword.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") + auth_method = Keyword.get(config, :auth_method) + token_url = Keyword.get(config, :token_url, "/oauth/token") with {:ok, base_url} <- Config.__base_url__(config), {:ok, auth_headers, auth_body} <- authentication_params(auth_method, config) do @@ -287,13 +279,13 @@ defmodule Assent.Strategy.OAuth2 do body = URI.encode_query(params) :post - |> Helpers.request(url, body, headers, config) + |> Helpers.http_request(url, body, headers, config) |> process_access_token_response() end end defp authentication_params(nil, config) do - with {:ok, client_id} <- Config.fetch(config, :client_id) do + with {:ok, client_id} <- Assent.fetch_config(config, :client_id) do headers = [] body = [client_id: client_id] @@ -302,8 +294,8 @@ defmodule Assent.Strategy.OAuth2 do end defp authentication_params(:client_secret_basic, config) do - with {:ok, client_id} <- Config.fetch(config, :client_id), - {:ok, client_secret} <- Config.fetch(config, :client_secret) do + with {:ok, client_id} <- Assent.fetch_config(config, :client_id), + {:ok, client_secret} <- Assent.fetch_config(config, :client_secret) do auth = Base.encode64("#{client_id}:#{client_secret}") headers = [{"authorization", "Basic #{auth}"}] body = [] @@ -313,8 +305,8 @@ defmodule Assent.Strategy.OAuth2 do end defp authentication_params(:client_secret_post, config) do - with {:ok, client_id} <- Config.fetch(config, :client_id), - {:ok, client_secret} <- Config.fetch(config, :client_secret) do + with {:ok, client_id} <- Assent.fetch_config(config, :client_id), + {:ok, client_secret} <- Assent.fetch_config(config, :client_secret) do headers = [] body = [client_id: client_id, client_secret: client_secret] @@ -323,18 +315,18 @@ defmodule Assent.Strategy.OAuth2 do end defp authentication_params(:client_secret_jwt, config) do - alg = Config.get(config, :jwt_algorithm, "HS256") + alg = Keyword.get(config, :jwt_algorithm, "HS256") - with {:ok, client_secret} <- Config.fetch(config, :client_secret) do + with {:ok, client_secret} <- Assent.fetch_config(config, :client_secret) 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") + alg = Keyword.get(config, :jwt_algorithm, "RS256") with {:ok, pem} <- JWTAdapter.load_private_key(config), - {:ok, _private_key_id} <- Config.fetch(config, :private_key_id) do + {:ok, _private_key_id} <- Assent.fetch_config(config, :private_key_id) do jwt_authentication_params(alg, pem, config) end end @@ -361,7 +353,7 @@ defmodule Assent.Strategy.OAuth2 do timestamp = :os.system_time(:second) with {:ok, base_url} <- Config.__base_url__(config), - {:ok, client_id} <- Config.fetch(config, :client_id) do + {:ok, client_id} <- Assent.fetch_config(config, :client_id) do {:ok, %{ "iss" => client_id, @@ -402,7 +394,7 @@ defmodule Assent.Strategy.OAuth2 do @doc """ Refreshes the access token. """ - @spec refresh_access_token(Config.t(), map(), Keyword.t()) :: {:ok, map()} | {:error, term()} + @spec refresh_access_token(Keyword.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( @@ -416,7 +408,7 @@ defmodule Assent.Strategy.OAuth2 do @doc """ Performs a HTTP request to the API using the access token. """ - @spec request(Config.t(), map(), atom(), binary(), map() | Keyword.t(), [{binary(), binary()}]) :: + @spec request(Keyword.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), @@ -426,7 +418,7 @@ defmodule Assent.Strategy.OAuth2 do params = url_params(method, params) url = Helpers.to_url(base_url, url, params) - Helpers.request(method, url, req_body, req_headers, config) + Helpers.http_request(method, url, req_body, req_headers, config) end end @@ -470,10 +462,10 @@ defmodule Assent.Strategy.OAuth2 do Uses `request/6` to fetch the user data. """ - @spec fetch_user(Config.t(), map(), map() | Keyword.t(), [{binary(), binary()}]) :: + @spec fetch_user(Keyword.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 + with {:ok, user_url} <- Assent.fetch_config(config, :user_url) do case request(config, token, :get, user_url, params, headers) do {:ok, %HTTPResponse{status: 200, body: user}} when is_map(user) -> {:ok, user} diff --git a/lib/assent/strategies/oidc.ex b/lib/assent/strategies/oidc.ex index 292d9ab..f2b78ef 100644 --- a/lib/assent/strategies/oidc.ex +++ b/lib/assent/strategies/oidc.ex @@ -48,13 +48,13 @@ defmodule Assent.Strategy.OIDC do {:ok, {url: url, session_params: session_params}} = config - |> Assent.Config.put(:redirect_uri, "http://localhost:4000/auth/callback") + |> Keyword.put(:redirect_uri, "http://localhost:4000/auth/callback") |> Assent.Strategy.OIDC.authorize_url() {:ok, %{user: user, token: token}} = config - |> Assent.Config.put(:redirect_uri, "http://localhost:4000/auth/callback") - |> Assent.Config.put(:session_params, session_params) + |> Keyword.put(:redirect_uri, "http://localhost:4000/auth/callback") + |> Keyword.put(:session_params, session_params) |> Assent.Strategy.OIDC.callback(params) ## Nonce @@ -114,22 +114,22 @@ defmodule Assent.Strategy.OIDC do See `Assent.Strategy.OAuth2.authorize_url/1` for more. """ @impl true - @spec authorize_url(Config.t()) :: on_authorize_url() + @spec authorize_url(Keyword.t()) :: on_authorize_url() def authorize_url(config) do with {:ok, openid_config} <- fetch_openid_configuration(config), {:ok, authorize_url} <- fetch_from_openid_config(openid_config, "authorization_endpoint"), {:ok, params} <- fetch_authorization_params(config) do config - |> Config.put(:authorization_params, params) - |> Config.put(:authorize_url, authorize_url) + |> Keyword.put(:authorization_params, params) + |> Keyword.put(:authorize_url, authorize_url) |> OAuth2.authorize_url() |> add_nonce_to_session_params(config) end end defp fetch_openid_configuration(config) do - case Config.get(config, :openid_configuration, nil) do + case Keyword.get(config, :openid_configuration, nil) do nil -> fetch_openid_configuration_from_uri(config) openid_config -> {:ok, openid_config} end @@ -138,11 +138,11 @@ defmodule Assent.Strategy.OIDC do defp fetch_openid_configuration_from_uri(config) do with {:ok, base_url} <- Config.__base_url__(config) do configuration_url = - Config.get(config, :openid_configuration_uri, "/.well-known/openid-configuration") + Keyword.get(config, :openid_configuration_uri, "/.well-known/openid-configuration") url = Helpers.to_url(base_url, configuration_url) - case Helpers.request(:get, url, nil, [], config) do + case Helpers.http_request(:get, url, nil, [], config) do {:ok, %HTTPResponse{status: 200, body: configuration}} -> {:ok, configuration} @@ -168,7 +168,7 @@ defmodule Assent.Strategy.OIDC do defp fetch_authorization_params(config) do new_params = config - |> Config.get(:authorization_params, []) + |> Keyword.get(:authorization_params, []) |> add_default_scope_param(config) |> add_nonce_param(config) @@ -176,26 +176,27 @@ 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") - new_scope = String.trim(default <> " " <> scope) + default = Keyword.get(config, :openid_default_scope, "openid") - Config.put(params, :scope, new_scope) + case Keyword.fetch(params, :scope) do + :error -> Keyword.put(params, :scope, default) + {:ok, scope} -> Keyword.put(params, :scope, String.trim("#{default} #{scope}")) + end end defp add_nonce_param(params, config) do - case Config.get(config, :nonce, nil) do - nil -> params - nonce -> Config.put(params, :nonce, nonce) + case Keyword.fetch(config, :nonce) do + :error -> params + {:ok, nonce} -> Keyword.put(params, :nonce, nonce) end end defp add_nonce_to_session_params({:ok, resp}, config) do - case Config.get(config, :nonce, nil) do - nil -> + case Keyword.fetch(config, :nonce) do + :error -> {:ok, resp} - nonce -> + {:ok, nonce} -> session_params = resp |> Map.get(:session_params, %{}) @@ -224,21 +225,21 @@ defmodule Assent.Strategy.OIDC do See `Assent.Strategy.OAuth2.callback/3` for more. """ @impl true - @spec callback(Config.t(), map(), atom()) :: on_callback() + @spec callback(Keyword.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do with {:ok, openid_config} <- fetch_openid_configuration(config), {: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) - |> Config.put(:token_url, token_url) + |> Keyword.put(:openid_configuration, openid_config) + |> Keyword.put(:auth_method, method) + |> Keyword.put(:token_url, token_url) |> OAuth2.callback(params, strategy) end end defp fetch_client_authentication_method(openid_config, config) do - method = Config.get(config, :client_authentication_method, "client_secret_basic") + method = Keyword.get(config, :client_authentication_method, "client_secret_basic") methods = Map.get(openid_config, "token_endpoint_auth_methods_supported") supported_method? = (is_nil(methods) && true) || method in methods @@ -272,7 +273,7 @@ defmodule Assent.Strategy.OIDC do The ID Token is validated, and the claims is returned as the user params. Use `fetch_userinfo/2` to fetch the claims from the `userinfo` endpoint. """ - @spec fetch_user(Config.t(), map()) :: {:ok, map()} | {:error, term()} + @spec fetch_user(Keyword.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 @@ -300,12 +301,12 @@ defmodule Assent.Strategy.OIDC do The ID Token will be validated per [OpenID Connect Core 1.0 rules](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation). """ - @spec validate_id_token(Config.t(), binary()) :: {:ok, map()} | {:error, term()} + @spec validate_id_token(Keyword.t(), binary()) :: {:ok, map()} | {:error, term()} def validate_id_token(config, id_token) do - expected_alg = Config.get(config, :id_token_signed_response_alg, "RS256") + expected_alg = Keyword.get(config, :id_token_signed_response_alg, "RS256") with {:ok, openid_config} <- fetch_openid_configuration(config), - {:ok, client_id} <- Config.fetch(config, :client_id), + {:ok, client_id} <- Assent.fetch_config(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), @@ -331,7 +332,7 @@ defmodule Assent.Strategy.OIDC do defp peek_header(encoded, config) do with {:ok, header} <- split_header(encoded), {:ok, json} <- decode_base64_url(header) do - Config.json_library(config).decode(json) + Assent.json_library(config).decode(json) end end @@ -352,7 +353,7 @@ defmodule Assent.Strategy.OIDC do 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) + Assent.fetch_config(config, :client_secret) end defp fetch_secret(header, openid_config, config) do @@ -363,7 +364,7 @@ defmodule Assent.Strategy.OIDC do end defp fetch_public_keys(uri, config) do - case Helpers.request(:get, uri, nil, [], config) do + case Helpers.http_request(:get, uri, nil, [], config) do {:ok, %HTTPResponse{status: 200, body: %{"keys" => keys}}} -> {:ok, keys} @@ -419,7 +420,7 @@ defmodule Assent.Strategy.OIDC do 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] + trusted_audiences = Keyword.get(config, :trusted_audiences, []) ++ [client_id] missing_client_id? = client_id not in auds untrusted_auds = Enum.filter(auds, &(&1 not in trusted_audiences)) @@ -461,9 +462,9 @@ defmodule Assent.Strategy.OIDC do end defp validate_issued_at(%{claims: %{"iat" => iat}}, config) do - case Config.get(config, :id_token_ttl_seconds, nil) do - nil -> :ok - ttl -> validate_ttl_reached(iat, ttl) + case Keyword.fetch(config, :id_token_ttl_seconds) do + :error -> :ok + {:ok, ttl} -> validate_ttl_reached(iat, ttl) end end @@ -477,7 +478,7 @@ defmodule Assent.Strategy.OIDC do end defp validate_nonce(jwt, config) do - with {:ok, session_params} <- Config.fetch(config, :session_params) do + with {:ok, session_params} <- Assent.fetch_config(config, :session_params) do validate_for_nonce(session_params, jwt) end end @@ -506,7 +507,7 @@ defmodule Assent.Strategy.OIDC do The returned claims will be validated against the `id_token` verifying that `sub` is equal. """ - @spec fetch_userinfo(Config.t(), map()) :: {:ok, map()} | {:error, term()} + @spec fetch_userinfo(Keyword.t(), map()) :: {:ok, map()} | {:error, term()} def fetch_userinfo(config, token) do with {:ok, openid_config} <- fetch_openid_configuration(config), {:ok, userinfo_url} <- fetch_from_openid_config(openid_config, "userinfo_endpoint"), diff --git a/lib/assent/strategies/slack.ex b/lib/assent/strategies/slack.ex index 02fe5a9..c730f39 100644 --- a/lib/assent/strategies/slack.ex +++ b/lib/assent/strategies/slack.ex @@ -36,8 +36,6 @@ defmodule Assent.Strategy.Slack do """ use Assent.Strategy.OIDC.Base - alias Assent.Config - @impl true def default_config(config) do [ @@ -50,9 +48,9 @@ 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 + case Keyword.fetch(config, :team_id) do + {:ok, team_id} -> Keyword.put(default, :team, team_id) + :error -> default end end end diff --git a/lib/assent/strategies/telegram.ex b/lib/assent/strategies/telegram.ex index e06fb88..66d33ff 100644 --- a/lib/assent/strategies/telegram.ex +++ b/lib/assent/strategies/telegram.ex @@ -86,18 +86,18 @@ defmodule Assent.Strategy.Telegram do @behaviour Assent.Strategy - alias Assent.{CallbackError, Config, MissingParamError, Strategy} + alias Assent.{CallbackError, Strategy} @auth_ttl_seconds 60 @web_mini_app :web_mini_app @login_widget :login_widget @impl Assent.Strategy - @spec authorize_url(Config.t()) :: {:ok, %{url: binary()}} | {:error, term()} + @spec authorize_url(Keyword.t()) :: {:ok, %{url: binary()}} | {:error, term()} def authorize_url(config) do - with {:ok, bot_token} <- Config.fetch(config, :bot_token), - {:ok, origin} <- Config.fetch(config, :origin), - {:ok, return_to} <- Config.fetch(config, :return_to) do + with {:ok, bot_token} <- Assent.fetch_config(config, :bot_token), + {:ok, origin} <- Assent.fetch_config(config, :origin), + {:ok, return_to} <- Assent.fetch_config(config, :return_to) do [bot_id | _rest] = String.split(bot_token, ":") query = @@ -114,7 +114,7 @@ defmodule Assent.Strategy.Telegram do end @impl Assent.Strategy - @spec callback(Config.t(), map()) :: {:ok, %{user: map()} | {:error, term()}} + @spec callback(Keyword.t(), map()) :: {:ok, %{user: map()} | {:error, term()}} def callback(config, params) do with {:ok, authorization_channel} <- fetch_authorization_channel(config), {:ok, {hash, params}} <- split_hash_params(config, params, authorization_channel), @@ -127,7 +127,7 @@ defmodule Assent.Strategy.Telegram do end defp fetch_authorization_channel(config) do - case Config.get(config, :authorization_channel, @login_widget) do + case Keyword.get(config, :authorization_channel, @login_widget) do @login_widget -> {:ok, @login_widget} @@ -143,49 +143,46 @@ defmodule Assent.Strategy.Telegram do end defp split_hash_params(_config, params, @login_widget) do - case Map.split(params, ["hash"]) do - {%{"hash" => hash}, params} -> {:ok, {hash, params}} - {_, _} -> {:error, MissingParamError.exception(expected_key: "hash", params: params)} + with {:ok, hash} <- Assent.fetch_param(params, "hash") do + {:ok, {hash, Map.delete(params, "hash")}} end end - defp split_hash_params(config, %{"init_data" => init_data}, @web_mini_app) do - split_hash_params(config, URI.decode_query(init_data), @login_widget) + defp split_hash_params(config, params, @web_mini_app) do + with {:ok, init_data} <- Assent.fetch_param(params, "init_data") do + split_hash_params(config, URI.decode_query(init_data), @login_widget) + end end - defp split_hash_params(_config, params, @web_mini_app), - do: {:error, MissingParamError.exception(expected_key: "init_data", params: params)} - defp generate_token_signature(config, @login_widget) do - case Config.fetch(config, :bot_token) do + case Assent.fetch_config(config, :bot_token) do {:ok, bot_token} -> {:ok, :crypto.hash(:sha256, bot_token)} {:error, error} -> {:error, error} end end defp generate_token_signature(config, @web_mini_app) do - case Config.fetch(config, :bot_token) do + case Assent.fetch_config(config, :bot_token) do {:ok, bot_token} -> {:ok, :crypto.mac(:hmac, :sha256, "WebAppData", bot_token)} {:error, error} -> {:error, error} end end - defp verify_ttl(_config, %{"auth_date" => auth_date}) do - auth_timestamp = (is_binary(auth_date) && String.to_integer(auth_date)) || auth_date - - DateTime.utc_now() - |> DateTime.to_unix(:second) - |> Kernel.-(auth_timestamp) - |> Kernel.<=(@auth_ttl_seconds) - |> case do - true -> :ok - false -> {:error, CallbackError.exception(message: "Authorization request has expired")} + defp verify_ttl(_config, params) do + with {:ok, auth_date} <- Assent.fetch_param(params, "auth_date") do + auth_timestamp = (is_binary(auth_date) && String.to_integer(auth_date)) || auth_date + + DateTime.utc_now() + |> DateTime.to_unix(:second) + |> Kernel.-(auth_timestamp) + |> Kernel.<=(@auth_ttl_seconds) + |> case do + true -> :ok + false -> {:error, CallbackError.exception(message: "Authorization request has expired")} + end end end - defp verify_ttl(_config, params), - do: {:error, MissingParamError.exception(expected_key: "auth_date", params: params)} - defp verify_hash(secret, hash, params) do data = params diff --git a/lib/assent/strategies/vk.ex b/lib/assent/strategies/vk.ex index 68481d3..2847574 100644 --- a/lib/assent/strategies/vk.ex +++ b/lib/assent/strategies/vk.ex @@ -22,15 +22,15 @@ defmodule Assent.Strategy.VK do """ use Assent.Strategy.OAuth2.Base - alias Assent.{Config, Strategy.OAuth2} + alias Assent.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"] @impl true def default_config(config) do - params = Config.get(config, :user_url_params, []) - user_url_params = Config.merge(@url_params, params) + params = Keyword.get(config, :user_url_params, []) + user_url_params = Keyword.merge(@url_params, params) [ base_url: "https://api.vk.com", @@ -59,8 +59,8 @@ defmodule Assent.Strategy.VK do def fetch_user(config, token) do params = config - |> Config.get(:user_url_params, []) - |> Config.put(:access_token, token["access_token"]) + |> Keyword.get(:user_url_params, []) + |> Keyword.put(:access_token, token["access_token"]) config |> OAuth2.fetch_user(token, params) diff --git a/lib/assent/strategies/zitadel.ex b/lib/assent/strategies/zitadel.ex index 57dab74..7117957 100644 --- a/lib/assent/strategies/zitadel.ex +++ b/lib/assent/strategies/zitadel.ex @@ -18,13 +18,13 @@ defmodule Assent.Strategy.Zitadel do """ use Assent.Strategy.OIDC.Base - alias Assent.{Config, Strategy.OIDC} + alias Assent.Strategy.OIDC @impl true def default_config(config) do trusted_audiences = config - |> Config.get(:resource_id, nil) + |> Keyword.get(:resource_id) |> List.wrap() [ diff --git a/lib/assent/strategy.ex b/lib/assent/strategy.ex index cded5c3..1ca3f32 100644 --- a/lib/assent/strategy.ex +++ b/lib/assent/strategy.ex @@ -26,123 +26,40 @@ defmodule Assent.Strategy do end end """ - alias Assent.{Config, HTTPAdapter.HTTPResponse, InvalidResponseError, ServerUnreachableError} - - @callback authorize_url(Config.t()) :: + @callback authorize_url(Keyword.t()) :: {:ok, %{:url => binary(), optional(atom()) => any()}} | {:error, term()} - @callback callback(Config.t(), map()) :: + @callback callback(Keyword.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()} - def request(method, url, body, headers, config) do - {http_adapter, opts} = get_http_adapter(config) - method - |> http_adapter.request(url, body, headers, opts) - |> case do - {:ok, response} -> - decode_response(response, config) - - {:error, error} -> - {:error, - ServerUnreachableError.exception( - reason: error, - http_adapter: http_adapter, - request_url: url - )} - end - |> case do - {:ok, %{status: status} = resp} when status in 200..399 -> - {:ok, %{resp | http_adapter: http_adapter, request_url: url}} - - {:ok, %{status: status} = resp} when status in 400..599 -> - {:error, %{resp | http_adapter: http_adapter, request_url: url}} - - {:error, error} -> - {:error, error} - end - end - - @default_http_client Enum.find_value( - [ - {Req, Assent.HTTPAdapter.Req}, - {:httpc, Assent.HTTPAdapter.Httpc} - ], - fn {dep, module} -> - Code.ensure_loaded?(dep) && {module, []} - end - ) - - defp get_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 when is_atom(http_adapter) -> {http_adapter, nil} - end - end - - @doc """ - Decodes request response body. + See `Assent.HTTPAdapter.request/5`. """ - @spec decode_response(HTTPResponse.t(), Config.t()) :: - {:ok, HTTPResponse.t()} | {:error, InvalidResponseError.t()} - def decode_response(%HTTPResponse{} = response, config) do - case decode(response.headers, response.body, config) do - {:ok, body} -> {:ok, %{response | body: body}} - {:error, _error} -> {:error, InvalidResponseError.exception(response: response)} - end - end + def http_request(method, url, body, headers, config) do + opts = Keyword.take(config, [:http_adapter, :json_library]) - # TODO: Remove in 0.3 release - def decode_response({res, %HTTPResponse{} = response}, config) do - IO.warn("Passing {:ok | :error, response} to decode_response/2 is deprecated") - - case decode(response.headers, response.body, config) do - {:ok, body} -> {res, %{response | body: body}} - {:error, error} -> {:error, error} - end - end - - # TODO: Remove in 0.3 release - def decode_response({:error, error}, _config) do - IO.warn("Passing {:error, error} to decode_response/2 is deprecated") - - {:error, error} + Assent.HTTPAdapter.request(method, url, body, headers, opts) end - defp decode(headers, body, config) when is_binary(body) 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} - end - end + @doc """ + Decode a JSON string. - defp decode(_headers, body, _config), do: {:ok, body} + ## Options - @doc """ - Decode a JSON response to a map + - `:json_library` - The JSON library to use, see + `Assent.json_library/1` """ - @spec decode_json(binary(), Config.t()) :: {:ok, map()} | {:error, term()} - def decode_json(response, config), do: Config.json_library(config).decode(response) + @spec decode_json(binary(), Keyword.t()) :: {:ok, term()} | {:error, term()} + def decode_json(response, config), do: Assent.json_library(config).decode(response) @doc """ - Verifies a JWT + Verifies a JSON Web Token. + + See `Assent.JWTAdapter.verify/3` for options. """ - @spec verify_jwt(binary(), binary() | map() | nil, Config.t()) :: {:ok, map()} | {:error, any()} + @spec verify_jwt(binary(), binary() | map() | nil, Keyword.t()) :: + {:ok, map()} | {:error, any()} def verify_jwt(token, secret, config), do: Assent.JWTAdapter.verify(token, secret, jwt_adapter_opts(config)) @@ -150,23 +67,32 @@ defmodule Assent.Strategy do do: Keyword.take(config, [:json_library, :jwt_adapter, :private_key_id]) @doc """ - Signs a JWT + Signs a JSON Web Token. + + See `Assent.JWTAdapter.sign/3` for options. """ - @spec sign_jwt(map(), binary(), binary(), Config.t()) :: {:ok, binary()} | {:error, term()} + @spec sign_jwt(map(), binary(), binary(), Keyword.t()) :: {:ok, binary()} | {:error, term()} def sign_jwt(claims, alg, secret, config), do: Assent.JWTAdapter.sign(claims, alg, secret, jwt_adapter_opts(config)) @doc """ - Generates a URL + Generates a URL. """ @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) + def to_url(base_url, uri, params), do: "#{endpoint(base_url, uri)}?#{encode_query(params)}" + + defp endpoint(base_url, "/" <> uri) do + case :binary.last(base_url) do + ?/ -> "#{base_url}#{uri}" + _ -> "#{base_url}/#{uri}" + end end + defp endpoint(_base_url, uri), do: uri + defp encode_query(enumerable) do enumerable |> Enum.map(&encode_pair(&1, "")) @@ -181,9 +107,7 @@ defmodule Assent.Strategy do end defp encode_pair({key, value}, encoded_key) do - key = encoded_key <> "[" <> encode_value(key) <> "]" - - encode_pair(value, key) + encode_pair(value, "#{encoded_key}[#{encode_value(key)}]") end defp encode_pair([{_key, _value} | _rest] = values, encoded_key) do @@ -191,28 +115,17 @@ defmodule Assent.Strategy do end defp encode_pair(values, encoded_key) when is_list(values) do - key = encoded_key <> "[]" - - Enum.map(values, &encode_pair(&1, key)) + Enum.map(values, &encode_pair(&1, "#{encoded_key}[]")) end defp encode_pair(value, encoded_key) do - encoded_key <> "=" <> encode_value(value) + "#{encoded_key}=#{encode_value(value)}" end defp encode_value(value), do: URI.encode_www_form(Kernel.to_string(value)) - defp endpoint(base_url, "/" <> uri = all) do - case :binary.last(base_url) do - ?/ -> base_url <> uri - _ -> base_url <> all - end - end - - defp endpoint(_base_url, uri), do: uri - @doc """ - Normalize API user request response into standard claims + Normalize API user request response into standard claims. Based on https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.5.1 """ @@ -233,7 +146,7 @@ defmodule Assent.Strategy do @doc """ Recursively prunes map for nil values. """ - @spec prune(map) :: map + @spec prune(map()) :: map() def prune(map) do map |> Enum.map(fn {k, v} -> if is_map(v), do: {k, prune(v)}, else: {k, v} end) @@ -257,4 +170,30 @@ defmodule Assent.Strategy do end def __normalize__({:error, error}, _config, _strategy), do: {:error, error} + + # TODO: Remove in 0.3 + @deprecated "Use http_request/4 instead" + def request(method, url, body, headers, config), + do: http_request(method, url, body, headers, config) + + # TODO: Remove in 0.3 + def decode_response({res, %Assent.HTTPAdapter.HTTPResponse{} = response}, config) do + IO.warn("Passing {:ok | :error, response} to decode_response/2 is deprecated") + + case decode_response(response, config) do + {:ok, body} -> {res, %{response | body: body}} + {:error, error} -> {:error, error} + end + end + + # TODO: Remove in 0.3 + def decode_response({:error, error}, _config) do + IO.warn("Passing {:error, error} to decode_response/2 is deprecated") + + {:error, error} + end + + # TODO: Remove in 0.3 + @deprecated "Use Assent.HTTPAdapter.decode_response/2 instead" + def decode_response(response, config), do: Assent.HTTPAdapter.decode_response(response, config) end diff --git a/test/assent/http_adapter_test.exs b/test/assent/http_adapter_test.exs new file mode 100644 index 0000000..4f24849 --- /dev/null +++ b/test/assent/http_adapter_test.exs @@ -0,0 +1,239 @@ +defmodule Assent.HTTPAdapterTest do + use Assent.TestCase + doctest Assent.Strategy + + alias Assent.{HTTPAdapter, HTTPAdapter.HTTPResponse, InvalidResponseError} + + defmodule HTTPMock do + @json_library (Code.ensure_loaded?(JSON) && JSON) || Jason + + def request(:get, "http-adapter", nil, [], nil) do + {:ok, %HTTPResponse{status: 200, headers: [], body: nil}} + end + + def request(:get, "http-adapter-with-opts", nil, [], opts) do + {:ok, %HTTPResponse{status: 200, headers: [], body: opts}} + end + + def request(:get, "json-encoded-body", nil, [], nil) do + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "application/json"}], + body: @json_library.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} + }} + end + + def request(:get, "json-encoded-body-text/javascript-header", nil, [], nil) do + {:ok, + %HTTPResponse{ + status: 200, + headers: [{"content-type", "text/javascript"}], + body: @json_library.encode!(%{"a" => 1}) + }} + end + + def request(:get, "invalid-json-body", nil, [], nil) do + {:ok, + %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: "%"}} + end + + def request(:get, "json-no-headers", nil, [], nil) do + {:ok, %HTTPResponse{status: 200, headers: [], body: @json_library.encode!(%{"a" => 1})}} + 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}) + }} + 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} + }} + end + end + + test "request/5" do + assert HTTPAdapter.request(:get, "http-adapter", nil, [], http_adapter: HTTPMock) == + {:ok, + %HTTPResponse{ + status: 200, + headers: [], + body: nil, + http_adapter: HTTPMock, + request_url: "http-adapter" + }} + + assert HTTPAdapter.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 HTTPAdapter.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 HTTPAdapter.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 HTTPAdapter.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, %InvalidResponseError{}} = + HTTPAdapter.request(:get, "invalid-json-body", nil, [], http_adapter: HTTPMock) + + assert HTTPAdapter.request(:get, "json-no-headers", nil, [], http_adapter: HTTPMock) == + {:ok, + %HTTPResponse{ + status: 200, + headers: [], + body: @json_library.encode!(%{"a" => 1}), + http_adapter: HTTPMock, + request_url: "json-no-headers" + }} + + assert HTTPAdapter.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 HTTPAdapter.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 + @moduledoc false + + def sign(_claims, _alg, _secret, _opts), do: :signed + + def verify(_binary, _secret, _opts), do: :verified + end + + defmodule CustomJSONLibrary do + @moduledoc false + + def decode(_binary), do: {:ok, %{"alg" => "none", "custom_json" => true}} + + def encode!(_any), do: "" + end + + @body %{"a" => "1", "b" => "2"} + @headers [{"content-type", "application/json"}] + @json_encoded_body @json_library.encode!(@body) + @uri_encoded_body URI.encode_query(@body) + + test "decode_response/2" do + assert {:ok, response} = + HTTPAdapter.decode_response( + %HTTPResponse{body: @json_encoded_body, headers: @headers}, + [] + ) + + assert response.body == @body + + assert {:ok, response} = + HTTPAdapter.decode_response( + %HTTPResponse{ + body: @json_encoded_body, + headers: [{"content-type", "application/json; charset=utf-8"}] + }, + [] + ) + + assert response.body == @body + + assert {:ok, response} = + HTTPAdapter.decode_response( + %HTTPResponse{ + body: @json_encoded_body, + headers: [{"content-type", "text/javascript"}] + }, + [] + ) + + assert response.body == @body + + assert {:ok, response} = + HTTPAdapter.decode_response( + %HTTPResponse{ + body: @uri_encoded_body, + headers: [{"content-type", "application/x-www-form-urlencoded"}] + }, + [] + ) + + assert response.body == @body + + assert {:ok, response} = + HTTPAdapter.decode_response(%HTTPResponse{body: @body, headers: []}, []) + + assert response.body == @body + + assert {:error, %InvalidResponseError{} = error} = + HTTPAdapter.decode_response(%HTTPResponse{body: "%", headers: @headers}, []) + + assert error.response.body == "%" + end +end diff --git a/test/assent/jwt_adapter_test.exs b/test/assent/jwt_adapter_test.exs new file mode 100644 index 0000000..66bb901 --- /dev/null +++ b/test/assent/jwt_adapter_test.exs @@ -0,0 +1,63 @@ +defmodule Assent.JWTAdapterTest do + use Assent.TestCase + doctest Assent.Strategy + + alias Assent.JWTAdapter + + defmodule CustomJWTAdapter do + @moduledoc false + + def sign(_claims, _alg, _secret, _opts), do: :signed + + def verify(_binary, _secret, _opts), do: :verified + end + + defmodule CustomJSONLibrary do + @moduledoc false + + def decode(_binary), do: {:ok, %{"alg" => "none", "custom_json" => true}} + + def encode!(_any), do: "" + end + + @claims %{"iat" => 1_516_239_022, "name" => "John Doe", "sub" => "1234567890"} + @alg "HS256" + @secret "your-256-bit-secret" + @token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMzQ1Njc4OTAifQ.fdOPQ05ZfRhkST2-rIWgUpbqUsVhkkNVNcuG7Ki0s-8" + + @empty_encoding Base.url_encode64("", padding: false) + + test "sign/2" do + assert JWTAdapter.sign(@claims, @alg, @secret, []) == {:ok, @token} + + assert JWTAdapter.sign(@token, @alg, @secret, jwt_adapter: CustomJWTAdapter) == :signed + + assert {:ok, @empty_encoding <> "." <> _rest} = + JWTAdapter.sign(@token, @alg, @secret, json_library: CustomJSONLibrary) + end + + test "verify/2" do + assert {:ok, jwt} = JWTAdapter.verify(@token, @secret, []) + assert jwt.verified? + assert JWTAdapter.verify(@token, @secret, jwt_adapter: CustomJWTAdapter) == :verified + + assert {:ok, %{header: %{"custom_json" => true}}} = + JWTAdapter.verify(@token, @secret, json_library: CustomJSONLibrary) + end + + test "load_private_key/1" do + assert {:error, %Assent.MissingConfigError{} = error} = JWTAdapter.load_private_key([]) + assert error.key == :private_key + + assert JWTAdapter.load_private_key(private_key: "private_key") == {:ok, "private_key"} + + assert JWTAdapter.load_private_key(private_key_path: "tmp/invalid.pem") == + {:error, "Failed to read \"tmp/invalid.pem\", got; :enoent"} + + File.mkdir_p!("tmp/") + File.write!("tmp/private-key.pem", "private_key") + + assert JWTAdapter.load_private_key(private_key_path: "tmp/private-key.pem") == + {:ok, "private_key"} + end +end diff --git a/test/assent/strategies/auth0_test.exs b/test/assent/strategies/auth0_test.exs index 67c0e46..ed77ece 100644 --- a/test/assent/strategies/auth0_test.exs +++ b/test/assent/strategies/auth0_test.exs @@ -1,7 +1,7 @@ defmodule Assent.Strategy.Auth0Test do use Assent.Test.OAuth2TestCase - alias Assent.{Config.MissingKeyError, Strategy.Auth0} + alias Assent.{MissingConfigError, Strategy.Auth0} # From https://auth0.com/docs/api/authentication#user-profile @user_response %{ @@ -34,7 +34,7 @@ defmodule Assent.Strategy.Auth0Test do test "requires domain or base_url configuration", %{config: config} do config = Keyword.take(config, [:client_id, :redirect_uri]) - assert {:error, %MissingKeyError{} = error} = Auth0.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = Auth0.authorize_url(config) assert error.key == :base_url assert {:ok, %{url: url}} = Auth0.authorize_url(config ++ [base_url: "https://localhost"]) diff --git a/test/assent/strategies/oauth2_test.exs b/test/assent/strategies/oauth2_test.exs index 0d1e2ff..1a0d3be 100644 --- a/test/assent/strategies/oauth2_test.exs +++ b/test/assent/strategies/oauth2_test.exs @@ -8,8 +8,8 @@ defmodule Assent.Strategy.OAuth2Test do alias Assent.{ CallbackCSRFError, CallbackError, - Config.MissingKeyError, JWTAdapter.AssentJWT, + MissingConfigError, MissingParamError, RequestError, Strategy.OAuth2 @@ -63,21 +63,21 @@ defmodule Assent.Strategy.OAuth2Test do test "with missing `:redirect_uri` config", %{config: config} do config = Keyword.delete(config, :redirect_uri) - assert {:error, %MissingKeyError{} = error} = OAuth2.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth2.authorize_url(config) assert error.key == :redirect_uri end test "with missing `:base_url` config", %{config: config} do config = Keyword.delete(config, :base_url) - assert {:error, %MissingKeyError{} = error} = OAuth2.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth2.authorize_url(config) assert error.key == :base_url end test "with missing `:client_id` config", %{config: config} do config = Keyword.delete(config, :client_id) - assert {:error, %MissingKeyError{} = error} = OAuth2.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth2.authorize_url(config) assert error.key == :client_id end @@ -156,7 +156,7 @@ defmodule Assent.Strategy.OAuth2Test do test "with missing `:session_params` config", %{config: config, callback_params: params} do config = Keyword.delete(config, :session_params) - assert {:error, %MissingKeyError{} = error} = OAuth2.callback(config, params) + assert {:error, %MissingConfigError{} = error} = OAuth2.callback(config, params) assert error.key == :session_params end @@ -188,9 +188,7 @@ defmodule Assent.Strategy.OAuth2Test do params = Map.delete(params, "state") assert {:error, %MissingParamError{} = error} = OAuth2.callback(config, params) - assert Exception.message(error) == "Expected \"state\" in params, got: [\"code\"]" - assert error.expected_key == "state" - assert error.params == %{"code" => "code_test_value"} + assert error.key == "state" end test "with invalid `state` param", %{config: config, callback_params: params} do @@ -235,9 +233,7 @@ defmodule Assent.Strategy.OAuth2Test do params = Map.delete(params, "code") assert {:error, %MissingParamError{} = error} = OAuth2.callback(config, params) - assert Exception.message(error) == "Expected \"code\" in params, got: [\"state\"]" - assert error.expected_key == "code" - assert error.params == %{"state" => "state_test_value"} + assert error.key == "code" end test "with `code_verifier: true` with missing `:code_verifier` in session_params", %{ @@ -311,7 +307,7 @@ defmodule Assent.Strategy.OAuth2Test do expect_oauth2_access_token_request() - assert {:error, %MissingKeyError{} = error} = OAuth2.callback(config, params) + assert {:error, %MissingConfigError{} = error} = OAuth2.callback(config, params) assert error.key == :user_url end @@ -629,7 +625,9 @@ defmodule Assent.Strategy.OAuth2Test do test "with missing `:base_url` config", %{config: config, token: token} do config = Keyword.delete(config, :base_url) - assert {:error, %MissingKeyError{} = error} = OAuth2.request(config, token, :get, "/info") + assert {:error, %MissingConfigError{} = error} = + OAuth2.request(config, token, :get, "/info") + assert error.key == :base_url end diff --git a/test/assent/strategies/oauth_test.exs b/test/assent/strategies/oauth_test.exs index 0a5d4ac..2ed0a13 100644 --- a/test/assent/strategies/oauth_test.exs +++ b/test/assent/strategies/oauth_test.exs @@ -4,8 +4,8 @@ defmodule Assent.Strategy.OAuthTest do alias Assent.UnexpectedResponseError alias Assent.{ - Config.MissingKeyError, InvalidResponseError, + MissingConfigError, MissingParamError, RequestError, ServerUnreachableError, @@ -57,28 +57,28 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:redirect_uri` config", %{config: config} do config = Keyword.delete(config, :redirect_uri) - assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth.authorize_url(config) assert error.key == :redirect_uri end test "with missing `:base_url` config", %{config: config} do config = Keyword.delete(config, :base_url) - assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth.authorize_url(config) assert error.key == :base_url end test "with missing `:consumer_key` config", %{config: config} do config = Keyword.delete(config, :consumer_key) - assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth.authorize_url(config) assert error.key == :consumer_key end test "with missing `:consumer_secret` config", %{config: config} do config = Keyword.delete(config, :consumer_secret) - assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth.authorize_url(config) assert error.key == :consumer_secret end @@ -258,7 +258,7 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:private_key` config", %{config: config} do config = Keyword.delete(config, :private_key) - assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth.authorize_url(config) assert error.key == :private_key end @@ -336,7 +336,7 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:consumer_secret` config", %{config: config} do config = Keyword.delete(config, :consumer_secret) - assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert {:error, %MissingConfigError{} = error} = OAuth.authorize_url(config) assert error.key == :consumer_secret end @@ -367,11 +367,7 @@ 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 error.expected_key == "oauth_token" + assert error.key == "oauth_token" assert error.params == %{"oauth_verifier" => "hfdp7dh39dks9884"} end @@ -379,18 +375,14 @@ 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 error.expected_key == "oauth_verifier" + assert error.key == "oauth_verifier" assert error.params == %{"oauth_token" => "hh5s93j4hdidpola"} end test "with missing `:base_url` config", %{config: config, callback_params: callback_params} do config = Keyword.delete(config, :base_url) - assert {:error, %MissingKeyError{} = error} = OAuth.callback(config, callback_params) + assert {:error, %MissingConfigError{} = error} = OAuth.callback(config, callback_params) assert error.key == :base_url end @@ -450,7 +442,7 @@ defmodule Assent.Strategy.OAuthTest do expect_oauth_access_token_request() - assert {:error, %MissingKeyError{} = error} = OAuth.callback(config, params) + assert {:error, %MissingConfigError{} = error} = OAuth.callback(config, params) assert error.key == :user_url end @@ -529,7 +521,7 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:base_url` config", %{config: config, token: token} do config = Keyword.delete(config, :base_url) - assert {:error, %MissingKeyError{} = error} = OAuth.request(config, token, :get, "/info") + assert {:error, %MissingConfigError{} = error} = OAuth.request(config, token, :get, "/info") assert error.key == :base_url end @@ -546,14 +538,14 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:consumer_key` config", %{config: config, token: token} do config = Keyword.delete(config, :consumer_key) - assert {:error, %MissingKeyError{} = error} = OAuth.request(config, token, :get, "/info") + assert {:error, %MissingConfigError{} = error} = OAuth.request(config, token, :get, "/info") assert error.key == :consumer_key end test "with missing `:consumer_secret` config", %{config: config, token: token} do config = Keyword.delete(config, :consumer_secret) - assert {:error, %MissingKeyError{} = error} = OAuth.request(config, token, :get, "/info") + assert {:error, %MissingConfigError{} = error} = OAuth.request(config, token, :get, "/info") assert error.key == :consumer_secret end diff --git a/test/assent/strategies/oidc_test.exs b/test/assent/strategies/oidc_test.exs index 6aa3254..a37d139 100644 --- a/test/assent/strategies/oidc_test.exs +++ b/test/assent/strategies/oidc_test.exs @@ -351,7 +351,7 @@ 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} = + assert {:error, %Assent.MissingConfigError{} = error} = OIDC.validate_id_token(config, id_token) assert error.key == :client_id @@ -378,7 +378,7 @@ defmodule Assent.Strategy.OIDCTest do test "with no `:client_secret`", %{config: config, id_token: id_token} do config = Keyword.delete(config, :client_secret) - assert {:error, %Assent.Config.MissingKeyError{} = error} = + assert {:error, %Assent.MissingConfigError{} = error} = OIDC.validate_id_token(config, id_token) assert error.key == :client_secret @@ -501,7 +501,7 @@ defmodule Assent.Strategy.OIDCTest do 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} = + assert {:error, %Assent.MissingConfigError{} = error} = OIDC.validate_id_token(config, id_token) assert error.key == :session_params diff --git a/test/assent/strategies/telegram_test.exs b/test/assent/strategies/telegram_test.exs index 58701bd..ff1335e 100644 --- a/test/assent/strategies/telegram_test.exs +++ b/test/assent/strategies/telegram_test.exs @@ -68,21 +68,21 @@ defmodule Assent.Strategy.TelegramTest do test "with missing `:bot_token` config", %{config: config} do config = Keyword.delete(config, :bot_token) - assert {:error, %Assent.Config.MissingKeyError{} = error} = Telegram.authorize_url(config) + assert {:error, %Assent.MissingConfigError{} = error} = Telegram.authorize_url(config) assert error.key == :bot_token end test "with missing `:origin` config", %{config: config} do config = Keyword.delete(config, :origin) - assert {:error, %Assent.Config.MissingKeyError{} = error} = Telegram.authorize_url(config) + assert {:error, %Assent.MissingConfigError{} = error} = Telegram.authorize_url(config) assert error.key == :origin end test "with missing `:return_to` config", %{config: config} do config = Keyword.delete(config, :return_to) - assert {:error, %Assent.Config.MissingKeyError{} = error} = Telegram.authorize_url(config) + assert {:error, %Assent.MissingConfigError{} = error} = Telegram.authorize_url(config) assert error.key == :return_to end @@ -118,7 +118,7 @@ defmodule Assent.Strategy.TelegramTest do assert {:error, %Assent.MissingParamError{} = error} = Telegram.callback(config, callback_params) - assert error.expected_key == "hash" + assert error.key == "hash" end @tag authorization_channel: :web_mini_app @@ -131,7 +131,7 @@ defmodule Assent.Strategy.TelegramTest do assert {:error, %Assent.MissingParamError{} = error} = Telegram.callback(config, callback_params) - assert error.expected_key == "init_data" + assert error.key == "init_data" end test "with missing auth_date param", %{config: config, callback_params: callback_params} do @@ -140,7 +140,7 @@ defmodule Assent.Strategy.TelegramTest do assert {:error, %Assent.MissingParamError{} = error} = Telegram.callback(config, callback_params) - assert error.expected_key == "auth_date" + assert error.key == "auth_date" end test "with expired auth_date param", %{config: config, callback_params: callback_params} do @@ -158,7 +158,7 @@ defmodule Assent.Strategy.TelegramTest do test "with missing bot_token config", %{config: config, callback_params: callback_params} do config = Keyword.delete(config, :bot_token) - assert {:error, %Assent.Config.MissingKeyError{} = error} = + assert {:error, %Assent.MissingConfigError{} = error} = Telegram.callback(config, callback_params) assert error.key == :bot_token diff --git a/test/assent/strategy_test.exs b/test/assent/strategy_test.exs index c6c4f34..0bcc351 100644 --- a/test/assent/strategy_test.exs +++ b/test/assent/strategy_test.exs @@ -2,271 +2,48 @@ defmodule Assent.StrategyTest do use Assent.TestCase doctest Assent.Strategy - alias Assent.{HTTPAdapter.HTTPResponse, InvalidResponseError, Strategy} + alias Assent.Strategy - @body %{"a" => "1", "b" => "2"} - @headers [{"content-type", "application/json"}] - @json_encoded_body @json_library.encode!(@body) - @uri_encoded_body URI.encode_query(@body) - - test "decode_response/2" do - assert {:ok, response} = - Strategy.decode_response( - %HTTPResponse{body: @json_encoded_body, headers: @headers}, - [] - ) - - assert response.body == @body - - assert {:ok, response} = - Strategy.decode_response( - %HTTPResponse{ - body: @json_encoded_body, - headers: [{"content-type", "application/json; charset=utf-8"}] - }, - [] - ) - - assert response.body == @body - - assert {:ok, response} = - Strategy.decode_response( - %HTTPResponse{ - body: @json_encoded_body, - headers: [{"content-type", "text/javascript"}] - }, - [] - ) - - assert response.body == @body - - assert {:ok, response} = - Strategy.decode_response( - %HTTPResponse{ - body: @uri_encoded_body, - headers: [{"content-type", "application/x-www-form-urlencoded"}] - }, - [] - ) - - assert response.body == @body - - assert {:ok, response} = Strategy.decode_response(%HTTPResponse{body: @body, headers: []}, []) - assert response.body == @body - - assert {:error, %InvalidResponseError{} = error} = - Strategy.decode_response(%HTTPResponse{body: "%", headers: @headers}, []) - - assert error.response.body == "%" + defmodule HTTPMock do + def request(:get, _, _, _, _), do: {:error, __MODULE__} end defmodule JSONMock do def decode(_string), do: {:ok, :decoded} end - test "decode_json/2" do - assert Strategy.decode_json("{\"a\": 1}", []) == {:ok, %{"a" => 1}} - assert Strategy.decode_json("{\"a\": 1}", json_library: JSONMock) == {:ok, :decoded} - end - - defmodule HTTPMock do - @json_library (Code.ensure_loaded?(JSON) && JSON) || Jason - - def request(:get, "http-adapter", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [], body: nil}} - end - - def request(:get, "http-adapter-with-opts", nil, [], opts) do - {:ok, %HTTPResponse{status: 200, headers: [], body: opts}} - end - - def request(:get, "json-encoded-body", nil, [], nil) do - {:ok, - %HTTPResponse{ - status: 200, - headers: [{"content-type", "application/json"}], - body: @json_library.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} - }} - end - - def request(:get, "json-encoded-body-text/javascript-header", nil, [], nil) do - {:ok, - %HTTPResponse{ - status: 200, - headers: [{"content-type", "text/javascript"}], - body: @json_library.encode!(%{"a" => 1}) - }} - end - - def request(:get, "invalid-json-body", nil, [], nil) do - {:ok, - %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: "%"}} - end - - def request(:get, "json-no-headers", nil, [], nil) do - {:ok, %HTTPResponse{status: 200, headers: [], body: @json_library.encode!(%{"a" => 1})}} - 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}) - }} - 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} - }} - 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" - }} - - 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" - }} - - assert {:error, %InvalidResponseError{}} = - 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: @json_library.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" - }} - end - - defmodule CustomJWTAdapter do + defmodule JWTMock do @moduledoc false - def sign(_claims, _alg, _secret, _opts), do: :signed + def sign(_claims, _alg, _secret, opts), do: {:error, opts} - def verify(_binary, _secret, _opts), do: :verified + def verify(_binary, _secret, opts), do: {:error, opts} end - defmodule CustomJSONLibrary do - @moduledoc false - - def decode(_binary), do: {:ok, %{"alg" => "none", "custom_json" => true}} + test "http_request/5" do + config = [http_adapter: HTTPMock, json_library: JSONMock] - def encode!(_any), do: "" + assert {:error, error} = Strategy.http_request(:get, "/path", nil, [], config) + assert error.reason == HTTPMock end - @claims %{"iat" => 1_516_239_022, "name" => "John Doe", "sub" => "1234567890"} - @alg "HS256" - @secret "your-256-bit-secret" - @token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMzQ1Njc4OTAifQ.fdOPQ05ZfRhkST2-rIWgUpbqUsVhkkNVNcuG7Ki0s-8" - - @empty_encoding Base.url_encode64("", padding: false) + test "sign_jwt/4" do + config = [json_library: JSONMock, jwt_adapter: JWTMock, private_key: "myprivatekey.pem", a: 1] - test "sign_jwt/2" do - assert Strategy.sign_jwt(@claims, @alg, @secret, []) == {:ok, @token} + assert {:error, opts} = Strategy.sign_jwt(%{"claim" => "a"}, "alg", "secret", config) + assert opts == [json_library: JSONMock, jwt_adapter: JWTMock] + end - assert Strategy.sign_jwt(@token, @alg, @secret, jwt_adapter: CustomJWTAdapter) == :signed + test "verify_jwt/4" do + config = [json_library: JSONMock, jwt_adapter: JWTMock, private_key: "myprivatekey.pem", a: 1] - assert {:ok, @empty_encoding <> "." <> _rest} = - Strategy.sign_jwt(@token, @alg, @secret, json_library: CustomJSONLibrary) + assert {:error, opts} = Strategy.verify_jwt("token", "secret", config) + assert opts == [json_library: JSONMock, jwt_adapter: JWTMock] 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) + test "decode_json/2" do + assert Strategy.decode_json("{\"a\": 1}", []) == {:ok, %{"a" => 1}} + assert Strategy.decode_json("{\"a\": 1}", json_library: JSONMock) == {:ok, :decoded} end test "to_url/3" do diff --git a/test/assent_test.exs b/test/assent_test.exs index e143805..756ce06 100644 --- a/test/assent_test.exs +++ b/test/assent_test.exs @@ -1,4 +1,26 @@ defmodule AssentTest do use Assent.TestCase doctest Assent + + test "fetch_config/2" do + config = [a: 1, b: 2] + + assert Assent.fetch_config(config, :a) == {:ok, 1} + + assert {:error, %Assent.MissingConfigError{} = error} = Assent.fetch_config(config, :c) + assert error.key == :c + assert error.config == config + assert Exception.message(error) == "Expected :c in config, got: [:a, :b]" + end + + test "fetch_param/2" do + params = %{"a" => 1, "b" => 2} + + assert Assent.fetch_param(params, "a") == {:ok, 1} + + assert {:error, %Assent.MissingParamError{} = error} = Assent.fetch_param(params, "c") + assert error.key == "c" + assert error.params == params + assert Exception.message(error) == "Expected \"c\" in params, got: [\"a\", \"b\"]" + end end