lib/google_certs.ex

defmodule GoogleCerts do
  @moduledoc """
  Context for interfacing with Google's Public certificates.

  For practical usage examples see our documentation on [how to use with the joken library](./readme.html#how-to-use-with-the-joken-library)

  See Google's documentation on
  [verify the integrity of the id token](https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token)
  for more details.
  """

  require Logger
  alias GoogleCerts.{Certificate, CertificateCache, Certificates, Client, Client.Response, Env}

  @doc """
  Returns the currently stored `GoogleCerts.Certifcates`.

  ## Examples
      iex> GoogleCerts.get()
      %GoogleCerts.Certifcates{
        algorithm: "RS256",
        certs: [
          %GoogleCerts.Certificate{
            cert: %{
              "alg" => "RS256",
              "e" => "AQAB",
              "kid" => "6fcf413224765156b48768a42fac06496a30ff5a",
              "kty" => "RSA",
              "n" => "1sUr077w2aaSnm08qFmuH1UON9e2n6vDNlUxm6WgM95n0_x1GwWTrhXtd_6U6x6R6m-50mVS_ki2BHZ9Fj3Y9W5zBww_TNyNLp4b1802gbXeGhVtQMcFQQ-hFne5HaTVTi1y6QNbu_3V1NW6nNAbpR_t79l1WzGiN4ilFiYFU0OVjk7isf7Dv3-6Trz9riHBExl34qhriu3x5pfipPT1rf4J6jMroJTEeU6L7zd9k_BwjNtptS8wAenYaK4FENR2gxvWWTX40i548Sh-3Ffprlu_9CZCswCkQCdhTq9lo3DbZYPEcW4aOLBEi3FfLiFm-DNDK_P_gBtNz8gW3VMQ2w",
              "use" => "sig"
            },
            kid: "6fcf413224765156b48768a42fac06496a30ff5a"
          },
          %GoogleCerts.Certificate{
            cert: %{
              "alg" => "RS256",
              "e" => "AQAB",
              "kid" => "257f6a5828d1e4a3a6a03fcd1a2461db9593e624",
              "kty" => "RSA",
              "n" => "kXPOxGSWngQ6Q02jhaJfzSum2FaU5_6e6irUuiwZbgUjyN2Q1VYHwuxq2o-aHqUhNPqf2cyCf2HspYwKAbeK9gFXqScrGLPW5pcquOWOVYUzPw87lBGH2fSxCYH35eB14wfLmF_im8DLTtZsaJvMRbqBgikM8Km2UA9ozjfK6E8pWW91fIT-ZF4Qy5zDkT3yX8EnAIMOuXg43v4t03FwFTyF4D9IET2ri2_n2qDhWTgtxJ0FHk3wG2KXdJIIVy2kUCTzMcZKaamRgUExt3Mu_z-2eyny8b6IdLPEIGF51VCgHebPQXE5iZmLGyw6M_pCApGJUw5GpXi6imo3pOvLjQ",
              "use" => "sig"
            },
            kid: "257f6a5828d1e4a3a6a03fcd1a2461db9593e624"
          }
        ],
        expire: "2020-04-10T03:40:42.616266Z",
        version: 3
      }
  """
  @spec get() :: Certificates.t()
  def get, do: CertificateCache.get()

  @doc """
  Returns the algorithm and certificate for a given kid.

  ## Examples
      iex> GoogleCerts.fetch("257f6a5828d1e4a3a6a03fcd1a2461db9593e624")
      {:ok, "RS256",
        %{
          "alg" => "RS256",
          "e" => "AQAB",
          "kid" => "257f6a5828d1e4a3a6a03fcd1a2461db9593e624",
          "kty" => "RSA",
          "n" => "kXPOxGSWngQ6Q02jhaJfzSum2FaU5_6e6irUuiwZbgUjyN2Q1VYHwuxq2o-aHqUhNPqf2cyCf2HspYwKAbeK9gFXqScrGLPW5pcquOWOVYUzPw87lBGH2fSxCYH35eB14wfLmF_im8DLTtZsaJvMRbqBgikM8Km2UA9ozjfK6E8pWW91fIT-ZF4Qy5zDkT3yX8EnAIMOuXg43v4t03FwFTyF4D9IET2ri2_n2qDhWTgtxJ0FHk3wG2KXdJIIVy2kUCTzMcZKaamRgUExt3Mu_z-2eyny8b6IdLPEIGF51VCgHebPQXE5iZmLGyw6M_pCApGJUw5GpXi6imo3pOvLjQ",
          "use" => "sig"
        }
      }


  """
  @spec fetch(String.t()) :: {:ok, String.t(), map()} | {:error, :cert_not_found}
  def fetch(kid) do
    with certificates = %Certificates{algorithm: algorithm} <- get(),
         %Certificate{cert: cert} <- Certificates.find(certificates, kid) do
      {:ok, algorithm, cert}
    else
      _ -> {:error, :cert_not_found}
    end
  end

  defp client, do: Application.get_env(Env.app(), :client, Client)

  @doc """
  Returns a `GoogleCerts.Certifcates` that is not expired.

  If the provided certificates are not expired, then the same certificates are returned.
  If the provided certificates are expired, then new certificates are retrieved via an HTTP request.
  The certificates returned will always be the same version as the certificates provided.
  """
  @spec refresh(Certificates.t()) :: Certificates.t()
  def refresh(certs = %Certificates{version: version}) do
    if Certificates.expired?(certs) do
      Logger.debug("Certificates are expired. Request new certificates via HTTP.")

      case client().get(version) do
        {:ok, %Response{expiration: exp, cert: cert}} ->
          Logger.debug(
            "Retrieved new certificates (v#{inspect(version)}) via HTTP. Certificates will expire on: #{exp}"
          )

          certs =
            Certificates.new()
            |> Certificates.set_version(version)
            |> Certificates.set_expiration(exp)
            |> add_certificates(cert)

          # Async write updated certs to file (disabled by default)
          Task.Supervisor.start_child(GoogleCerts.TaskSupervisor, fn ->
            CertificateCache.serialize(certs)
          end)

          certs

        error ->
          Logger.error("Error getting Google OAuth2 certificates. Error: " <> inspect(error))
          certs
      end
    else
      Logger.debug("Certificates are not expired.")
      certs
    end
  end

  defp add_certificates(certs = %Certificates{version: 1}, body) do
    Enum.reduce(body, certs, fn {kid, cert}, acc ->
      Certificates.add_cert(acc, kid, cert)
    end)
  end

  defp add_certificates(certs = %Certificates{version: version}, body) when version in 2..3 do
    body
    |> Map.get("keys")
    |> Enum.reduce(certs, fn cert = %{"kid" => kid}, acc ->
      Certificates.add_cert(acc, kid, cert)
    end)
  end
end