diff --git a/lib/assent/jwt_adapter.ex b/lib/assent/jwt_adapter.ex index 3f65d6c..163952f 100644 --- a/lib/assent/jwt_adapter.ex +++ b/lib/assent/jwt_adapter.ex @@ -42,7 +42,7 @@ defmodule Assent.JWTAdapter do """ @spec sign(map(), binary(), binary(), Keyword.t()) :: {:ok, binary()} | {:error, term()} def sign(claims, alg, secret, opts \\ []) do - {adapter, opts} = fetch_adapter(opts) + {adapter, opts} = get_adapter(opts) adapter.sign(claims, alg, secret, opts) end @@ -51,11 +51,11 @@ defmodule Assent.JWTAdapter do """ @spec verify(binary(), binary() | map() | nil, Keyword.t()) :: {:ok, map()} | {:error, any()} def verify(token, secret, opts \\ []) do - {adapter, opts} = fetch_adapter(opts) + {adapter, opts} = get_adapter(opts) adapter.verify(token, secret, opts) end - defp fetch_adapter(opts) do + 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) diff --git a/lib/assent/strategies/oauth.ex b/lib/assent/strategies/oauth.ex index 050d34f..d40f823 100644 --- a/lib/assent/strategies/oauth.ex +++ b/lib/assent/strategies/oauth.ex @@ -81,26 +81,14 @@ defmodule Assent.Strategy.OAuth do @impl true @spec authorize_url(Config.t()) :: on_authorize_url() def authorize_url(config) do - case Config.fetch(config, :redirect_uri) do - {:ok, redirect_uri} -> authorize_url(config, redirect_uri) - {:error, error} -> {:error, error} + with {:ok, redirect_uri} <- Config.fetch(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}}} end end - defp authorize_url(config, redirect_uri) do - config - |> get_request_token([{"oauth_callback", redirect_uri}]) - |> build_authorize_url(config) - |> case do - {:ok, url, oauth_token_secret} -> - {:ok, %{url: url, session_params: %{oauth_token_secret: oauth_token_secret}}} - - {:error, error} -> - {:error, error} - end - end - - defp get_request_token(config, oauth_params) 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") url = process_url(base_url, request_token_url) @@ -158,12 +146,16 @@ defmodule Assent.Strategy.OAuth do defp gen_oauth_params(config, signature_method, oauth_params) do with {:ok, consumer_key} <- Config.fetch(config, :consumer_key) do + nonce = gen_nonce() + signature_method = signature_method_value(signature_method) + timestamp = to_string(:os.system_time(:second)) + params = [ {"oauth_consumer_key", consumer_key}, - {"oauth_nonce", gen_nonce()}, - {"oauth_signature_method", to_signature_method_string(signature_method)}, - {"oauth_timestamp", timestamp()}, + {"oauth_nonce", nonce}, + {"oauth_signature_method", signature_method}, + {"oauth_timestamp", timestamp}, {"oauth_version", "1.0"} | oauth_params ] @@ -172,6 +164,16 @@ defmodule Assent.Strategy.OAuth do end end + defp gen_nonce do + 16 + |> :crypto.strong_rand_bytes() + |> Base.encode64(padding: false) + end + + defp signature_method_value(:hmac_sha1), do: "HMAC-SHA1" + defp signature_method_value(:rsa_sha1), do: "RSA-SHA1" + defp signature_method_value(:plaintext), do: "PLAINTEXT" + defp signed_header(config, signature_method, method, url, oauth_params, params, token_secret) do uri = URI.parse(url) query_params = Map.to_list(URI.decode_query(uri.query || "")) @@ -188,25 +190,13 @@ defmodule Assent.Strategy.OAuth do end end - defp gen_nonce do - 16 - |> :crypto.strong_rand_bytes() - |> Base.encode64(padding: false) - end - - defp to_signature_method_string(:hmac_sha1), do: "HMAC-SHA1" - defp to_signature_method_string(:rsa_sha1), do: "RSA-SHA1" - defp to_signature_method_string(:plaintext), do: "PLAINTEXT" - - defp timestamp, do: to_string(:os.system_time(:second)) - defp gen_signature(config, method, uri, request_params, :hmac_sha1, token_secret) do with {:ok, shared_secret} <- encoded_shared_secret(config, token_secret) do - text = signature_base_string(method, uri, request_params) + signature_base = encode_signature_base(method, uri, request_params) signature = :hmac - |> :crypto.mac(:sha, shared_secret, text) + |> :crypto.mac(:sha, shared_secret, signature_base) |> Base.encode64() {:ok, signature} @@ -218,7 +208,7 @@ defmodule Assent.Strategy.OAuth do {:ok, private_key} <- decode_pem(pem) do signature = method - |> signature_base_string(uri, request_params) + |> encode_signature_base(uri, request_params) |> :public_key.sign(:sha, private_key) |> Base.encode64() @@ -243,7 +233,7 @@ defmodule Assent.Strategy.OAuth do |> URI.encode(&URI.char_unreserved?/1) end - defp signature_base_string(method, uri, request_params) do + defp encode_signature_base(method, uri, request_params) do method = method |> to_string() @@ -308,7 +298,7 @@ defmodule Assent.Strategy.OAuth do defp process_response({:error, error}), do: {:error, error} - defp build_authorize_url({:ok, token}, config) do + defp gen_authorize_url(config, token) 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 @@ -320,8 +310,6 @@ defmodule Assent.Strategy.OAuth do end end - defp build_authorize_url({:error, error}, _config), do: {:error, error} - defp fetch_from_token(token, key) do case Map.fetch(token, key) do {:ok, value} -> {:ok, value} @@ -350,25 +338,22 @@ defmodule Assent.Strategy.OAuth do @impl true @spec callback(Config.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do - with {:ok, oauth_token} <- fetch_oauth_token(params), - {:ok, oauth_verifier} <- fetch_oauth_verifier(params), - {:ok, token} <- get_access_token(config, oauth_token, oauth_verifier), + with {:ok, oauth_token} <- fetch_param(params, "oauth_token"), + {:ok, oauth_verifier} <- 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_oauth_token(%{"oauth_token" => code}), do: {:ok, code} - - defp fetch_oauth_token(params), - do: {:error, MissingParamError.exception(expected_key: "oauth_token", params: params)} - - defp fetch_oauth_verifier(%{"oauth_verifier" => code}), do: {:ok, code} - - defp fetch_oauth_verifier(params), - do: {:error, MissingParamError.exception(expected_key: "oauth_verifier", params: params)} + 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 get_access_token(config, oauth_token, oauth_verifier) do + 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") url = process_url(base_url, access_token_url) @@ -416,16 +401,16 @@ defmodule Assent.Strategy.OAuth do @spec fetch_user(Config.t(), map()) :: {:ok, map()} | {:error, term()} def fetch_user(config, token) do with {:ok, url} <- Config.fetch(config, :user_url) do - config - |> request(token, :get, url) - |> process_user_response() - end - end - - defp process_user_response({:ok, %HTTPResponse{status: 200, body: user}}), do: {:ok, user} + case request(config, token, :get, url) do + {:ok, %HTTPResponse{status: 200, body: user}} -> + {:ok, user} - defp process_user_response({:error, %HTTPResponse{status: 401} = response}), - do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} + {:error, %HTTPResponse{status: 401} = response} -> + {:error, RequestError.exception(message: "Unauthorized token", response: response)} - defp process_user_response(any), do: process_response(any) + other -> + process_response(other) + end + end + end end diff --git a/lib/assent/strategies/oauth2.ex b/lib/assent/strategies/oauth2.ex index 5781ddf..61158aa 100644 --- a/lib/assent/strategies/oauth2.ex +++ b/lib/assent/strategies/oauth2.ex @@ -271,6 +271,27 @@ defmodule Assent.Strategy.OAuth2 do end end + @doc """ + Grants an access token. + """ + @spec grant_access_token(Config.t(), binary(), Keyword.t()) :: {:ok, map()} | {:error, term()} + def grant_access_token(config, grant_type, params) do + auth_method = Config.get(config, :auth_method, nil) + token_url = Config.get(config, :token_url, "/oauth/token") + + with {:ok, base_url} <- Config.__base_url__(config), + {:ok, auth_headers, auth_body} <- authentication_params(auth_method, config) do + headers = [{"content-type", "application/x-www-form-urlencoded"}] ++ auth_headers + params = Keyword.merge(params, Keyword.put(auth_body, :grant_type, grant_type)) + url = Helpers.to_url(base_url, token_url) + body = URI.encode_query(params) + + :post + |> Helpers.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 headers = [] @@ -352,27 +373,6 @@ defmodule Assent.Strategy.OAuth2 do end end - @doc """ - Grants an access token. - """ - @spec grant_access_token(Config.t(), binary(), Keyword.t()) :: {:ok, map()} | {:error, term()} - def grant_access_token(config, grant_type, params) do - auth_method = Config.get(config, :auth_method, nil) - token_url = Config.get(config, :token_url, "/oauth/token") - - with {:ok, base_url} <- Config.__base_url__(config), - {:ok, auth_headers, auth_body} <- authentication_params(auth_method, config) do - headers = [{"content-type", "application/x-www-form-urlencoded"}] ++ auth_headers - params = Keyword.merge(params, Keyword.put(auth_body, :grant_type, grant_type)) - url = Helpers.to_url(base_url, token_url) - body = URI.encode_query(params) - - :post - |> Helpers.request(url, body, headers, config) - |> process_access_token_response() - end - end - defp process_access_token_response( {:ok, %HTTPResponse{status: status, body: %{"access_token" => _} = token}} ) @@ -420,7 +420,7 @@ defmodule Assent.Strategy.OAuth2 do {:ok, map()} | {:error, term()} def request(config, token, method, url, params \\ [], headers \\ []) do with {:ok, base_url} <- Config.__base_url__(config), - {:ok, auth_headers} <- authorization_headers(config, token) do + {:ok, auth_headers} <- authorization_headers(token) do req_headers = request_headers(method, auth_headers ++ headers) req_body = request_body(method, params) params = url_params(method, params) @@ -430,6 +430,30 @@ defmodule Assent.Strategy.OAuth2 do end end + defp authorization_headers(token) do + token + |> Map.get("token_type", "Bearer") + |> String.downcase() + |> authorization_headers(token) + end + + defp authorization_headers("bearer", token) do + with {:ok, access_token} <- fetch_from_token(token, "access_token") do + {:ok, [{"authorization", "Bearer #{access_token}"}]} + end + end + + defp authorization_headers(type, _token) do + {:error, "Authorization with token type `#{type}` not supported"} + end + + defp fetch_from_token(token, key) do + case Map.fetch(token, key) do + {:ok, value} -> {:ok, value} + :error -> {:error, "No `#{key}` in token map"} + end + end + defp request_headers(:post, headers), do: [{"content-type", "application/x-www-form-urlencoded"}] ++ headers @@ -450,42 +474,16 @@ defmodule Assent.Strategy.OAuth2 do {:ok, map()} | {:error, term()} def fetch_user(config, token, params \\ [], headers \\ []) do with {:ok, user_url} <- Config.fetch(config, :user_url) do - config - |> request(token, :get, user_url, params, headers) - |> process_user_response() - end - end + case request(config, token, :get, user_url, params, headers) do + {:ok, %HTTPResponse{status: 200, body: user}} -> + {:ok, user} - defp authorization_headers(config, token) do - type = - token - |> Map.get("token_type", "Bearer") - |> String.downcase() + {:error, %HTTPResponse{status: 401} = response} -> + {:error, RequestError.exception(message: "Unauthorized token", response: response)} - authorization_headers(config, token, type) - end - - defp authorization_headers(_config, token, "bearer") do - with {:ok, access_token} <- fetch_from_token(token, "access_token") do - {:ok, [{"authorization", "Bearer #{access_token}"}]} + other -> + process_response(other) + end end end - - defp authorization_headers(_config, _token, type) do - {:error, "Authorization with token type `#{type}` not supported"} - end - - defp fetch_from_token(token, key) do - case Map.fetch(token, key) do - {:ok, value} -> {:ok, value} - :error -> {:error, "No `#{key}` in token map"} - end - end - - defp process_user_response({:ok, %HTTPResponse{status: 200, body: user}}), do: {:ok, user} - - defp process_user_response({:error, %HTTPResponse{status: 401} = response}), - do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} - - defp process_user_response(any), do: process_response(any) end diff --git a/lib/assent/strategies/oidc.ex b/lib/assent/strategies/oidc.ex index 4f09d54..292d9ab 100644 --- a/lib/assent/strategies/oidc.ex +++ b/lib/assent/strategies/oidc.ex @@ -116,10 +116,10 @@ defmodule Assent.Strategy.OIDC do @impl true @spec authorize_url(Config.t()) :: on_authorize_url() def authorize_url(config) do - with {:ok, openid_config} <- openid_configuration(config), + with {:ok, openid_config} <- fetch_openid_configuration(config), {:ok, authorize_url} <- fetch_from_openid_config(openid_config, "authorization_endpoint"), - {:ok, params} <- authorization_params(config) do + {:ok, params} <- fetch_authorization_params(config) do config |> Config.put(:authorization_params, params) |> Config.put(:authorize_url, authorize_url) @@ -128,41 +128,35 @@ defmodule Assent.Strategy.OIDC do end end - defp openid_configuration(config) do + defp fetch_openid_configuration(config) do case Config.get(config, :openid_configuration, nil) do - nil -> fetch_openid_configuration(config) + nil -> fetch_openid_configuration_from_uri(config) openid_config -> {:ok, openid_config} end end - defp fetch_openid_configuration(config) 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") url = Helpers.to_url(base_url, configuration_url) - :get - |> Helpers.request(url, nil, [], config) - |> process_openid_configuration_response() - end - end - - defp process_openid_configuration_response( - {:ok, %HTTPResponse{status: 200, body: configuration}} - ) do - {:ok, configuration} - end - - defp process_openid_configuration_response(any), do: process_response(any) + case Helpers.request(:get, url, nil, [], config) do + {:ok, %HTTPResponse{status: 200, body: configuration}} -> + {:ok, configuration} - defp process_response({:ok, %HTTPResponse{} = response}), - do: {:error, UnexpectedResponseError.exception(response: response)} + {:ok, %HTTPResponse{} = response} -> + {:error, UnexpectedResponseError.exception(response: response)} - defp process_response({:error, %HTTPResponse{} = response}), - do: {:error, InvalidResponseError.exception(response: response)} + {:error, %HTTPResponse{} = response} -> + {:error, InvalidResponseError.exception(response: response)} - defp process_response({:error, error}), do: {:error, error} + {:error, error} -> + {:error, error} + end + end + end defp fetch_from_openid_config(config, key) do case Map.fetch(config, key) do @@ -171,7 +165,7 @@ defmodule Assent.Strategy.OIDC do end end - defp authorization_params(config) do + defp fetch_authorization_params(config) do new_params = config |> Config.get(:authorization_params, []) @@ -232,7 +226,7 @@ defmodule Assent.Strategy.OIDC do @impl true @spec callback(Config.t(), map(), atom()) :: on_callback() def callback(config, params, strategy \\ __MODULE__) do - with {:ok, openid_config} <- openid_configuration(config), + 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 @@ -246,22 +240,21 @@ defmodule Assent.Strategy.OIDC do defp fetch_client_authentication_method(openid_config, config) do method = Config.get(config, :client_authentication_method, "client_secret_basic") methods = Map.get(openid_config, "token_endpoint_auth_methods_supported") - - supported_method? = if is_nil(methods), do: true, else: method in methods + supported_method? = (is_nil(methods) && true) || method in methods case supported_method? do - true -> to_client_auth_method(method) + true -> parse_client_auth_method(method) false -> {:error, "Unsupported client authentication method: #{method}"} end end - defp to_client_auth_method("none"), do: {:ok, nil} - defp to_client_auth_method("client_secret_basic"), do: {:ok, :client_secret_basic} - defp to_client_auth_method("client_secret_post"), do: {:ok, :client_secret_post} - defp to_client_auth_method("client_secret_jwt"), do: {:ok, :client_secret_jwt} - defp to_client_auth_method("private_key_jwt"), do: {:ok, :private_key_jwt} + defp parse_client_auth_method("none"), do: {:ok, nil} + defp parse_client_auth_method("client_secret_basic"), do: {:ok, :client_secret_basic} + defp parse_client_auth_method("client_secret_post"), do: {:ok, :client_secret_post} + defp parse_client_auth_method("client_secret_jwt"), do: {:ok, :client_secret_jwt} + defp parse_client_auth_method("private_key_jwt"), do: {:ok, :private_key_jwt} - defp to_client_auth_method(method), + defp parse_client_auth_method(method), do: {:error, "Invalid client authentication method: #{method}"} # https://openid.net/specs/draft-jones-json-web-token-07.html#ReservedClaimName @@ -311,7 +304,7 @@ defmodule Assent.Strategy.OIDC do def validate_id_token(config, id_token) do expected_alg = Config.get(config, :id_token_signed_response_alg, "RS256") - with {:ok, openid_config} <- openid_configuration(config), + with {:ok, openid_config} <- fetch_openid_configuration(config), {:ok, client_id} <- Config.fetch(config, :client_id), {:ok, issuer} <- fetch_from_openid_config(openid_config, "issuer"), {:ok, jwt} <- verify_jwt(id_token, openid_config, config), @@ -370,16 +363,23 @@ defmodule Assent.Strategy.OIDC do end defp fetch_public_keys(uri, config) do - :get - |> Helpers.request(uri, nil, [], config) - |> process_public_keys_response() - end + case Helpers.request(:get, uri, nil, [], config) do + {:ok, %HTTPResponse{status: 200, body: %{"keys" => keys}}} -> + {:ok, keys} - defp process_public_keys_response({:ok, %HTTPResponse{status: 200, body: %{"keys" => keys}}}), - do: {:ok, keys} + {:ok, %HTTPResponse{status: 200}} -> + {:ok, []} - defp process_public_keys_response({:ok, %HTTPResponse{status: 200}}), do: {:ok, []} - defp process_public_keys_response(any), do: process_response(any) + {:ok, %HTTPResponse{} = response} -> + {:error, UnexpectedResponseError.exception(response: response)} + + {:error, %HTTPResponse{} = response} -> + {:error, InvalidResponseError.exception(response: response)} + + {:error, error} -> + {:error, error} + end + end defp find_key(%{"kid" => kid}, [%{"kid" => kid} = key | _keys]), do: {:ok, key} @@ -508,7 +508,7 @@ defmodule Assent.Strategy.OIDC do """ @spec fetch_userinfo(Config.t(), map()) :: {:ok, map()} | {:error, term()} def fetch_userinfo(config, token) do - with {:ok, openid_config} <- openid_configuration(config), + with {:ok, openid_config} <- fetch_openid_configuration(config), {:ok, userinfo_url} <- fetch_from_openid_config(openid_config, "userinfo_endpoint"), {:ok, claims} <- fetch_from_userinfo_endpoint(config, openid_config, token, userinfo_url), @@ -518,32 +518,31 @@ defmodule Assent.Strategy.OIDC do end defp fetch_from_userinfo_endpoint(config, openid_config, token, userinfo_url) do - config - |> OAuth2.request(token, :get, userinfo_url) - |> process_userinfo_response(openid_config, config) + case OAuth2.request(config, token, :get, userinfo_url) do + {:ok, %HTTPResponse{status: 200, body: body, headers: headers}} -> + maybe_validate_jwt_header(config, openid_config, headers, body) + + {:error, %HTTPResponse{status: 401} = response} -> + {:error, RequestError.exception(message: "Unauthorized token", response: response)} + + {:ok, %HTTPResponse{} = response} -> + {:error, UnexpectedResponseError.exception(response: response)} + + {:error, %HTTPResponse{} = response} -> + {:error, InvalidResponseError.exception(response: response)} + + {:error, error} -> + {:error, error} + end end - defp process_userinfo_response( - {:ok, %HTTPResponse{status: 200, body: body, headers: headers}}, - openid_config, - config - ) do + defp maybe_validate_jwt_header(config, openid_config, headers, body) do case List.keyfind(headers, "content-type", 0) do {"content-type", "application/jwt" <> _rest} -> process_jwt(body, openid_config, config) _any -> {:ok, body} end end - defp process_userinfo_response( - {:error, %HTTPResponse{status: 401} = response}, - _openid_config, - _config - ) do - {:error, RequestError.exception(message: "Unauthorized token", response: response)} - end - - defp process_userinfo_response(any, _openid_config, _config), do: process_response(any) - defp process_jwt(body, openid_config, config) do with {:ok, jwt} <- verify_jwt(body, openid_config, config), :ok <- validate_verified(jwt) do diff --git a/lib/assent/strategy.ex b/lib/assent/strategy.ex index 44bb622..b715cad 100644 --- a/lib/assent/strategy.ex +++ b/lib/assent/strategy.ex @@ -39,12 +39,32 @@ defmodule Assent.Strategy do @spec request(atom(), binary(), binary() | nil, list(), Config.t()) :: {:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()} def request(method, url, body, headers, config) do - {http_adapter, opts} = fetch_http_adapter(config) + {http_adapter, opts} = get_http_adapter(config) method |> http_adapter.request(url, body, headers, opts) - |> parse_status_response(http_adapter, url) - |> decode_response(config) + |> case do + {:ok, response} -> + decode_response({:ok, 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( @@ -57,7 +77,7 @@ defmodule Assent.Strategy do end ) - defp fetch_http_adapter(config) do + 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 @@ -66,21 +86,6 @@ defmodule Assent.Strategy do end end - defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) - when status in 200..399 do - {:ok, %{resp | http_adapter: http_adapter, request_url: url}} - end - - defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) - when status in 400..599 do - {:error, %{resp | http_adapter: http_adapter, request_url: url}} - end - - defp parse_status_response({:error, error}, http_adapter, url) do - {:error, - ServerUnreachableError.exception(reason: error, http_adapter: http_adapter, request_url: url)} - end - @doc """ Decodes a request response. """ diff --git a/test/assent/http_adapter/httpc_test.exs b/test/assent/http_adapter/httpc_test.exs index ae8b19f..3340a63 100644 --- a/test/assent/http_adapter/httpc_test.exs +++ b/test/assent/http_adapter/httpc_test.exs @@ -40,7 +40,7 @@ defmodule Assent.HTTPAdapter.HttpcTest do assert {:error, {:failed_connect, error}} = Httpc.request(:get, bad_host_url, nil, [], httpc_opts) - assert {:tls_alert, {:handshake_failure, _error}} = fetch_inet_error(error) + assert {:tls_alert, {:handshake_failure, _error}} = inet_error(error) end test "handles SSL with bad certificate and no verification" do @@ -85,7 +85,7 @@ defmodule Assent.HTTPAdapter.HttpcTest do TestServer.stop() assert {:error, {:failed_connect, error}} = Httpc.request(:get, url, nil, []) - assert fetch_inet_error(error) == :econnrefused + assert inet_error(error) == :econnrefused end test "handles query in URL" do @@ -129,7 +129,7 @@ defmodule Assent.HTTPAdapter.HttpcTest do end end - defp fetch_inet_error([_, {:inet, [:inet], error}]), do: error + defp inet_error([_, {:inet, [:inet], error}]), do: error defp request_with_deps(deps) do deps = deps ++ ["{:assent, path: \"../../\"}"] diff --git a/test/assent/strategies/oauth_test.exs b/test/assent/strategies/oauth_test.exs index fbc1266..06b25b6 100644 --- a/test/assent/strategies/oauth_test.exs +++ b/test/assent/strategies/oauth_test.exs @@ -7,6 +7,7 @@ defmodule Assent.Strategy.OAuthTest do Config.MissingKeyError, InvalidResponseError, MissingParamError, + RequestError, ServerUnreachableError, Strategy.OAuth } @@ -81,7 +82,7 @@ defmodule Assent.Strategy.OAuthTest do assert error.key == :consumer_secret end - test "with unreachable request token url", %{config: config} do + test "with `:request_token_url` being unreachable", %{config: config} do request_token_url = TestServer.url("/request_token") TestServer.stop() @@ -92,7 +93,7 @@ defmodule Assent.Strategy.OAuthTest do assert {:failed_connect, _} = error.reason end - test "with unexpected successful response", %{config: config} do + test "with `:request_token_url` returning unexpected success", %{config: config} do expect_oauth_request_token_request( params: %{"error_code" => 215, "error_message" => "Bad Authentication data."} ) @@ -109,7 +110,7 @@ defmodule Assent.Strategy.OAuthTest do } end - test "with error response", %{config: config} do + test "with `:request_token_url` returning HTTP error", %{config: config} do expect_oauth_request_token_request( status_code: 500, params: %{"error_code" => 215, "error_message" => "Bad Authentication data."} @@ -127,22 +128,7 @@ defmodule Assent.Strategy.OAuthTest do } end - test "with json error response", %{config: config} do - expect_oauth_request_token_request( - status_code: 500, - content_type: "application/json", - params: %{"errors" => [%{"code" => 215, "message" => "Bad Authentication data."}]} - ) - - assert {:error, %InvalidResponseError{} = error} = OAuth.authorize_url(config) - assert error.response.status == 500 - - assert error.response.body == %{ - "errors" => [%{"code" => 215, "message" => "Bad Authentication data."}] - } - end - - test "with missing `oauth_token` in access token response", %{config: config} do + test "with `:request_token_url` returning missing `oauth_token`", %{config: config} do expect_oauth_request_token_request(params: %{oauth_token_secret: "hdhd0244k9j7ao03"}) assert {:error, %UnexpectedResponseError{} = error} = OAuth.authorize_url(config) @@ -150,7 +136,7 @@ defmodule Assent.Strategy.OAuthTest do assert error.response.body == %{"oauth_token_secret" => "hdhd0244k9j7ao03"} end - test "with missing `oauth_token_secret` in access token response", %{config: config} do + test "with `:request_token_url` returning missing `oauth_token_secret`", %{config: config} do expect_oauth_request_token_request(params: %{oauth_token: "hh5s93j4hdidpola"}) assert {:error, %UnexpectedResponseError{} = error} = OAuth.authorize_url(config) @@ -190,7 +176,7 @@ defmodule Assent.Strategy.OAuthTest do assert url == TestServer.url("/authorize?oauth_token=hh5s93j4hdidpola") end - test "parses URI query params in `:request_token_url` for the signature", %{config: config} do + test "with `:request_token_url` URI query params", %{config: config} do config = Keyword.put(config, :request_token_url, "/request_token?a=1&c=3&b=2") request_token_url = TestServer.url("/request_token") @@ -213,7 +199,7 @@ defmodule Assent.Strategy.OAuthTest do assert {:ok, _res} = OAuth.authorize_url(config) end - test "parses URI query response with authorization params", %{config: config} do + test "with `:authorization_params`", %{config: config} do authorization_params = [scope: "reading writing", another_param: "param"] config = Keyword.put(config, :authorization_params, authorization_params) expect_oauth_request_token_request() @@ -227,7 +213,7 @@ defmodule Assent.Strategy.OAuthTest do ) end - test "parses URI query response", %{config: config} do + test "with `:request_token_url` returning URI query encoded response", %{config: config} do expect_oauth_request_token_request( content_type: "text/html", params: @@ -377,7 +363,7 @@ defmodule Assent.Strategy.OAuthTest do {:ok, config: config} end - test "with missing oauth_token param", %{config: config, callback_params: params} do + test "with missing `oauth_token` param", %{config: config, callback_params: params} do params = Map.delete(params, "oauth_token") assert {:error, %MissingParamError{} = error} = OAuth.callback(config, params) @@ -389,7 +375,7 @@ defmodule Assent.Strategy.OAuthTest do assert error.params == %{"oauth_verifier" => "hfdp7dh39dks9884"} end - test "with missing oauth_verifier param", %{config: config, callback_params: params} do + test "with missing `oauth_verifier` param", %{config: config, callback_params: params} do params = Map.delete(params, "oauth_verifier") assert {:error, %MissingParamError{} = error} = OAuth.callback(config, params) @@ -408,7 +394,10 @@ defmodule Assent.Strategy.OAuthTest do assert error.key == :base_url end - test "with unreachable token url", %{config: config, callback_params: callback_params} do + test "with `:access_token_url` being unreachable", %{ + config: config, + callback_params: callback_params + } do access_token_url = TestServer.url("/access_token") TestServer.stop() @@ -419,7 +408,10 @@ defmodule Assent.Strategy.OAuthTest do assert {:failed_connect, _} = error.reason end - test "with token url error response", %{config: config, callback_params: callback_params} do + test "with `:access_token_url` returning error", %{ + config: config, + callback_params: callback_params + } do expect_oauth_access_token_request(status_code: 500, params: %{error: "Unknown error"}) assert {:error, %InvalidResponseError{} = error} = OAuth.callback(config, callback_params) @@ -427,7 +419,7 @@ defmodule Assent.Strategy.OAuthTest do assert error.response.body == %{"error" => "Unknown error"} end - test "with missing `oauth_token` in access token response", %{ + test "with `:access_token_url` returning missing `oauth_token`", %{ config: config, callback_params: callback_params } do @@ -440,7 +432,7 @@ defmodule Assent.Strategy.OAuthTest do assert error.response.body == %{"oauth_token_secret" => "token_secret"} end - test "with missing `oauth_token_secret` in access token response", %{ + test "with `:access_token_url` returning missing `oauth_token_secret`", %{ config: config, callback_params: callback_params } do @@ -453,16 +445,34 @@ defmodule Assent.Strategy.OAuthTest do assert error.response.body == %{"oauth_token" => "token"} end - test "bubbles up user request error response", %{ + test "with missing `:user_url`", %{config: config, callback_params: params} do + config = Keyword.delete(config, :user_url) + + expect_oauth_access_token_request() + + assert {:error, %MissingKeyError{} = error} = OAuth.callback(config, params) + assert error.key == :user_url + end + + test "with `:user_url` being unreachable", %{config: config, callback_params: params} do + config = Keyword.put(config, :user_url, "http://localhost:8888/api/user") + + expect_oauth_access_token_request() + + assert {:error, %ServerUnreachableError{}} = OAuth.callback(config, params) + end + + test "with `:user_url` returning HTTP unauthorized", %{ config: config, - callback_params: callback_params + callback_params: params } do expect_oauth_access_token_request() - expect_oauth_user_request(%{error: "Unknown error"}, status_code: 500) + expect_oauth_user_request(%{"error" => "Unauthorized"}, status_code: 401) - assert {:error, %InvalidResponseError{} = error} = OAuth.callback(config, callback_params) - assert Exception.message(error) =~ "Response status: 500" - assert error.response.body == %{"error" => "Unknown error"} + assert {:error, %RequestError{} = error} = OAuth.callback(config, params) + assert error.message == "Unauthorized token" + assert error.response.status == 401 + assert error.response.body == %{"error" => "Unauthorized"} end test "normalizes data", %{config: config, callback_params: callback_params} do