Current section
Files
Jump to
Current section
Files
lib/wallet_passes/google/router.ex
defmodule WalletPasses.Google.Router do
@moduledoc """
Plug.Router for Google Wallet class callbacks.
Mount in your Phoenix router outside any CSRF-protected pipeline:
forward "/passes/google", to: WalletPasses.Google.Router
The endpoint is `POST /callback`. Configure the URL Google should hit:
config :wallet_passes, :google_callback_url, "https://yourdomain.com/passes/google/callback"
"""
use Plug.Router
require Logger
alias WalletPasses.Config
alias WalletPasses.EventHandler.Dispatch
alias WalletPasses.Google.CallbackVerifier
alias WalletPasses.Schema
plug Plug.Parsers, parsers: [:json], json_decoder: Jason
plug :match
plug :dispatch
post "/callback" do
issuer_id = Config.google_issuer_id()
envelope = conn.body_params
with {:ok, signed_message_str} <- CallbackVerifier.verify(envelope, issuer_id),
{:ok, message} <- Jason.decode(signed_message_str),
{:ok, serial} <- extract_serial(message["objectId"]),
{:ok, outcome} <- handle_event(message, serial) do
respond(conn, outcome, serial, message)
else
{:error, :verification_failed} -> send_resp(conn, 401, "")
{:error, :unsupported_protocol} -> send_resp(conn, 401, "")
_ -> send_resp(conn, 200, "")
end
end
match _ do
send_resp(conn, 404, "")
end
defp respond(conn, {:dispatched, event, meta}, serial, _message) do
Dispatch.dispatch(event, serial, :google, meta)
send_resp(conn, 200, "")
end
defp respond(conn, :duplicate, _serial, _message), do: send_resp(conn, 200, "")
defp respond(conn, :unknown_pass, _serial, _message), do: send_resp(conn, 200, "")
defp respond(conn, :invalid, _serial, _message), do: send_resp(conn, 200, "")
defp handle_event(message, serial) do
case Schema.get_google_pass(serial) do
nil ->
{:ok, :unknown_pass}
google_pass ->
attrs = %{
event_type: message["eventType"],
object_id: message["objectId"],
class_id: message["classId"],
nonce: message["nonce"],
exp_time_millis: parse_exp(message["expTimeMillis"]),
received_at: DateTime.utc_now(),
}
case Schema.record_google_callback(google_pass, attrs) do
{:ok, _callback} ->
event = if message["eventType"] == "save", do: :pass_added, else: :pass_removed
meta = %{
object_id: message["objectId"],
class_id: message["classId"],
nonce: message["nonce"],
exp_time_millis: attrs.exp_time_millis,
}
{:ok, {:dispatched, event, meta}}
{:error, :duplicate_nonce} ->
{:ok, :duplicate}
{:error, :invalid} ->
Logger.warning(
"WalletPasses: Google callback rejected by schema validation " <>
"(event_type=#{inspect(message["eventType"])} object_id=#{inspect(message["objectId"])} " <>
"class_id=#{inspect(message["classId"])})"
)
{:ok, :invalid}
end
end
end
defp extract_serial(nil), do: {:error, :verification_failed}
defp extract_serial(object_id) when is_binary(object_id) do
case String.split(object_id, ".", parts: 2) do
[_issuer, serial] when serial != "" -> {:ok, serial}
_ -> {:error, :verification_failed}
end
end
defp parse_exp(nil), do: nil
defp parse_exp(n) when is_integer(n), do: n
defp parse_exp(s) when is_binary(s) do
case Integer.parse(s) do
{n, ""} -> n
_ -> nil
end
end
end