Using authentication tokens
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
Warning
Take care to ensure that the authentication token secret is only available in server builds and is never present in any build you distribute, even if it is never used.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
.
Note
Although one would typically escape vertical bar characters with %7C when performing URL encoding, Unreal does not perform URL decoding and the vertical bars should remain in the token as-is.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 );