Skip to main content

Integrate player validation in game session

Last updated on May 6, 2024
info

AccelByte Gaming Services (AGS) Game Unity SDK does not support this feature yet. This article will be updated once Unity support is added..

Overview

Session Service Player Validation is a feature in the AccelByte Gaming Services (AGS) Play service that enhances the level of security when players enter a game session maintained by a dedicated server. The dedicated server (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 game client can successfully connect to the DS.

This document explains the implementation of the Session service player validation feature on both the game client and DS sides from AccelByte's Unreal SDK Plugin.

Prerequisites

Before completing this guide, you must know how to do the following:

  • Log in to the AGS Access Service.
  • Configure matches.
  • Configure and join game session.

Flow and code snippet

To make it easier to explain the process of implementing session player validation, you can think of it as two parts: namely, the DS side and the game client side. On the DS side, this article explains how the server can generate secrets. Meanwhile, on 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 DSHub websocket connection from the Session service.

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, including how to obtain secrets 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 the Session service 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, the server 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 requested TOTP (Time-based OTP) to SDK by Session secret. On the other hand, DS will 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);
}