Skip to content

Instantly share code, notes, and snippets.

@gor181
Last active April 9, 2024 09:59
Show Gist options
  • Save gor181/cf65285e2225bdf629862460835aed40 to your computer and use it in GitHub Desktop.
Save gor181/cf65285e2225bdf629862460835aed40 to your computer and use it in GitHub Desktop.
Apple - API - Authentication
defmodule Project.AppleApi do
@moduledoc """
Apple API: https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api
Relies on Confex and having the following configuration in config/config.exs
```
config :project, Project.Apple,
api_url: {:system, "PROJECT_APPLE_URL", ""},
certificate: {:system, "PROJECT_APPLE_CERTIFICATE", ""},
iss: {:system, "PROJECT_APPLE_ISS", ""},
bid: {:system, "PROJECT_APPLE_BID", ""},
kid: {:system, "PROJECT_APPLE_KID", ""}
```
Additionally, make sure to inject the environment variables (e.g. via envrc.local using direnv):
```
export PROJECT_APPLE_URL=https://api.storekit-sandbox.itunes.apple.com
export PROJECT_APPLE_CERTIFICATE=BASE_64_ENCODED_PRIVATE_KEY_FROM_APPLE
export PROJECT_APPLE_ISS=ISS
export PROJECT_APPLE_BID=app.yourapp.YourApp.State
export PROJECT_APPLE_KID=2X9R4HXF34
```
Dependencies required:
```
{:joken, "~> 2.5"},
{:jose, "~> 1.11"},
```
"""
require Finch
require Logger
@doc """
Retrieves the transaction
"""
def get_transaction(transaction_id) do
api_url = "#{get_config(:api_url)}/inApps/v1/transactions/#{transaction_id}"
receive_timeout = 4_000
case "GET"
|> build(api_url, get_headers(), %{})
|> Finch.request(FinchHttpClient, receive_timeout: receive_timeout) do
{:ok, %{status: 200, body: body}} ->
body = Jason.decode!(body, keys: :atoms)
{:ok, decode_data(body.signedTransactionInfo)}
{:ok, %{status: 404}} ->
{:error, :not_found}
{:error, %Mint.TransportError{reason: :timeout}} ->
{:error, :timeout}
{ok_nok, response} ->
Logger.error(
"#{__MODULE__}: handle_send_response -> ok_nok:#{ok_nok} -> #{inspect(response)}"
)
{:error, :general}
end
end
defp build(method, uri, headers, nil) do
Finch.build(method, uri, headers_to_list(headers))
end
defp build(method, uri, headers, body) when is_binary(body) do
Finch.build(method, uri, headers_to_list(headers), body)
end
defp build(method, uri, headers, body) when is_map(body) and map_size(body) == 0 do
Finch.build(method, uri, headers_to_list(headers))
end
defp build(method, uri, headers, body) when is_map(body) do
Finch.build(method, uri, headers_to_list(headers), Jason.encode!(body))
end
defp headers_to_list(headers) do
headers
|> Map.to_list()
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
end
defp get_config(key) do
Confex.fetch_env!(:capi, __MODULE__) |> Keyword.get(key)
end
defp get_headers() do
%{"Content-Type": "application/json", Authorization: "Bearer #{get_token()}"}
end
defp decode_data(data) do
JOSE.JWT.peek_payload(data) |> Map.get(:fields)
end
defp get_token do
certificate = get_config(:certificate)
iss = get_config(:iss)
bid = get_config(:bid)
kid = get_config(:kid)
{_, key_map} =
certificate
|> Base.decode64!()
|> JOSE.JWK.from_pem()
|> JOSE.JWK.to_map()
signer = Joken.Signer.create("ES256", key_map, %{
alg: "ES256",
kid: kid,
typ: "JWT"
})
claims = %{
iss: iss,
iat: :os.system_time(:second),
exp: :os.system_time(:second) + 3599,
aud: "appstoreconnect-v1",
bid: bid
}
{:ok, secret} = Joken.Signer.sign(claims, signer)
secret
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment