Write an Elixir Pokémon Go API — Login

As many, I have followed the rise of Pokémon Go, both with the game itself, and the development of various unofficial APIs made by the community. The development is especially fascinating to me, because it shows how fast people can reverse engineer some complicated protection by Niantic lab. I got curious, and wanted to look into the internal working of those API. I decided to write a port of it in Elixir to learn more about it.

Disclaimer: This is not intended as a hacking tool, but an educational learning experience. As thus, I won't attempt to use the encryption library, nor provide any ready-to-run binary.


The first and probably hardest part is naming your library. After a little bit of research, I found this description on the net

The Pokédex (Japanese: ポケモン図鑑 illustrated Pokémon encyclopedia) is … an invaluable tool to Trainers in the Pokémon world. It gives information about all Pokémon in the world that are contained in its database, although it differs in how it acquires and presents information over the different media. However, they are also only given to a few Trainers at a time, generally to the ones that are felt to have exceptional potential and skill.

The library should acquire information from Pokémon Go API and present them in different media, as a library. So Pokédex seems to fit. The -ex suffix also fits the naming pattern of Elixir library.

$ mix new pokedex --sup

To make it easier to keep various information, I define a Pokedex struct:

# lib/pokedex.ex
defmodule Pokedex do
...
defstruct username: nil,
password: nil,
token: nil,
expires: nil
end

The first thing we need to do is to login into the server. From the look of they python API, it seems to be OAuth2. However, a closer look show that logging into PTC account requires a manual authentication dance. So we need to write a HTTP client for that purpose. I choose `httpoison` as the HTTP client, and `poison` as my JSON encoder/decoder.

We use HTTPoison.Base to make it easier for our client. Since we will make requests to the https://sso.pokemon.com/sso/login?service=https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize and https://sso.pokemon.com/sso/oauth2.0/accessToken, and we use a custom User-Agent header, we define the two HTTPoison callbacks:

# lib/pokedex/client.ex
...
def process_url(url) do
https://sso.pokemon.com/sso <> url
end
  defp process_request_headers(headers) do
headers ++ [{"User-Agent", "niantic"}]
end

Now we define a new/1 that takes a Pokedex struct with username and password in it. The first step is to perform a preflight GET to the login endpoint to retrieve the lt and execution data, then use them to POST to the same login endpoint to get the ticket.

# lib/pokedex/client.ex
...
@login_url "/login?service=https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize"
def new(%Pokedex{username: username, password: password}) do
Logger.debug "GET #{@login_url} for preflight 'lt' and 'execution' data."
%{body: body, headers: headers} = __MODULE__.get!(@login_url)
# Need to retrieve the cookie for next requests.
{_, cookie} = List.keyfind(headers, "Set-Cookie", 0)
data = Poison.decode!(body)
    params = [
{"lt", data["lt"]},
{"execution", data["execution"]},
{"_eventId", "submit"},
{"username", username},
{"password", password}
]
    Logger.debug "POST #{@login_url} with params #{inspect params} and cookie #{cookie}"    
%{headers: headers} = __MODULE__.post!(@login_url, {:form, params}, [], hackney: [cookie: cookie])
    {"Location", location} = List.keyfind(headers, "Location", 0)
%{query: "ticket=" <> ticket} = URI.parse(location)

The nice thing with Elixir/Erlang is that we can pattern match on the query string to get the ticket value out immediately, without resorting to any regex parsing.

Now that we have the ticket, we can exchange it with the token endpoint for an access token

...
@token_url "/oauth2.0/accessToken"
  def new(pokedex = %Pokedex{username: username, password: password}) do
...
    oauth_payload = [
{"client_secret", @client_secret},
{"redirect_uri", @redirect_uri},
{"client_id", @client_id},
{"grant_type", "refresh_token"},
{"code", ticket}
]
    Logger.debug "POST #{@token_url} with ticket #{ticket}"
%{body: body} = __MODULE__.post!(@token_url, {:form, oauth_payload})
%{"access_token" => access_token, "expires" => expires} = URI.decode_query(body)
    Logger.debug "Trainer #{username} logged in successfully."
    {:ok, %{pokedex | token: access_token, expires: Util.timestamp + String.to_integer(expires)}}
end

The whole lib/pokedex/client.ex after some refactoring is below:

defmodule Pokedex.Client do
use HTTPoison.Base
  require Logger
  alias Pokedex.Util
  @client_id "mobile-app_pokemon-go"
@client_secret "w8ScCUXJQc6kXKw8FiOhd8Fixzht18Dq3PEVkUCP5ZPxtgyWsbTvWHFLm2wNY0JR"
@site "https://sso.pokemon.com/sso"
@login_url "/login?service=https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize"
@redirect_uri "https://www.nianticlabs.com/pokemongo/error"
@token_url "/oauth2.0/accessToken"
@headers [
{"User-Agent", "niantic"}
]
  def new(pokedex = %{username: username, password: password}) do
__MODULE__.start
    Logger.debug "GET #{@login_url} for preflight 'lt' and 'execution' data."
%{body: body, headers: headers} = __MODULE__.get!(@login_url)
{_, cookie} = List.keyfind(headers, "Set-Cookie", 0)
data = Poison.decode!(body)
    params = [
{"lt", data["lt"]},
{"execution", data["execution"]},
{"_eventId", "submit"},
{"username", username},
{"password", password}
]
    Logger.debug "POST #{@login_url} with params #{inspect params} and cookie #{cookie}"
%{headers: headers} = __MODULE__.post!(@login_url, {:form, params}, [], hackney: [cookie: cookie])

{"Location", location} = List.keyfind(headers, "Location", 0)
%{query: "ticket=" <> ticket} = URI.parse(location)
    oauth_payload = [
{"client_secret", @client_secret},
{"redirect_uri", @redirect_uri},
{"client_id", @client_id},
{"grant_type", "refresh_token"},
{"code", ticket}
]
    Logger.debug "POST #{@token_url} with ticket #{ticket}"
%{body: body} = __MODULE__.post!(@token_url, {:form, oauth_payload})
%{"access_token" => access_token, "expires" => expires} = URI.decode_query(body)
    Logger.debug "Trainer #{username} logged in successfully."
    {:ok, %{pokedex | token: access_token, expires: Util.timestamp +   String.to_integer(expires)}}
end
  # HTTPoison callbacks
def process_url(url) do
@site <> url
end
  defp process_request_headers(headers) do
headers ++ @headers
end
end

With an access_token, we can look into making actual request to the API.