mhlab

A journey through multiplayer games engineering

Backend Services for Games - Auth Service #2

In the previous article I’ve built the authentication functionality. It outputs the previously mentioned Auth Token: a token that allows the client to call protected API in a safe way.

But, as I said, a new problem arises:

The user must be able to continue with their session without providing login credentials every few minutes.

Refresh the token

The easiest method to implement the refreshing of the auth token is… Using the current and valid auth token to request a new one.

Very easy.

Let’s write the extension method for the client:

public static async ValueTask<Response<RefreshResponse>> RefreshToken(this IBackendClient client)
{
    const string endpoint = "Auth/refresh";

    if (!client.IsAuthTokenSet)
        return new Response<RefreshResponse>(null, HttpStatusCode.Unauthorized);
    
    var response = await client.GetAsync<RefreshResponse>(endpoint);
    
    if (response.IsSuccess)
        client.SetAuthToken(response.Data.AuthToken);

    return response;
}

As you can see, the code is simple. The only interesting detail is the IsAuthTokenSet check: in this way I can avoid to send the actual network request if I already know it will fail.

Now it’s the controller’s turn:

/// <summary>
/// Provides a new Auth Token if the Authorization header is valid.
/// </summary>
/// <response code="200">The Auth Token is valid and a new one has been returned.</response>
/// <response code="401">The Auth Token is not valid.</response>
[ProducesResponseType(typeof(RefreshResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[HttpGet("refresh")]
public async ValueTask<IActionResult> RefreshToken()
{
    var token = _tokenGenerator.Generate(Enumerable.Empty<Claim>());

    return Ok(new RefreshResponse(token));
}

And now I just need to add some more tests:

[Test]
public async Task Refresh_Success()
{
    await _client.Authenticate(
        new AuthenticationRequest("correctUsername", "correctPassword")
    );

    var refreshResult = await _client.RefreshToken();
    
    Assert.True(refreshResult.IsSuccess);
    Assert.False(string.IsNullOrWhiteSpace(refreshResult.Data.AuthToken));
    Assert.AreEqual(HttpStatusCode.OK, refreshResult.StatusCode);
}

[TestCase("TheWrongToken")]
[TestCase(null)]
public async Task Refresh_Invalid_Token_Fail(string token)
{
    await _client.Authenticate(
        new AuthenticationRequest("correctUsername", "correctPassword")
    );
    
    _client.SetAuthToken(token);

    var refreshResult = await _client.RefreshToken();
    
    Assert.False(refreshResult.IsSuccess);
    Assert.IsNull(refreshResult.Data);
    Assert.AreEqual(HttpStatusCode.Unauthorized, refreshResult.StatusCode);
}

Good: they all turn green!

I think the code is pretty straightforward and self-explanatory, so I won’t dive into detailed explanations. More complex refresh strategies can be implemented (for example, using a dedicated refresh token for obtaining a new valid auth token), but for now this simple one is enough.

Accounts authentication

When I implemented the Authenticate functionality I used a hardcoded username/password pair. But of course I need a way to test the authentication against actual accounts.

I need some new functionalities in order to achieve this:

  • register a new account
  • find an account

This is really simple and limited, but it is a good starting point if you want to build on top of it.

Register a new account

From the user’s perspective, this feature is really simple:

The user should be able to register their own account providing email, username and password.

On the developer’s side, instead, it’s more complex and involves some reasoning.

First thing first: I need a database to store and persist these accounts. There are a lot of DBMS out there: what should I pick? But also: relational? Document-based? Key-value stores? What?

I usually pick a relational database when I haven’t a clear picture of the data access patterns used in my service. Relational database are reasonably good in every scenario: you can build on them while collecting info about the data-flow in your application and - if needed - introducing a specialized storage. Definitely a good compromise, imho.

Multiple options are available as DBMS in this area. The classic MySQL, SQL Server and PostgreSQL, the newer AWS Aurora, Azure SQL Database, CockroachDB (based on PostgreSQL), MariaDB (based on MySQL), etc.

Usually I go with MySQL/MariaDB, but for this series I want to try something different (for learning purposes). I am particularly curious about PostgreSQL/CockroachDB, so let’s check it.

PostgreSQL is solid an well-tested, CockroachDB adds improved scalability and resilience to the package.

They both use the same driver for C#, so I can optimistically switch between them at any time. It also offers a provider for Entity Framework Core, the ORM of choice for this project.