In-depth Look at How Wizards Unite Codename Reservation Works

2
Wizards Unite Data Mining
Wizards Unite Data Mining

Niantic has launched a new mini-website titled Wizards Unite Code Name Reservation, built on top of their existing official website. After claiming my username, the little dataminer inside of me couldn’t help but wonder: how does this work?

The website is separated into two parts:

  • Use my Ingress Agent Name
  • Use my Pokémon GO Trainer Nickname

Ingress reservation redirects to intel.ingress.com/hpwu, the website that actually serves the Ingress Intel Map web application. Ingress and Pokémon GO reservations are fundamentally different, but they all target the same data layer (player profiles stored in Niantic’s Real World Platform).

Pokémon GO reservation takes place on the original website (nianticlabs.com/hpwu-codenamereservation) and it’s actually a client side AngularJS application written in plain ol’ JavaScript. And you know what they say about plain ol’ JavaScript: anyone can read it.

Which is exactly what we’ve done 🧙


Reservation form

The reservation form is controlled by CodenameReservationController, a simple class that has a few different methods (some omitted for brevity):

  • fbLoginClicked
    • loginFacebook
    • loginWithFacebookApi
  • googleLoginClicked
    • loginGoogle
  • superAwesomeLoginClicked
    • loginSuperAwesome
  • getPlayer
  • reserveName

When you click on one of the login methods, the flow is as follows:

Store current user

Regardless of the method you used to login, Niantic is storing your logged in information into localStorage. Luckily for everyone involved, only an access token is stored – no Personally identifiable information (PII) is stored in your browser.

Get Player

The system tries to authorize you in the background. If you’re not logged in on FB/Google/SuperAwesome an authorization prompt will pop up urging you to login. Once logged in, the system fires a getPlayer call that goes to this address:

https://social-prod-codenamereserve.eng.nianticlabs.com/plfe/web/json_action/GET_PLAYER

and tries to load player information associated with the logged in user. Curiously, Niantic is using your Facebook/Google/SuperAwesome ID to identify you and lookup your current codename!

If successful, getPlayer returns a JSON object similar to this:

{status: "SUCCESS", codename: "Zer0ghan"}

Reserve Codename

Once getPlayer finishes, you can now click Reserve Codename and the system will post your request to another endpoint on Niantic’s servers:

https://social-prod-codenamereserve.eng.nianticlabs.com/plfe/web/json_action/RESERVE_CODENAME

Once again, you are identified with a token, but this time it’s a Niantic-issued “Bearer Token” stored in the Authorization header of the POST request. This method of relaying user’s request without actually sending your codename is secure, fast and ingenious. We salute Niantic for implementing the reservation securely: there are no passwords, codenames or any other vulnerable information exchanged during the entire process.

We are also impressed that trying to reserve a codename with several different login methods actually points to the same Niantic account! I’ve tried claiming my Zer0ghan codename via Facebook (first) and Google login (second), and the Google login method correctly stated the following:

We’re sorry, we are unable to complete this reservation. You’ve already reserved a Code Name for Harry Potter: Wizards Unite.

Apparently, once you claim a codename, you will be able to use any login method currently associated with your account to use it when Wizards Unite launches in your region.


Conclusion

Niantic is using industry standards code for authorization, authentication and information transfer. Your current codename / username is safe from hijacking, modifications and overwrites. Despite site saying that you will have to use the same login method once Wizards Unite is available, evidence suggests that any login method associated with your Niantic account will give you access to your reserved codename.


Code Dump

As per tradition, an analysis is not complete without a proof of mining. Well, here we go (parts of the code omitted for brevity):

CodenameReservationController

var CodenameReservationController = function($scope, $http) {
            var PGO_BASE_URL = "https://social-prod-codenamereserve.eng.nianticlabs.com/plfe"
              , INGRESS_BASE_URL = "https://intel.ingress.com"
              , KWS_CALLBACK_BASE = window.location.origin;
            this.URLS = Object.freeze({
                login: PGO_BASE_URL + "/web/login",
                get_player: PGO_BASE_URL + "/web/json_action/GET_PLAYER",
                reserve_codename: PGO_BASE_URL + "/web/json_action/RESERVE_CODENAME",
                ingress_flow: INGRESS_BASE_URL + "/hpwu",
                get_updates: "https://www.harrypotterwizardsunite.com/",
                kws_callback: KWS_CALLBACK_BASE + "/hpwu-codenamereservation"
            }),
            this.STATES = Object.freeze({
                unset: 0,
                success: 1,
                failure: 2,
                already_reserved: 3,
                init: 4,
                pgo: 5,
                pgo_logged_in: 6,
                ptc: 7,
                reservation_period_ended: 8,
                heavy_load: 9
            }),
        };

GetPlayer

CodenameReservationController.prototype.getPlayer = function() {
            var currentUser = JSON.parse(localStorage.getItem("currentUser"))
              , self = this
              , config = {
                headers: {
                    Authorization: "Bearer " + currentUser.accessToken
                }
            };
            this.$http.post(this.URLS.get_player, {}, config).then(function(response) {
                self.setButtonsDisabled(!1),
                response.data && response.data.codename && "SUCCESS" === response.data.status && (self.codename = response.data.codename,
                self.setState(self.STATES.pgo_logged_in))
            }, function(response) {
                self.setButtonsDisabled(!1)
            })
        }

ReserveName

CodenameReservationController.prototype.reserveName = function() {
            this.setButtonsDisabled(!0);
            var currentUser = JSON.parse(localStorage.getItem("currentUser"))
              , self = this
              , config = {
                headers: {
                    Authorization: "Bearer " + currentUser.accessToken
                }
            };
            this.$http.post(this.URLS.reserve_codename, {}, config).then(function(response) {
                if (self.setButtonsDisabled(!1),
                response.data) {
                    var status = response.data.status;
                    if ("SUCCESS" === status)
                        return void self.setState(self.STATES.success);
                    if ("ALREADY_HAS_CODENAME" === status)
                        return void self.setState(self.STATES.already_reserved)
                }
                self.setState(self.STATES.failure)
            }, function(response) {
                self.setButtonsDisabled(!1)
            })
        }
Advertisement

2 COMMENTS