Set up Countersign
From a new account to a game only genuine copies can join.
Install the package
You need Unity 2021.3 or newer and PUN 2. Add the Meta XR Platform SDK as well if your game is on the Meta Horizon Store.
Download the Unity package 0.1.0
- Save the file in your project's
Packagesfolder and commit it, so everyone on your team gets the same one. - In Unity, open Window, then Package Manager.
- Press +, then Add package from tarball, and pick the file.
SHA-256 of this file: d3ad95d5a65a2425fb544829efbed5eaaeb00bddfd2c9a719b8443cdb897b096
Choose who may play
Make an account and add your game. The most important choice on that form is which builds may play:
| Choice | Use it for | You'll need |
|---|---|---|
| Testing only | The Unity editor and your own headset. Any copy gets in, so never ship with this. | Nothing |
| APK builds | Games shared through SideQuest or a Discord. | Your package name and the SHA-256 of your release signing certificate |
| APK builds, strict | As above, and Meta must give the headset its highest rating. Confirmed on a Quest 3 under the developer's own account, so try it with a player's account from outside your team before you ship: if Meta won't answer for them, they can't get in. | As above, your Meta app credentials, and Meta's Platform SDK in your build |
| Store builds | Games on the Meta Horizon Store. | Your package name and Meta app credentials |
| Store builds, strict | As above, and Meta must also vouch for the headset itself. | As above |
To get your certificate's SHA-256, run apksigner verify --print-certs your.apk on a release build. Meta app credentials look like OC|app id|app secret and come from the Meta developer dashboard.
Sign players in
This signs the player in, reads from PlayFab through Countersign, and connects to Photon. Leave out the PlayFab lines if you don't use it.
using Countersign;
using Countersign.Pun;
using Countersign.Unity;
using UnityEngine;
public class Online : MonoBehaviour
{
const string AuthServer = "https://api.countersignauth.com";
const string GameId = "your game id, from the game's Overview";
async void Start()
{
CountersignClient countersign = CountersignUnity.CreateClient(AuthServer, GameId, sideload: true);
var playfab = new CountersignPlayFab(countersign);
try
{
PlayFabLogin login = await playfab.LoginAsync();
string inventory = await playfab.CallAsync("GetUserInventory");
await CountersignPun.ConnectAsync(countersign);
}
catch (CountersignException e)
{
Handle(e);
}
}
void Handle(CountersignException e)
{
switch (e.Code)
{
case ErrorCode.Banned:
Debug.Log("Banned: " + e.BanReason);
break;
case ErrorCode.Denied:
Debug.Log("This copy of the game or this headset can't play online.");
break;
case ErrorCode.PlayFab:
Debug.Log("PlayFab said " + e.PlayFabError);
break;
default:
if (e.IsRetryable) Debug.Log("Try again in " + e.RetryAfterSeconds + " s");
else Debug.LogError(e);
break;
}
}
}
That's all the session code you need. The client renews passes while the player plays and recovers from most errors by itself.
sideload: true is for APK builds. For a store build, pass Meta's provider instead:
// initialize the Meta Platform SDK first, then:
var countersign = CountersignUnity.CreateClient(AuthServer, GameId, new Countersign.Meta.MetaIntegrityProvider());
APK builds should use Meta too. Add your Meta app credentials in Settings and Meta's Platform SDK to your build, then pass the provider as well as sideload: true. Meta then rates each headset and gives it an id, and its own device bans become available. If Meta won't answer for a player, they still get in on their headset's hardware key. Install com.meta.xr.sdk.platform from Meta's package registry, call Core.AsyncInitialize with your App ID first, and on Unity 2021.3 use version 72.0.0 and turn on the built-in Audio module.
Bans are on the headset. The client sends hashes of two ids the headset gives any app, one for each game, and the server only believes them from a build it has proved genuine. A ban covers every player seen on that headset, and lifting it lifts it for all of them, so a family sharing one headset shares its bans. A banned account signing in on someone else's headset bans that headset and whoever plays on it, too, so tell your players not to lend or borrow. It goes one step: their other headsets, and the people on them, aren't touched. Say in your privacy policy that you keep a hashed device identifier to enforce bans. Pass deviceMarks: false to leave them out, and a ban then relies on Meta's id, which changes every 30 days.
A ban can follow the Meta account too. Pass account: new Countersign.Meta.MetaAccountProvider() to CreateClient, once your app has the User ID grant in Meta's Data Use Checkup. Meta confirms each proof, so nobody can claim an account that isn't theirs. A banned account is then refused on any headset, and a player who resets their headset or buys a new one keeps their progress. Only a hash of the account's id is kept. Turn on Require proof of the player's Meta account in your settings if a player shouldn't get in without one.
Reinstalling keeps the player. The same headset and Meta profile comes back as the same player, with the same PlayFab account. A factory reset starts them over.
Reconnecting to Photon? Use CountersignPun.ReconnectAndRejoinAsync, or call CountersignPun.PrepareAsync before your own reconnect. Each proof works only once, and Photon checks again whenever its own token runs out.
Test in the editor
Press Play and the client works, using a test key saved on your computer. A test key can't prove anything about an install, so the editor only gets in while the game is set to Testing only. Under any other choice it gets Denied, which is the check doing its job.
Players who signed in during testing start over as new players when you change the policy, because a test key doesn't count under a real one.
Once your game is live, add a second game set to Testing only and point your editor at its id. Every plan has room for it, and the live game stays locked.
Build for Quest
In Project Settings, under Player and Android:
- Scripting Backend: IL2CPP, with ARM64 as the target architecture, as Quest requires.
- Minimum API Level: 29, the level Quest apps need.
- Internet Access: Require.
The package adds the keep rule its Java code needs, so minified builds work without anything extra.
Sign release builds with the same key whose certificate you gave Countersign. A build signed with any other key is treated as a modded copy.
Test on a real headset. Hardware checks are confirmed on a Quest 3 so far, so if you support other headsets, sign in on each of them under your real policy before you ship. An Android emulator's keystore can't prove anything about the device, so an emulator only gets in under Testing only.
Photon
In the Photon dashboard, for your app:
- Add a Custom Server authentication provider with the address on your game's Overview page.
- Switch off Allow anonymous clients to connect. While it's on, anyone can skip Countersign.
- Turn on
EnableCloseConnectionif you use RoomGuard. - Optional: add webhooks. Turn them on in your game's Settings to get the address. Set
PathCreatetoCreate, andPathBeforeJointoBeforeJoinif your plan offers it. They turn away a player banned mid-session at their next room.
Photon gets each player's id from Countersign and enforces it, so the UserId you see in a room is the id to ban.
RoomGuard removes banned players from a room while they're in it. Add the component to an object that lasts the whole online session, fill in your auth server, game id and the signing key from your game's Overview page, and make rooms with CountersignPun.Guard(new RoomOptions { ... }). Paste the key in rather than fetching it, so a fake server can't hand over its own.
PlayFab
- Add your title id and secret key in your game's Settings. The key stays on the server and never goes in the game.
- List every Client API your game calls, such as
GetUserInventory. For CloudScript, list each function asExecuteCloudScript:GrantDaily. Logins and currency grants are refused even if listed, so anything that gives a player something has to be a CloudScript function you wrote. - Lock down logins on the PlayFab tab, so nobody can make accounts from their own code. PlayFab can take about six minutes to apply this everywhere. Check with PlayFab shows what it does right now.
By default the PlayFab session stays on the server, and only the calls you listed can run. If your game already uses the PlayFab SDK everywhere, choose session mode and hand over the session like this, then restrict the title with PlayFab's own API policies:
PlayFabLogin login = await playfab.LoginAsync();
PlayFabSettings.staticPlayer.PlayFabId = login.PlayFabId;
PlayFabSettings.staticPlayer.ClientSessionTicket = login.SessionTicket;
PlayFabSettings.staticPlayer.EntityToken = login.EntityToken;
PlayFabSettings.staticPlayer.EntityId = login.EntityId;
PlayFabSettings.staticPlayer.EntityType = login.EntityType;
Bans and moderators
A ban reaches Countersign, PlayFab, the next room join and, with RoomGuard, the room the player is in. Ban from your game's Players tab, or give your moderators slash commands in your own server on the Discord tab: /ban, /unban, /player, /refusals and /status. Alerts about refusals can go to a channel too, from Settings.
When someone is refused
Players are told as little as possible, so cheaters learn nothing from the answer. Your game's Refusals tab shows you the real reason for each one.
| The client gets | What it means | What to do |
|---|---|---|
Denied | Not a genuine copy or headset under your policy. | Read the Refusals tab. On your own build, the usual cause is a wrong package name or certificate. |
Banned | This player is banned. | Show BanReason and BannedUntil. |
Paused | You paused the game, or its plan has run out. | Resume it on the Overview page, or check Billing. |
UnknownGame | The game id in your build is wrong. | Copy it again from the Overview page. |
NotAllowed | That PlayFab call isn't on your list. | Add it in Settings if the game should make it. |
NotEnabled | PlayFab isn't turned on for this game. | Turn it on in Settings. |
PlayFab | PlayFab ran the call and said no. | Read PlayFabError, such as InsufficientFunds. |
RateLimited, Upstream, ServerError, Network, Integrity | Something was busy or couldn't be reached. | Try again after RetryAfterSeconds. |
DeviceKey | The headset's keystore failed. | Report it with the log. |
Your own tools
A game key from the Keys tab lets your scripts moderate one game without your password. Send it as the X-Game-Key header to https://api.countersignauth.com/manage/v1/your-game-id/...
| Request | Does |
|---|---|
POST ban | {"subject": "player id", "hours": 72, "reason": "speed hacking"}. Leave out hours for a permanent ban. |
POST unban | {"subject": "player id"} |
GET bans | Everyone banned from this game. |
GET players/player-id | One player's headset check, trust level and ban. |
GET decisions?limit=50 | The latest refusals and why. |
GET usage | Players this month and totals. |
Your account
Turn on two-step sign-in on your Account page. Your account holds your PlayFab secret key and can ban your players, so it shouldn't rest on a password alone. It works with any authenticator app.
Signing up gives you a recovery code, shown once. Keep it in your password manager. With it and your email you can set a new password from the forgot password page, which also signs out every other device and gives you a new code. A used code stops working. If you lose your phone, enter the recovery code where the login asks for your app's code. Either way, two-step sign-in turns off until you set it up again.
If you've lost your code but can still log in, make a new one on your Account page. If you lose both your password and your code, nobody can get you back in, because this site doesn't send email.