Using authentication tokens

Generate and use authentication tokens to grant clients access to sessions

Configure the shared secret

In order to use authentication tokens, you must first establish a shared secret known only to your backend and your game servers. Your backend will use this secret to sign authentication tokens for clients to provide when connecting to servers and your game servers will verify the signature on these tokens when accepting incoming connections.

The authentication secret must be exactly 32 bytes. It’s best to generate these bytes using a cryptographically secure random number generator.

Once you have your secret generated, you can configure it within your engine as follows:

Convert the secret to hexadecimal text and store it in <Game>/Content/SnapNet/auth-token-secret.txt where <Game> is the folder that contains your .uproject file. The SnapNet plugin will automatically package the secret and exclude it from all non-server builds. The secret will then be automatically loaded and used by the DTLS transport when present.

Sample auth-token-secret.txt contents: 580c5b97c79d787660ba55ad334eeccb4df63193274b3ffec343b5f86795b1bf

To set the authentication token secret, simply call the snapnet_transport_dtls_set_authentication_token_secret function.

#if GAME_SERVER_SECRETS
const uint8_t secret[] = {
    0x58, 0x0c, 0x5b, 0x97, 0xc7, 0x9d, 0x78, 0x76,
    0x60, 0xba, 0x55, 0xad, 0x33, 0x4e, 0xec, 0xcb,
    0x4d, 0xf6, 0x31, 0x93, 0x27, 0x4b, 0x3f, 0xfe,
    0xc3, 0x43, 0xb5, 0xf8, 0x67, 0x95, 0xb1, 0xbf
};
snapnet_transport_dtls_set_authentication_token_secret( transport, secret, sizeof( secret ) );
#endif

Generating authentication tokens

Because the authentication tokens are intended to be created by your game backend, they have been designed to be easily generated in languages commonly used for backend development. Some sample implementations are provided at the end of this section for convenience.

Authentication Token Structure

Authentication tokens are UTF-8 encoded strings that consist of 4 fields separated by vertical bars as follows:

<User ID>|<Session ID>|<Expiration Time>|<Signature>

User ID

The user ID field can be up to 63 characters and should uniquely identify the user for whom your backend is granting session access. The appropriate ID to use will vary based on your backend but could be a username, UUID, or anything unique to that particular player. SnapNet will only allow one simultaneous connection per user ID.

Session ID

The session ID field can be up to 63 characters and should uniquely identify the session to which the user is being granted access. This must be a value that is also known on the server and configured before any connections are attempted. Typically, the session ID would come from your matchmaker or server list. SnapNet will only accept an incoming connection if the session ID matches what has been configured on the server.

To configure the session ID, call USnapNetDTLSTransport::SetSessionId.

SnapNetServer->GetTransport<USnapNetDTLSTransport>()->SetSessionId( SessionId );

To configure the session ID, call snapnet_transport_dtls_set_session_id at any point prior to calling snapnet_server_start.

snapnet_transport_dtls_set_session_id( transport, session_id );
snapnet_server_start( server );

Expiration Time

The expiration time field is a unix timestamp indicating when the authentication token expires. Tokens should be short-lived and only allow enough time for the client to receive the token from the backend and connect to the desired server. A value of 45 seconds is recommended.

Signature

The signature field is a hex-encoded HMAC-SHA256 digest of the first three fields separated by vertical bars:

<User ID>|<Session ID>|<Expiration Time>

For example, when generating a token with a user ID of user123, a session ID of session-abc, and an expiration time of 1641092820, you would generate the signature on the following UTF-8 encoded string:

user123|session-abc|1641092820

Using a secret of 580c5b97c79d787660ba55ad334eeccb4df63193274b3ffec343b5f86795b1bf the final token would be:

user123|session-abc|1641092820|dfb224913efa1ef8a85e9deb69721674bace32ea86cc23ffb921ad5fec0fadf1

Sample Backend Implementations

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "time"
)

func generateToken(userId string, sessionId string, expiration int, secret string) string {
    secretBytes, _ := hex.DecodeString(secret)
    hmac := hmac.New(sha256.New, secretBytes)
    hmac.Write([]byte(fmt.Sprintf("%s|%s|%d", userId, sessionId, expiration)))
    signature := hex.EncodeToString(hmac.Sum(nil))
    return fmt.Sprintf("%s|%s|%d|%s", userId, sessionId, expiration, signature)
}

exampleUserId := "user123"
exampleSessionId := "session-abc"
exampleExpiration := int(time.Now().Unix())+45
exampleSecret := "580c5b97c79d787660ba55ad334eeccb4df63193274b3ffec343b5f86795b1bf"
token := generateToken(exampleUserId, exampleSessionId, exampleExpiration, exampleSecret)
fmt.Print(token)
const crypto = require('crypto');

function generateToken(userId, sessionId, expiration, secret) {
    const hmac = crypto.createHmac('sha256', Buffer.from(secret, 'hex'));
    hmac.update(`${userId}|${sessionId}|${expiration}`);
    const signature = hmac.digest('hex');
    return `${userId}|${sessionId}|${expiration}|${signature}`;
}

const exampleUserId = 'user123';
const exampleSessionId = 'session-abc';
const exampleExpiration = Math.floor(Date.now() / 1000) + 45;
const exampleSecret = '580c5b97c79d787660ba55ad334eeccb4df63193274b3ffec343b5f86795b1bf';
const token = generateToken(exampleUserId, exampleSessionId, exampleExpiration, exampleSecret);
console.log(token);
import hashlib
import hmac
import time

def generate_token(user_id, session_id, expiration, secret):
    signature = hmac.new(bytes.fromhex(secret), f'{user_id}|{session_id}|{expiration}'.encode('utf8'), hashlib.sha256).hexdigest()
    return f'{user_id}|{session_id}|{expiration}|{signature}'

example_user_id = 'user123';
example_session_id = 'session-abc';
example_expiration = int(time.time()) + 45;
example_secret = '580c5b97c79d787660ba55ad334eeccb4df63193274b3ffec343b5f86795b1bf';
token = generate_token(example_user_id, example_session_id, example_expiration, example_secret)
print(token)

Connecting with authentication tokens

Once your client has an authentication token, it must provide it as part of the connection process. You can do this as follows:

To connect using an authentication token, specify it using the AuthToken parameter in the URL when calling USnapNetDTLSTransport::SetupClientByUrl.

SnapNetClient->GetTransport()->SetupClientByUrl( TEXT( "127.0.0.1?AuthToken=user123|session-abc|1641092820|dfb224913efa1ef8a85e9deb69721674bace32ea86cc23ffb921ad5fec0fadf1" ) );

The authentication token can be set by calling snapnet_transport_dtls_set_authentication_token at any point prior to calling snapnet_client_connect.

snapnet_transport_dtls_set_authentication_token( transport, "user123|session-abc|1641092820|dfb224913efa1ef8a85e9deb69721674bace32ea86cc23ffb921ad5fec0fadf1" );
snapnet_client_connect( client );