Never Ship OAuth Client Secrets in Your Unity Build
Here's a rule worth treating as non-negotiable: an OAuth client secret must never ship inside your Unity game. Not obfuscated, not split across strings, not in a config file bundled with the build. Once a secret is in something you hand to players, it's public — the only questions are who pulls it out and when. This is why that's true, what actually goes wrong, and how to structure login so the secret never leaves a server.
A build is not a safe place for a secret
The instinct is understandable. The provider hands you a client ID and a client secret, the token exchange needs the secret, so you drop it into a MonoBehaviour next to the ID and move on. It compiles, it works in the editor, it ships.
The problem is that "compiled" is not "hidden." A Unity build carries your code as IL inside assemblies, and strings survive that intact. On a desktop binary, a strings dump surfaces a hardcoded secret in seconds. On Android, unzipping the .apk and reading the managed assemblies gets there just as fast. Obfuscators exist and they raise the effort a little, but they don't change the physics: the running game has to read the secret to use it, so anything the game can read, an attacker with the game can read too. There is no client-side hiding place that survives someone who owns the binary.
What goes wrong when it leaks
This isn't a theoretical "best practice" — a leaked client secret has concrete consequences.
With your client ID and secret, an attacker can impersonate your app to the provider. They can run OAuth flows under your app's identity, burn through your API quotas, and — this is the nasty one — present users with a real consent screen carrying your app's name to phish permissions or tokens. To the provider it's your app doing it, because as far as the provider knows, only your app has that secret.
And the cleanup is expensive. The fix for a leaked secret is to rotate it — generate a new one and invalidate the old. If the secret lived in your build, rotating it breaks every copy already installed, so you're shipping an emergency update and hoping players take it before the old secret is abused. A secret that lives on a server rotates with a dashboard change and zero client impact. That difference is the whole argument.
"But what about PKCE?"
Fair question, and it's the right instinct. PKCE was designed exactly for clients that can't hold a secret — it replaces the secret with a one-time proof generated per login, so a public client can do the authorization-code flow safely. If every provider accepted PKCE in place of a secret, a game could be a safe public client and this article would be shorter.
They don't. Several major providers — Google and Facebook among them — still require the client secret on the token exchange, PKCE or not. So "just use PKCE" isn't a general escape hatch for games. The durable answer is the same for every provider: the token exchange, secret and all, runs on a server, and PKCE is a good additional hardening on top where supported.
Keep the secret on a server — without running one
The secret has to live on a confidential server. That doesn't mean you have to build and operate that server — you can add auth to Unity without a backend of your own by letting a hosted service be the confidential client.
That's Oathwall's whole shape. Your provider client secrets live in the dashboard, the token exchange happens on Oathwall's servers, and your game receives the finished tokens. The only credential your build ships is a public appKey:
using PixitGames.SSOLoginKit;
public class LoginManager : SsoClientBase
{
// No client secret anywhere in this class or this build.
public void OnLoginButton() => StartLogin("google");
protected override void OnLoginSuccess(LoginResult user)
{
Debug.Log($"Signed in as {user.name}");
}
}
There's no client secret in that code because there's no client secret in the build at all. The appKey is meant to be public — it identifies your app but is useless without the secret sitting behind it on the server. Rotate a provider secret? Change it in the dashboard; no player ever downloads a new build for it.
The short version: treat the client secret like a server-side password, because that's what it is. A game build is a public artifact, and public artifacts don't keep secrets.
FAQ
The details — extraction, why PKCE isn't a full substitute, blast radius, and where the secret belongs — are in the section below.
Ready to do it right?
Oathwall keeps every provider secret server-side so your Unity build ships only a public appKey. No extractable secret, no emergency rotation build. It's available on the Unity Asset Store.
Keep reading: the Unity authentication guide for the full flow, auth without a backend for where the server work goes, or what PKCE is for the extension that hardens public clients.
Frequently asked questions
- Can someone actually extract a client secret from a Unity build?
- Yes, easily. Compiled Unity code and its assemblies still contain readable strings, and a hardcoded secret shows up with basic tools — a strings dump on a desktop binary, or unzipping an .apk and reading the IL. Obfuscation raises the effort slightly but doesn't make a shipped secret safe; anything the running game can read, a determined attacker can too.
- Isn't PKCE enough to skip the client secret?
- PKCE removes the need for a secret on public clients, and it's the right tool for one. But several providers — Google and Facebook among them — still require a client secret on the token exchange even with PKCE, so you can't rely on PKCE alone to make a game a safe confidential client. The secret has to live on a server.
- What can an attacker do with a leaked client secret?
- Impersonate your app to the OAuth provider — run their own auth flows under your app's identity, consume your quota, and in some cases phish users behind your app's name and consent screen. Fixing it means rotating the secret, which invalidates the old one and forces a new build for every player.
- How do I keep the client secret out of my Unity build?
- Don't put it there. The secret belongs on a confidential server that performs the token exchange, not in the client. With Oathwall the secret lives in the dashboard and the exchange runs on Oathwall's servers; your build ships only a public appKey, which is safe to expose.