Skip to main content

Implement user ownership validation in Extend Service Extension

Last updated on May 2, 2025

Overview

By default, Extend Service Extension app template only includes a basic user verification mechanism. However, if user validation is required for a specific set of data, you can easily implement it by customizing your code.

Prerequisites

You have cloned the Extend Service Extension app template.

git clone https://github.com/AccelByte/extend-service-extension-csharp

Implement user ownership validation

As an example, Extend Service Extension app template can be extended to incorporate ownership information for each guild data. During guild data creation, it will assign the currently authenticated user ID as the owner ID and store this information along with guild data. Subsequently, when a user attempts to access the guild data, it will verify the user ID and denies access if it does not match with stored owner ID.

note

This mechanism relies on the user ID stored in the sub field of the user access token payload. Since OAuth client access tokens do not contain a sub field, ownership information will not be stored when using client credentials.

  1. Add OwnerId property in Model/GuildProgressData.cs under Namespace property.
...

[JsonPropertyName("owner_id")]
public string OwnerId { get; set; } = "";

...
  1. Add user id from access token payload to gRpc context's UserState in Classes/AuthorizationInterceptor.cs.
...

qPermission = (new Regex(@"\{(namespace|NAMESPACE)\}")).Replace(qPermission, (m) => _ABProvider.Sdk.Namespace);
int actNum = (int)qAction;
bool b = _ABProvider.Sdk.ValidateToken(authParts[1], qPermission, actNum);
if (!b)
throw new RpcException(new Status(StatusCode.PermissionDenied, $"Permission {qPermission} [{qAction}] is required."));

// Add these code below after ValidateToken
var tokenPayload = _ABProvider.Sdk.ParseAccessToken(authParts[1], false);
if (tokenPayload == null)
throw new RpcException(new Status(StatusCode.Unauthenticated, $"Invalid access token payload."));

string userId = "";
if (tokenPayload.Sub != null)
userId = tokenPayload.Sub;

context.UserState.Add("loginUserId", userId);

...
  1. Modify CreateOrUpdateGuildProgress implementation in Service/MyService.cs file.
public override Task<CreateOrUpdateGuildProgressResponse> CreateOrUpdateGuildProgress(CreateOrUpdateGuildProgressRequest request, ServerCallContext context)
{
string actualGuildId = request.GuildProgress.GuildId.Trim();
if (actualGuildId == "")
actualGuildId = Guid.NewGuid().ToString().Replace("-", "");

string gpKey = $"guildProgress_{actualGuildId}";

string loginUserId = "";
if (context.UserState.TryGetValue("loginUserId", out object? tempValue))
{
if (tempValue != null)
loginUserId = tempValue.ToString()!;
}
else
throw new Exception("No login user id data in context.");

try
{
//check whether guild data specified by guild is exists or not
var guildData = _ABProvider.Sdk.Cloudsave.AdminGameRecord.AdminGetGameRecordHandlerV1Op
.Execute<GuildProgressData>(gpKey, request.Namespace);
if (guildData == null)
throw new Exception("NULL response from cloudsave service.");

//if it exists, check owner user id.
if (guildData.Value != null)
{
if (guildData.Value.OwnerId != loginUserId)
throw new Exception("You don't have access to this data.");
}

//Guild data exists and belong to login user. Proceed to update
}
catch (HttpResponseException hrx)
{
if (hrx.StatusCode != HttpStatusCode.NotFound)
throw;

//Guild data does not exists. Proceed to create.
}

var gpValue = GuildProgressData.FromGuildProgressGrpcData(request.GuildProgress);
gpValue.GuildId = actualGuildId;
gpValue.OwnerId = loginUserId;

var response = _ABProvider.Sdk.Cloudsave.AdminGameRecord.AdminPostGameRecordHandlerV1Op
.Execute<GuildProgressData>(gpValue, gpKey, request.Namespace);
if (response == null)
throw new Exception("NULL response from cloudsave service.");

GuildProgressData savedData = response.Value!;

return Task.FromResult(new CreateOrUpdateGuildProgressResponse()
{
GuildProgress = savedData.ToGuildProgressGrpcData()
});
}
  1. Modify GetGuildProgress implementation in Service/MyService.cs file.
public override Task<GetGuildProgressResponse> GetGuildProgress(GetGuildProgressRequest request, ServerCallContext context)
{
string gpKey = $"guildProgress_{request.GuildId.Trim()}";

string loginUserId = "";
if (context.UserState.TryGetValue("loginUserId", out object? tempValue))
{
if (tempValue != null)
loginUserId = tempValue.ToString()!;
}
else
throw new Exception("No login user id data in context.");

var response = _ABProvider.Sdk.Cloudsave.AdminGameRecord.AdminGetGameRecordHandlerV1Op
.Execute<GuildProgressData>(gpKey, request.Namespace);
if (response == null)
throw new Exception("NULL response from cloudsave service.");

//if it exists, check owner user id.
if (response.Value != null)
{
if (response.Value.OwnerId != loginUserId)
throw new Exception("You don't have access to this data.");
}

GuildProgressData savedData = response.Value!;
return Task.FromResult(new GetGuildProgressResponse()
{
GuildProgress = savedData.ToGuildProgressGrpcData()
});
}
info

You could find more information about gRPC request handling here.