defmodule ShinAuth.OIDC do @moduledoc """ OpenID Connect utilities. Performs decoding and validation based on the spec: https://openid.net/specs/openid-connect-discovery-1_0.html """ import ShinAuth.OIDC.ProviderConfiguration.Client alias ShinAuth.OIDC.ProviderConfiguration.Error alias ShinAuth.OIDC.ProviderConfiguration.Metadata @discovery_endpoint_path ".well-known/openid-configuration" @doc """ Loads provider configuration according to the [OpenID Connect Discovery 1.0 spec](https://openid.net/specs/openid-connect-discovery-1_0.html). """ @spec load_provider_configuration(discovery_endpoint :: :uri_string.uri_string()) :: {:ok, Metadata.t()} | {:error, Error.t()} def load_provider_configuration(discovery_endpoint) when is_binary(discovery_endpoint) do case validate_discovery_endpoint(discovery_endpoint) do {:ok, _} -> discovery_endpoint |> fetch_discovery_metadata |> validate_discovery_metadata error -> error end end defp validate_discovery_endpoint(discovery_endpoint) when is_binary(discovery_endpoint) do case URI.parse(discovery_endpoint) do %URI{scheme: nil} -> {:error, %Error{ tag: :malformed_discovery_endpoint, message: "Discovery endpoint has no URI scheme.", endpoint: discovery_endpoint }} %URI{host: nil} -> {:error, %Error{ tag: :malformed_discovery_endpoint, message: "Discovery endpoint has no URI host.", endpoint: discovery_endpoint }} _ -> {:ok, append_discovery_path(discovery_endpoint)} end end defp append_discovery_path(discovery_endpoint) do case String.ends_with?(discovery_endpoint, @discovery_endpoint_path) do true -> discovery_endpoint false -> "#{String.trim_trailing(discovery_endpoint, "/")}/#{@discovery_endpoint_path}" end end defp validate_discovery_metadata({:ok, metadata}) do validation_result = metadata |> fetch_authorization_endpoint |> validate_issuer_attribute |> fetch_jwks_uri |> fetch_token_endpoint case validation_result do {:ok, _} = parsed_metadata -> parsed_metadata error -> error end end defp validate_discovery_metadata({:error, _} = error), do: error defp validate_issuer_attribute({:ok, %Metadata{issuer: issuer}} = parsed_metadata) when is_binary(issuer), do: parsed_metadata defp validate_issuer_attribute({:ok, %Metadata{issuer: issuer}}) when is_nil(issuer) or not is_binary(issuer), do: {:error, %Error{ tag: :missing_issuer_attribute, message: "'issuer' attribute is missing. Ensure the OIDC provider supports the 'issuer' attribute." }} defp validate_issuer_attribute({:error, _} = error), do: error end