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