This guide explains how to make Photon Fusion and Unisave talk to each other. More specifically, how to authenticate requests sent from Fusion Dedicated Server to Unisave and how to authenticate players joining the Fusion Server using Unisave.
Network topology
There are three main ways in which you can set up Photon Fusion:
- Shared
- Client Host
- Dedicated Server
This guide focusses on the Dedicated Server setup, as it’s the only one where Photon-to-Unisave communication must be handled. In both two remaining cases, there are only game clients, which can be authenticated via standard Unisave authentication.
You would typically choose the Dedicated Server setup if you want the Fusion Server to have authority over the state of the game. And in this scenario, you want the Fusion Server to talk to Unisave, triggering business logic events (e.g. player won race, grant them reward coins).
In this situation, the Fusion Server is built as a “Headless Server”, build in Unity, and uploaded to some server hosting provider (say Hathora). From the Unisave’s perspective, your Fusion Server is just another build (like the PC build, mobile build, WebGL build). Therefore when the Fusion Server makes a request to Unisave, we need to make sure it is the Fusion Server indeed and not just some hacker-client trying to pretend.
This is the first scenario and we call this the Fusion Server Authentication scenario:
But even when we secure this, we still have to make sure that when the Fusion Server thinks a player won the race, the Fusion Server needs to know that this player is who he says he is. And this is handled when the player connects to the Fusion Server and the scenario is called the Fusion Player Authentication scenario:
With both these mechanisms in place, we can have Unisave trust the PlayerWonRace
facet call.
Fusion connection token
Firstly, we need to define what the Fusion Connection Token looks like. It’s a binary blob that you can send to your Fusion Server whenever a player wants to join. The server can then parse this token to know who is joining in.
Note: The connection token is passed to Photon in
StartGame
as a part of theStartGameArgs
. It can then be obtained by callingRunner.
GetPlayerConnectionToken
(playerRef)
.
We will create a custom C# class that will use the Unisave serializer to turn itself into a binary blob. The class will hold the Unisave player ID (the PlayerEntity
ID) and also the Fusion Player Token (explained later). Place the class into your backend folder as it will also be used by your Unisave backend:
public class FusionConnectionToken
{
public string unisavePlayerId;
public string fusionPlayerToken;
public byte[] ToBytes()
{
return Encoding.UTF8.Encode(
Serializer.ToJsonString(this)
);
}
public static FusionConnectionToken FromBytes(byte[] data)
{
return Serializer.FromJsonString<FusionConnectionToken>(
Encoding.UTF8.Decode(data)
);
}
}
Note: You can understand the
FusionConnectionToken
as a username and password pair, that will be used by a player to connect to the Fusion Server.
⚠️ Warning: The Connection Token has a limit of 128 bytes maximum, enforced by Photon Fusion. This limits how many more fields you can add to it substantially. Also note that there seems to be inconsistency with Client-Host setup, where the limit is only enforced for clients. Be wary of this.
Fusion Server Authentication
Fusion Server authentication is the first requirement for a secure Photon Fusion setup with Unisave and it makes sure that facet calls that only the Fusion Server is supposed to make cannot be made by any other client.
The solution is simple. Generate a random token (a password used by the fusion Server) and send it with each facet call to Unisave. Unisave then checks this token and if it matches, it performs the requested business action. We can call this token the Fusion Server Token.
Calling facets from the Fusion Server
Since the Fusion Server is built with Unity just like any other game build, it gets automatically registered by Unisave in the list of builds and can make facet calls like any other build.
Let’s say the Fusion Server tracks the winner of a race. When the race finishes, it calls Unisave and tells it which player won and that coins should be given to them:
public class MyRaceManager : NetworkBehaviour
{
void Update()
{
// Somehow detect the end of the race and the winner
// and trigger the Unisave logic.
if (raceHasFinished() && Runner.IsServer)
SendWinnerToUnisave(this.RacePlayers[0]);
}
async void SendWinnerToUnisave(PlayerRef player)
{
// Provided to the server by configuring
// your hosting provider, e.g. Hathora.
string fusionServerToken = Environment.GetEnvironmentVariable(
"FUSION_SERVER_TOKEN"
);
// We pull the Unisave player ID from the connection token
// again, but you should instead store the ID in some variable
// right after the player joins and use that here instead.
var connectionToken = FusionConnectionToken.FromBytes(
Runner.GetPlayerConnectionToken(
playerRef
)
);
// Make the facet call to Unisave about the player winning.
await this.CallFacet(
(MyRaceFacet f) => f.PlayerWonRace(
fusionServerToken,
connectionToken.unisavePlayerId
)
);
}
}
The Fusion Server Token can be passed into the Fusion Server via environment variables. How exactly that is performed depends on the hosting provider you use. Photon itself does not provide hosting for servers (Game Session servers in the Photon terminology). Check out this Photon Fusion documentation page. There are a number of listed providers, and for example Hathora can have environment variables set up like this.
Alternatively you could accept the token as one of the startup command-line arguments of the server, say --fusion-server-token=123456789
. Here is how to get these from C#.
Securing facets designated for the Fusion Server
On the Unisave side, once you receive the facet call, you verify the fusion Server token against the one stored in Unisave environment variables (you put the same token into both the fusion Server as well as the unisave environment) and if it matches, you perform the business logic:
public class MyRaceFacet : Facet
{
public void PlayerWonRace(
string fusionServerToken,
string unisavePlayerId
)
{
// Verify the given Fusion Server token.
string trueFusionToken = Env.Get("FUSION_SERVER_TOKEN");
if (trueFusionToken != fusionServerToken)
throw new Exception(
"Given Fusion Server token is invalid"
);
// Give the player their victory gold.
var player = DB.Find<PlayerEntity>(unisavePlayerId);
player.coins += 420;
player.Save();
}
}
For all the facet calls that originate from the Fusion Server, you need to send the Fusion Server Token and perform its validation.
Fusion Player Authentication
Now that Unisave can authenticate (verify the identity of) the Fusion Server, we also need to Fusion Server to authenticate (verify the identity of) players that connect to it.
Photon Fusion provides a mechanism for custom authentication, but because Photon does not host game servers, this authentication only supports making HTTP request to a given endpoint. It does not allow us to make facet calls. Therefore we will use a different approach.
We will let the player connect no matter what, and then when he starts being handled by our Fusion Server, we verify his identity there and kick him out in case the validation fails.
The process will be the following:
- We will create a new field in the
PlayerEntity
calledfusionPlayerToken
. This random token will act as the password used by the player to join our Fusion Server. - Before the player attempts to join the Fusion Server, it first fetches its Fusion Player Token from Unisave.
- Then the player connects to the Fusion Server, while providing the Unisave Player ID and the Fusion Player Token as part of the
ConnectionToken
(a binary payload that can be sent to your Fusion Server during connection). - Photon makes the player join a session and in turn triggers the
PlayerJoined
callback on the Fusion Server. - The Fusion Server sends the player’s connection credentials to Unisave and Unisave checks their validity.
- If the validation fails, the player is kicked from the Fusion Server.
The process is visualized in this diagram:
First, we modify the PlayerEntity
:
public class PlayerEntity : Entity
{
/* ... */
public string fusionPlayerToken;
}
Then we need to add the token generation and fetching facet to Unisave:
public class MyFusionAuthFacet : Facet
{
[Middleware(typeof(Authenticate))]
public FusionConnectionToken GetFusionConnectionToken()
{
var player = Auth.Get<PlayerEntity>();
// Regenerate fusion player token before returning it.
player.fusionPlayerToken = GenerateSecureToken();
player.Save();
return new FusionConnectionToken() {
unisavePlayerId = player.Id,
fusionPlayerToken = player.fusionPlayerToken
};
}
private string GenerateSecureToken()
{
byte[] bytes = new byte[32];
var cryptoProvider = new RNGCryptoServiceProvider();
cryptoProvider.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
}
Now, when the player wants to connect to the Fusion Server, we first call this facet and pass the information to Photon:
async void StartGame()
{
// Create the Fusion runner.
runner = gameObject.AddComponent<NetworkRunner>();
/* ... */
// Get the connection token from Unisave
FusionConnectionToken connectionToken = await this.CallFacet(
(MyFusionAuthFacet f) => f.GetFusionConnectionToken()
);
// Join the Fusion Server.
await runner.StartGame(new StartGameArgs()
{
ConnectionToken = connectionToken.ToBytes(),
/* ... */
});
}
Then, in the scene (in the Fusion Server), we need to have a NetworkBehaviour
running which will check for new incoming players and verify their identity:
public class FusionPlayerAuthenicator : NetworkBehaviour, IPlayerJoined
{
public async void PlayerJoined(PlayerRef player)
{
// We only perform authentication if we are the server.
if (!Runner.IsServer)
return;
// Get the connection token for the player.
var connectionToken = FusionConnectionToken.FromBytes(
Runner.GetPlayerConnectionToken(
playerRef
)
);
// Ask Unisave whether the token in valid.
bool isValid = await this.CallFacet(
(MyFusionAuthFacet f) => f.IsFusionConnectionTokenValid(
connectionToken
)
);
// If not, kick the player.
if (!isValid)
Runner.Disconnect(player);
}
}
Finally, we need to extend the MyFusionAuthFacet
to implement the token validation method:
public class MyFusionAuthFacet : Facet
{
/* ... */
public bool IsFusionConnectionTokenValid(
FusionConnectionToken connectionToken
)
{
var player = DB.Find<PlayerEntity>(
connectionToken.unisavePlayerId
);
string givenToken = connectionToken.fusionPlayerToken;
string trueToken = player.fusionPlayerToken;
if (givenToken != trueToken)
{
Log.Warning(
"Received invalid Fusion player token for player: " \
+ player.Id
);
return false;
}
return true;
}
}
Conclusion
While the setup might seem complicated at first, it completely outsources Photon authentication onto Unisave. This lets you add more authentication methods for Unisave (Steam, Epic), and this code can remain unchanged. You can also extend this logic to fetch metadata about the player from Unisave and grant them more fine-grained privileges.