Implement user ownership validation in Extend Service Extension
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.
- C#
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.
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.
- C#
- Add
OwnerId
property inModel/GuildProgressData.cs
underNamespace
property.
...
[JsonPropertyName("owner_id")]
public string OwnerId { get; set; } = "";
...
- Add user id from access token payload to gRpc context's
UserState
inClasses/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);
...
- Modify
CreateOrUpdateGuildProgress
implementation inService/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()
});
}
- Modify
GetGuildProgress
implementation inService/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()
});
}
You could find more information about gRPC request handling here.