Integrate game session player validation
Overview
AccelByte Gaming Services (AGS) Session includes a game session player validation feature that enhances the level of security when players enter a game session maintained by a dedicated server (DS). The DS must ensure that the game client attempting to connect is authenticated as a valid user and a member of the session. The process works as follows:
- The session service generates a secret key and returns it to the player who should possess it.
- When the game client attempts to connect to the DS, the server validates the secret key based on the player's user ID.
- Once validated, the player's game client can successfully connect to the DS.
This article explains the implementation of AGS Session's player validation feature on both the game client and the DS sides.
Prerequisites
To complete the steps in this article, you will need:
- Access to the AGS Admin Portal.
- Familiarity with configuring matches.
- Familiarity with configuring and joining game sessions.
Flow and code snippets
To make it easier to explain the process of implementing session player validation, you can think of it as consisting of two parts: the DS and the game client. For the DS side, this article explains how the server can generate secrets. For the game client side, this article explains how it can obtain secrets. This can happen either by joining a game session or creating a session through matchmaking, including how to obtain secrets when connection problems occur in the lobby.
Game server
When matchmaking successfully finds a match, a game session is created, and a dedicated server is assigned. Subsequently, the game server receives a notification of the secret via the DS Hub WebSocket connection from AGS Session.
- Unreal Engine
- Unity
void AMyActor::ConnectToDSHub()
{
...
const AccelByte::GameServerApi::FOnV2SessionSecretUpdateNotification
OnDSHubSessionSecreteUpdateNotificationDelegate =
AccelByte::GameServerApi::FOnV2SessionSecretUpdateNotification::CreateUObject(this, &AMyActor::OnDSHubSessionSecreteUpdateNotification);
ServerApiClient->ServerDSHub.SetOnV2SessionSecretUpdateNotification(OnDSHubSessionSecreteUpdateNotificationDelegate);
...
ServerApiClient->ServerDSHub.Connect(ServerName);
}
void AMyActor::OnDSHubSessionSecreteUpdateNotification(const FAccelByteModelsSessionSecretUpdateNotification& Notification)
{
UE_LOG(LogTemp, Log, TEXT("DS SessionSecreteUpdate, Secret:%s"), *Notification.Secret);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class DSController : MonoBehaviour
{
//server Id format is ds_ and followed by uuidv4
//it is set by dedicated server
string serverId = "ds_8f92300b-9791-475b-97ef-db14b445e415";
string sessionSecret = "";
void ConnectToDSHub()
{
// ...
AccelByteSDK.GetServerRegistry().GetApi().GetDsHub().SessionServerSecretReceived += (notification) =>
{
if (notification.IsError)
{
Debug.LogWarning($"Failed to receive SessionSecretUpdate");
return;
}
Debug.Log($"DS SessionSecreteUpdate, Secret:{notification.Value.secret}");
// session secret will be used later for session validation
sessionSecret = notification.Value.secret;
};
// ...
AccelByteSDK.GetServerRegistry().GetApi().GetDsHub().Connect(ServerId);
}
}
Game client
A game client can obtain secrets either by joining a game session or creating a session through matchmaking. It can also know what to do when connection problems occur in the lobby.
After successfully obtaining the session from starting matchmaking or joining a game session, the game client will be notified of the secret via the lobby WebSocket connection.
- Unreal Engine
- Unity
void AMyActor::ConnectToLobby()
{
...
const AccelByte::Api::Lobby::FV2SessionJoinedSecretNotif V2SessionJoinedSecretNotifDelegate =
AccelByte::Api::Lobby::FV2SessionJoinedSecretNotif::CreateUObject(this, &AMyActor::OnV2SessionJoinedSecretNotif); ApiClient->Lobby.SetV2SessionJoinedSecretNotifDelegate(V2SessionJoinedSecretNotifDelegate);
...
ApiClient->Lobby.Connect();
}
void AMyActor::OnV2SessionJoinedSecretNotif(const FAccelByteModelsV2SessionJoinedSecret& Data)
{
UE_LOG(LogTemp, Log, TEXT("Client OnV2SessionJoinedSecretNotif, Secret=%s"), *Data.Secret);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class DSController : MonoBehaviour
{
void ConnectToLobby()
{
// ...
AccelByteSDK.GetClientRegistry().GetLobby().SessionSecret += (notif) =>
{
Debug.Log($"Client Session Secret, Secret={notif.Value.secret}");
};
// ...
AccelByteSDK.GetClientRegistry().GetLobby().Connect();
}
}
Start matchmaking
The game client can receive the notification by initiating matchmaking.
- Unreal Engine
- Unity
void AMyActor::CreateMatchTicket(const FString& SessionId)
{
const FString MatchPoolName = PoolName;
FAccelByteModelsV2MatchTicketOptionalParams Option;
Option.Latencies = ApiClient->Qos.GetCachedLatencies();
FJsonObject AttributesJsonObj{};
AttributesJsonObj.SetStringField("server_name", ServerName);
FString JsonString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonString);
TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>(AttributesJsonObj);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
// Convert to JSON Object Wrapper
FJsonObjectWrapper Attributes;
FAccelByteJsonConverter::JsonObjectStringToUStruct(JsonString, &Attributes);
Option.Attributes = Attributes;
Option.SessionId = SessionId;
ApiClient->MatchmakingV2.CreateMatchTicket(MatchPoolName,
AccelByte::THandler<FAccelByteModelsV2MatchmakingCreateTicketResponse>::CreateUObject(this, &AMyActor::OnCreateMatchTicketSuccess),
AccelByte::FCreateMatchmakingTicketErrorHandler::CreateUObject(this, &AMyActor::OnCreateMatchTicketFail));
}
void AMyActor::OnCreateMatchTicketSuccess(const FAccelByteModelsV2MatchmakingCreateTicketResponse& Data)
{
UE_LOG(LogTemp, Log, TEXT("Client OnCreateMatchTicketSuccess "));
}
void AMyActor::OnCreateMatchTicketFail(int32 ErrorCode, const FString& ErrorMessage, const FErrorCreateMatchmakingTicketV2& CreateTicketErrorInfo)
{
UE_LOG(LogTemp, Log, TEXT("Client OnCreateMatchTicketFail, Error code: %d\nError message:%s"), ErrorCode, *ErrorMessage);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class ClientController : MonoBehaviour
{
public void CreateMatchTicket(string sessionId, string matchPoolName)
{
Dictionary<string, int> latencies = new Dictionary<string, int>();
AccelByteSDK.GetClientRegistry().GetApi().GetQos().GetAllActiveServerLatencies(result =>
{
if (!result.IsError)
{
latencies = result.Value;
}
else
{
Debug.LogWarning("Failed to fetch active server latencies");
}
});
var attributes = new Dictionary<string, object>()
{
{ "server_name", "my_server_123" }
};
var additionalParam = new MatchmakingV2CreateTicketRequestOptionalParams()
{
latencies = latencies,
attributes = attributes
};
AccelByteSDK.GetClientRegistry().GetApi().GetMatchmakingV2().CreateMatchTicket(matchPoolName, additionalParam, result =>
{
if (result.IsError)
{
Debug.LogWarning($"Client OnCreateMatchTicketFail, Error code: {result.Error.Code}\nError message:{result.Error.Message}");
}
else
{
Debug.Log("Client OnCreateMatchTicketSuccess");
}
});
}
}
Join game session
If the game session has already been created by the DS, another option to enter a game session is to join directly to the game session.
- Unreal Engine
- Unity
void AMyActor::JoinGameSession(const FString& SessionId)
{
ApiClient->Session.JoinGameSession(
SessionId,
AccelByte::THandler<FAccelByteModelsV2GameSession>::CreateUObject(this, &AMyActor::OnJoinGameSessionSuccess),
AccelByte::FErrorHandler::CreateUObject(this, &AMyActor::OnJoinGameSessionFail));
}
void AMyActor::OnJoinGameSessionSuccess(const FAccelByteModelsV2GameSession& Result)
{
UE_LOG(LogTemp, Log, TEXT("Join Game Session Success"));
}
void AMyActor::OnJoinGameSessionFail(int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("Join Game Session Fail, Error code: %d\nError message:%s"), ErrorCode, *ErrorMessage);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class CLientController : MonoBehaviour
{
public void JoinGameSession(string sessionId)
{
AccelByteSDK.GetClientRegistry().GetApi().GetSession().JoinGameSession(sessionId, result =>
{
if (result.IsError)
{
Debug.LogWarning($"Join Game Session Fail, Error code: {result.Error.Code}\nError message: {result.Error.Message}");
}
else
{
Debug.Log("Join Game Session Success");
}
});
}
}
Reconnect to server
If the game client is disconnected from the server or does not have a secret received from the previous service, the game client can get it from AGS Session by calling it directly via the function with the REST API response.
- Unreal Engine
- Unity
void AMyActor::GetGameSessions()
{
ApiClient->SessionBrowser.GetGameSessions(
EAccelByteSessionType::dedicated,
GameMode,
AccelByte::THandler<FAccelByteModelsSessionBrowserGetResult>::CreateUObject(this, &AMyActor::OnGetGameSessionsSuccess),
AccelByte::FErrorHandler::CreateUObject(this, &AMyActor::OnGetGameSessionsFail) );
}
void AMyActor::OnGetGameSessionsSuccess(const FAccelByteModelsSessionBrowserGetResult& Result)
{
UE_LOG(LogTemp, Log, TEXT("OnGetGameSessionsSuccess "));
}
void AMyActor::OnGetGameSessionsFail(int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("OnGetGameSessionsFail, Error code: %d\nError message:%s"), ErrorCode, *ErrorMessage);
}
public void GetGameSessions()
{
var session = AccelByteSDK.GetClientRegistry().GetApi().GetSession();
var attributeRequest = new Dictionary<string, object>()
{
{ "type", "dedicated" },
};
session.QueryGameSession(attributeRequest, result =>
{
if (result.IsError)
{
// Do something if QueryGameSession has an error
Debug.Log($"Error QueryGameSession, Error Code: {result.Error.Code} Error Message: {result.Error.Message}");
return;
}
// Do something if QueryGameSession succeeds
});
}
Player validation
The game client attempts to connect to the DS and the DS validates the secret key based on the player's user ID. Once validated, the game client can successfully connect to the DS.
Generate TOTP
The game client requests time-based OTP (TOTP) to the SDK by the session secret. Conversely, the DS will also check the TOTP value sent by the game client using the same secret. To compare the two values on both sides, you can use the following function:
- Unreal Engine
- Unity
void AMyActor::GenerateTOTP(const FString& SecretKey)
{
const FString Value = FAccelByteUtilities::GenerateTOTP(SecretKey);
UE_LOG(LogTemp, Log, TEXT("GenerateTOTP value = %s"), *Value);
}
using AccelByte.Api;
using AccelByte.Core;
using AccelByte.Utils;
using UnityEngine;
public class ClientController : MonoBehaviour
{
public void GenerateTOTP(string secretKey)
{
var totpUtils = new TotpUtils();
string value = totpUtils.GenerateTotp(secretKey);
Debug.Log($"GenerateTOTP value = {value}");
}
}
Client travel with TOTP
- Unreal Engine
- Unity
void AMyActor::ClientTravel(const FString& TOTP)
{
TravelUrl += FString::Printf(TEXT("?totp=%s?userid=%s"), *TOTP, *ApiClient->CredentialsRef->GetUserId());
auto PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
PC->ClientTravel(TravelUrl, TRAVEL_Absolute);
}
using AccelByte.Core;
using AccelByte.Utils;
using UnityEngine;
public class ClientController : MonoBehaviour
{
public void ConnectToDS(string serverIp, ushort port, string totp)
{
// Get the local player's User ID from the AccelByte SDK
string userId = AccelByteSDK.GetClientRegistry().GetApi().session.UserId;
// 1. Format your custom data as a query string
string queryStringPayload = $"?totp={totp}&userId={userId}";
// 2. Convert the query string to a byte array
byte[] payloadBytes = Encoding.UTF8.GetBytes(queryStringPayload);
// 3. Set the payload in the NetworkConfig's ConnectionData
NetworkManager.Singleton.NetworkConfig.ConnectionData = payloadBytes;
// 4. Set connection details on the transport
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
transport.SetConnectionData(serverIp, port);
// 5. Start the client. The payload will be sent during the handshake.
NetworkManager.Singleton.StartClient();
}
}
DS check validity
- Unreal Engine
- Unity
void AAccelByteGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
UE_LOG(LogAccelByteDSTest, Warning, TEXT("Option: %s, Address %s"), *Options, *Address);
TArray<FString> Out;
Options.ParseIntoArray(Out, TEXT("?"), true);
FString TOTP{};
FString UserId{};
for (auto Option : Out)
{
FString Param, Value;
Option.Split(TEXT("="), &Param, &Value);
if (Param == TEXT("totp"))
{
TOTP = Value;
}
if (Param == TEXT("userid"))
{
UserId = Value;
}
}
bool Valid = FAccelByteUtilities::ValidateTOTP(Secret, TOTP, UserId);
UE_LOG(LogAccelByteDSTest, Log, TEXT("User Validity = %s"), Valid ? TEXT("Valid") : TEXT("Invalid"));
if (!Valid)
{
return;
}
// Forward to PreLogin
Super::PreLogin(Options, Address, UniqueId, ErrorMessage);
}
using AccelByte.Core;
using AccelByte.Utils;
using UnityEngine;
public class DSController : MonoBehaviour
{
void Start()
{
var networkManager = NetworkManager.Singleton;
networkManager.NetworkConfig.ConnectionApproval = true;
networkManager.ConnectionApprovalCallback += ApprovalCheck;
networkManager.StartServer();
}
private void ApprovalCheck(
NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
// --- Fetch the Payload ---
byte[] payloadBytes = request.Payload;
string queryString = Encoding.UTF8.GetString(payloadBytes);
Debug.Log($"Server: Received connection request from client {request.ClientNetworkId}.");
Debug.Log($"Server: Raw query string payload: {queryString}");
// --- Parse the Query String ---
// You'll need a way to parse the query string.
// One way is to manually split it, or use a utility.
// For example, a simple manual parse:
string totp = "";
string userId = "";
// Remove the leading '?' if present
if (queryString.StartsWith("?"))
{
queryString = queryString.Substring(1);
}
string[] pairs = queryString.Split('&');
foreach (string pair in pairs)
{
string[] keyValue = pair.Split('=');
if (keyValue.Length == 2)
{
string key = keyValue[0];
string value = keyValue[1]; // You might want to HttpUtility.UrlDecode this value
if (key == "totp")
{
totp = value;
}
else if (key == "userId")
{
userId = value;
}
}
}
Debug.Log($"Server: Parsed TOTP: {totp}, UserId: {userId}");
// --- Your Validation Logic ---
var totpUtils = new TotpUtils();
// The 'Secret' variable is the server secret received from the DSHub's
// 'SessionServerSecretReceived' event when the DS first connected.
// It should be stored securely by the server.
bool isValid = totpUtils.ValidateTOTP(sessionSecret, totp, userId);
Debug.Log($"User Validity = {(isValid ? "Valid" : "Invalid")}");
response.Approved = isValid;
response.CreatePlayerObject = true;
if (isValid)
{
Debug.Log($"Server: Approved connection for client {request.ClientNetworkId}.");
}
else
{
response.Reason = "Validation failed.";
Debug.LogWarning($"Server: Denied connection for client {request.ClientNetworkId}. Reason: {response.Reason}");
}
}
}