lib/ueberauth_apple.ex

defmodule UeberauthApple do
  @moduledoc """
  UeberauthApple is a convenience module related to the Apple strategy for Ueberauth.

  For more information, see the [Overview](README.md).
  """

  @default_expires_in 86400 * 180

  @doc """
  Generate a Client Secret from the given options.

  This function does not concern itself with caching. Because generation of the Client Secret can
  be costly on every request, it is recommended to wrap this function with a cache-aware function.

  ## Options

    * `:client_id`: (**Required**) Reverse-domain Services ID configured for _Sign In with Apple_.

    * `:expires_in`: Duration, in number of seconds, for the secret to be valid. Apple specifies
      a maximum duration of `#{@default_expires_in}` seconds (6 months), which is the default.

    * `:key_id`: (**Required**) Key ID for the Apple-generated Private Key associated with the
      Services ID given in `:client_id`.

    * `:private_key` (**Required**) Full text of the Apple-generated Private Key file associated
      with the Services ID given in `:client_id`.

    * `:team_id`: (**Required**) Apple Developer Program team ID, as found in the top-right of the
      Apple Developer Console.

  ## Examples

  Below is an example of a client secret generator that maintains an expiring cache of the secret.

      defmodule MyApp.Apple
        @expiration_sec 86400 * 180

        @spec client_secret(keyword) :: String.t()
        def client_secret(_config \\\\ []) do
          with {:error, :not_found} <- get_client_secret_from_cache() do
            secret =
              UeberauthApple.generate_client_secret(%{
                client_id: Application.fetch_env!(:my_app, :apple_client_id),
                expires_in: @expiration_sec,
                key_id: Application.fetch_env!(:my_app, :apple_private_key_id),
                team_id: Application.fetch_env!(:my_app, :apple_team_id),
                private_key: Application.fetch_env!(:my_app, :apple_private_key)
              })

            put_client_secret_in_cache(secret, @expiration_sec)
            secret
          end
        end
      end
  """
  @spec generate_client_secret(map | keyword) :: String.t()
  def generate_client_secret(opts) do
    opts = Enum.into(opts, %{expires_in: @default_expires_in})
    now = DateTime.utc_now() |> DateTime.to_unix()

    jwk = JOSE.JWK.from_pem(opts.private_key)
    jws = %{"alg" => "ES256", "kid" => opts.key_id}

    jwt = %{
      "iss" => opts.team_id,
      "iat" => now,
      "exp" => now + opts.expires_in,
      "aud" => "https://appleid.apple.com",
      "sub" => opts.client_id
    }

    {_, token} = jwk |> JOSE.JWT.sign(jws, jwt) |> JOSE.JWS.compact()
    token
  end
end