Skip to main content

Integrate game session player validation

Last updated on October 24, 2024
info

AccelByte Gaming Services (AGS) Game SDK for Unity does not support game session player validation. This article will be updated once Unity support is added.

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.

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);
}

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.

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);
}

Start matchmaking

The game client can receive the notification by initiating matchmaking.

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);
}

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.

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);
}

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.

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);
}

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:

void AMyActor::GenerateTOTP(const FString& SecretKey)
{
const FString Value = FAccelByteUtilities::GenerateTOTP(SecretKey);
UE_LOG(LogTemp, Log, TEXT("GenerateTOTP value = %s"), *Value);
}
Client travel with TOTP
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);
}

DS check validity

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);
}