Google sign-in for a Unity desktop game, without the Firebase SDK

The Firebase Unity SDK's desktop build is for development only, and it has no desktop Google sign-in. Here is the RFC 8252 flow we shipped instead, in plain UnityWebRequest.

AIR COMMAND now has pilot accounts. Sign in with Google, and a Firestore document holds your flight hours, landings and hours by type. It is the same document this website reads on your pilot profile.

The obvious way to build that in Unity is the Firebase Unity SDK. We didn't use it. Its desktop libraries are for development only, and it has no desktop Google sign-in at all. The game ships on PC first, so that ruled it out.

It turns out you don't need it. Firebase Auth and Firestore are both plain HTTPS APIs, so UnityWebRequest covers every platform we'll ever target, mobile included. The hard part is getting a Google ID token into a desktop process safely. That is what this post covers.

Three pieces, each there because the obvious alternative fails

The flow is the one Google specifies for installed applications, RFC 8252. It has three parts.

1. The system browser, not a web view

Drawing Google's login page inside the game seems friendlier. Google refuses it with disallowed_useragent, and it's right to. A player can't tell whether a page the game draws is really Google's.

In their own browser, most players are already signed in, so signing in takes one click.

2. A loopback redirect

After consent, Google redirects to http://127.0.0.1:<port>/. The game runs a tiny HttpListener there to catch the authorization code. Google retired the old copy-and-paste "out-of-band" flow in 2022, so a loopback redirect is the only option left.

The listener is bound to 127.0.0.1 only. That means no firewall prompt and no elevation, and nothing outside the machine can reach it. Two details took real sessions to find:

  • The browser asks for /favicon.ico on its own. Only the real redirect carries a state parameter, so anything without one is ignored.
  • Tabs left over from an earlier attempt come back. They carry the wrong state. We refuse them and keep waiting for the redirect that belongs to this attempt.

We also always pass prompt=select_account. Without it, a browser signed in to one Google account signs the game in silently. On a shared family PC, that's the wrong pilot's logbook.

3. PKCE, because the client secret isn't secret

A desktop OAuth client has a client secret, and it ships inside every copy of the game. It proves nothing. PKCE does: the game makes a random verifier, sends only its hash to Google, and presents the verifier when it exchanges the code. If another program on the machine intercepts the code, the code is useless to it.

// Sketch of the idea. Only the challenge goes to Google in the browser URL.
string verifier  = RandomUrlSafe(64);
string challenge = Base64Url(SHA256(verifier));
// ...later, the token exchange proves we are the process that asked.
form["code_verifier"] = verifier;

It must be a Desktop app client

This one cost an afternoon. When you enable the Google provider, Firebase creates a Web OAuth client. Point the game at it and Google answers redirect_uri_mismatch. Only Desktop app clients may redirect to 127.0.0.1 on an arbitrary port.

Firebase accepts ID tokens issued to any OAuth client in its own Cloud project, so the desktop client needs no extra setup on the Firebase side.

From Google token to Firebase user

With a Google ID token in hand, accounts:signInWithIdp on the Identity Toolkit API returns a Firebase ID token and a refresh token. From then on, every Firestore call is a REST request with Authorization: Bearer <idToken>. The token is refreshed five minutes before it expires, so a request never goes out with a token that dies in transit.

Logbook writes are increments

Flight time is written every two minutes as a Firestore field transform, not a read-add-write:

{ "fieldPath": "flightHours", "increment": 0.0333 }

If you fly on two machines at once, both sessions count. The server adds each increment to the stored value, so neither write overwrites the other. It's also only a few writes per sortie, nowhere near a size that costs money.

Rules are the boundary

The Web API key and the desktop client secret are in every build by design. What a signed-in player can actually do is set by firestore.rules:

  • a pilot reads and writes only their own document;
  • a new document must start on the free package with an empty logbook;
  • updates can't touch packageType, and counters can never go down.

That last rule is also why this website can safely show your live logbook. It uses the same Google account, the same Firebase uid and the same rules. The browser can read your document and nothing else.