-
Notifications
You must be signed in to change notification settings - Fork 1
2. Authentication and authorization
Authentication is the process of identifying who is making a request. In this API, the Profile username identifies the logged-in user.
In this API, there are Profile and Player collections, which both contain user's data, but they have different purposes.
The Profile contains user's real-world information, such as username and password. It can also be extended in the future to hold some sensitive personal data such as name, email, phone, etc. This is why data from this collection should not be used inside the game or on webpages except, for example, on the user profile page if such exists.
The Player collection, on the other hand, can be used freely everywhere, and its data is not sensitive and can be shown to everyone.
In conclusion, the Profile data (username and password) is used for authentication only. The Player data is used everywhere else.
To identify the logged-in user by his username, Profile and Player objects for the user must be created first. As mentioned earlier, in this API there are Profile and Player collections, both containing user's data. While it is possible to create a Profile without creating a Player object, it is not recommended. Instead, it is better to create both at once by sending a POST request to the /profile endpoint with the following body:
// POST to /profile
{
"username": "user1", // unique username for logging-in
"password": "password", // string password for logging-in
"Player": {
"name": "User 1", // In-game unique name
"backpackCapacity": 453, // Any integer
"uniqueIdentifier": "1" // string unique identifier
}
}
The logging-in process can be broken down into multiple stages:
- The user provides their username and password.
- The system (game or webpage) sends the credentials to the API, as follows:
/auth/signIn POST
{
"username": "user1",
"password": "user1"
}
- The API checks whether these credentials are correct.
- If the credentials are correct, the access token will be returned*; if not, a 401 error will be returned.
*The response body will look as follows:
{
"_id": "665df7026bf5b8f670569ea2", // Profile _id (not Player)
"username": "user1", // Profile username
"__v": 0, // ignore this field
"Player": {
// User's Player data
},
"Clan": {
// User's Clan data, if user belongs to any
},
"accessToken": "long_string", // The access token for used for authentication
"tokenExpires": 1720110389 // For how long the access token can be used (Unix timestamp)
}
The access token is used for authentication. It contains the Profile username in encrypted form, and the API can determine permission rules based on this username. For example, suppose only Clan admins can delete the Clan. In that case, the API first checks whether the User is an admin of the Clan being deleted, and if he is, the Clan will be deleted. Permissions are applied to almost every endpoint of the API. Therefore, it is almost always required to authenticate the logged-in user. Authentication can be done by adding the Authorization header to the request with the word "Bearer", followed by a space and the access token given after successful logging-in:
"Authorization": "Bearer accessToken"
If authentication is required but the Authorization header is not provided, a 401 error will be returned.
The access token is not permanent and may expire after some period of time. If the token is expired, a 401 error will be returned.
Since the access token is used often, it makes sense to save it to the user's device and use it as a global variable across the entire system (game or webpage). The same thing can be done with its expiration timestamp. Notice that this token should be treated as a secret and should not be, for example, printed to the terminal or shown in the UI.
Different approaches can be used to determine whether the token is expired or not. The token can be checked at the start of the game, and if it is expiring within one day, the user can be redirected to the login page. Here is a TS and C# code example on how it can be checked:
TS:
function isTokenExpired(tokenExpires: number) {
const currentTime = Math.floor(Date.now() / 1000);
return tokenExpires <= currentTime + 86400; // + seconds in one day
};
C#:
public bool IsTokenExpired(long tokenExpires)
{
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return tokenExpires <= currentTime + 86400; // + seconds in one day
}
Also, it is good to have a fallback mechanism for each request in case the 401 error is returned. For example, redirect the user to the login page.
It is also worth mentioning that the typed-in user password should not be saved to the device memory, for example, for auto logging-in when the token expires.
Authorization is the process of identifying whether the logged-in user has permission for the request. It relies on successful authentication, which determines who is logged in. If there is any problem with authentication, the 401 error will be returned first. Only if the API can identify the logged-in user will it authorize (or not) the user's request. If the user does not have permission, the 403 error will be returned.
In this API, the rule-based authorization approach is used. The API determines rules for each user individually, providing more flexibility compared to the role-based approach. The rules are predefined, and errors related to authorization should be prevented. For example, if a user is not an admin of the Clan and cannot delete the Clan, then the "Delete Clan" button should not be displayed for the user.
Notice that not all endpoints have rules, and existing rules are meant to be as simple as possible. For example, a logged-in user can modify only his own Profile information, but not others'. Following such rules is simple in the UI since the profile page usually contains the logged-in user's information only, and the user cannot change another Profile's data accidentally. However, some permission errors, as mentioned earlier, can be harder to prevent and require hiding or disabling UI elements.
These rules determine permissions based on the username (who), resource (Clan, Player etc.) and action (CRUD operations). Different libraries can help to abstract and decouple rules from the primary code. For example, the CASL library can be used for TS, and Casbin for both C# and TS languages.
The authorization rules used on the API side (if any) can be found in the Swagger description for each endpoint separately.