defmodule ShinAuth.OIDC.ProviderConfiguration.Client do @moduledoc false alias ShinAuth.OIDC.ProviderConfiguration.Error alias ShinAuth.OIDC.ProviderConfiguration.Metadata def fetch_discovery_metadata(discovery_endpoint, config \\ default_config()) do http_client = get_http_client(config) discovery_endpoint |> http_client.get() |> handle_discovery_metadata() end defp handle_discovery_metadata({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do {:ok, struct( Metadata, Poison.decode!(body) |> Enum.map(fn {key, value} -> {key |> to_string() |> Macro.underscore() |> String.to_atom(), value} end) )} end defp handle_discovery_metadata({:ok, %HTTPoison.Response{body: _, status_code: status_code}}), do: {:error, %Error{ tag: :discovery_endpoint_unreachable, message: "Discovery endpoint is unreachable. Received status code: #{status_code}." }} defp handle_discovery_metadata(_), do: {:error, %Error{ tag: :discovery_endpoint_unreachable, message: "Discovery endpoint is unreachable. Verify if the endpoint is structured correctly." }} def fetch_authorization_endpoint( %Metadata{authorization_endpoint: authorization_endpoint} = metadata, config \\ default_config() ) when is_binary(authorization_endpoint) do http_client = get_http_client(config) response = authorization_endpoint |> http_client.get( [{"Content-Type", "application/json"}, {"Accept", "application/json"}], timeout: 5000 ) error = {:error, %Error{ tag: :authorization_endpoint_unreachable, message: "'authorization_code' is unreachable. Verify if the endpoint is structured correctly." }} case response do {:ok, %HTTPoison.Response{body: _body, status_code: 500}} -> error {:error, _} -> error {:ok, %HTTPoison.Response{body: _body, status_code: _}} -> {:ok, metadata} end end def fetch_token_endpoint({:error, _} = error), do: error def fetch_token_endpoint( {:ok, %Metadata{token_endpoint: token_endpoint} = metadata}, config \\ default_config() ) when is_binary(token_endpoint) do http_client = get_http_client(config) response = token_endpoint |> http_client.post( "", [{"Content-Type", "application/json"}, {"Accept", "application/json"}], timeout: 5000 ) error = {:error, %Error{ tag: :token_endpoint_unreachable, message: "'token_endpoint' is unreachable. Verify if the endpoint is structured correctly." }} case response do {:ok, %HTTPoison.Response{body: _body, status_code: 500}} -> error {:error, _} -> error {:ok, %HTTPoison.Response{body: _body, status_code: _}} -> {:ok, metadata} end end def fetch_jwks_uri({:error, _} = error), do: error def fetch_jwks_uri( {:ok, %Metadata{jwks_uri: jwks_uri} = metadata}, config \\ default_config() ) when is_binary(jwks_uri) do http_client = get_http_client(config) response = jwks_uri |> http_client.get( [{"Content-Type", "application/json"}, {"Accept", "application/json"}], timeout: 5000 ) case response do {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> data = Poison.decode!(body) case valid_jwk_response?(data) do true -> {:ok, metadata} false -> {:error, %Error{ tag: :malformed_jwks_uri_response, message: "JWKS URI response is malformed.", data: data }} end _ -> {:error, %Error{ tag: :jwks_uri_unreachable, message: "'jwks_uri' is unreachable. Verify if the endpoint is structured correctly." }} end end defp valid_jwk_response?(%{"keys" => keys}) when is_list(keys), do: Enum.all?(keys, &validate_jwk_keys/1) defp valid_jwk_response?(response) when is_map(response), do: validate_jwk_keys(response) defp valid_jwk_response?(_), do: false defp validate_jwk_keys(map) do Enum.all?(Map.keys(map), fn key -> value = Map.fetch!(map, key) is_binary(value) or (is_list(value) and Enum.all?(value, &is_binary/1)) end) end defp default_config, do: Application.get_env(:shin_auth, :provider_configuration_fetcher) defp get_http_client(config) do Keyword.get(config, :http_client, HTTPoison) end end