Skip to content

Reduced JWT tokens for HTTP login - #4967

Draft
ranisalt wants to merge 3 commits into
otland:masterfrom
ranisalt:http-jwt
Draft

Reduced JWT tokens for HTTP login#4967
ranisalt wants to merge 3 commits into
otland:masterfrom
ranisalt:http-jwt

Conversation

@ranisalt

@ranisalt ranisalt commented Jul 5, 2025

Copy link
Copy Markdown
Member

Pull Request Prelude

Changes Proposed

See #4963

Current issues:

  • Tibia client sends a malformed packet for login (does not decrypt), could be my client misbehaving.
  • OTClient crashes hard because the token is too long for it's expectations. OTClient bugs printing that a message was received with the wrong checksum. Probably just a bug.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR replaces the existing random session key mechanism with JWT-based tokens for both in-game and HTTP login flows, refactors Base64 and HMAC utilities, and removes IP arguments from HTTP handlers.

  • Refactor cryptographic helpers (transformToSHA1, hmac) to use RAII and custom deleters
  • Implement URL-safe, no-padding Base64 encode/decode and add a new sessionSecret config
  • Switch in-game and HTTP login to HS256 JWTs, update router and tests to drop IP parameters

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/tools.cpp Introduce C_ptr/Deleter and refactor transformToSHA1/hmac
src/base64.cpp Replace BIO-based Base64 with custom URL-safe, unpadded encoder/decoder
src/configmanager.h Add SESSION_SECRET key to string_config_t
src/configmanager.cpp Load/base64::decode the sessionSecret from Lua config
src/protocollogin.cpp Generate in-game session tokens as JWT(HS256)
src/rsa.cpp Add error check and exception on RSA decrypt failure
src/http/login.h Update handle_login signature to remove IP parameter
src/http/login.cpp Refactor HTTP login to JWT tokens, drop DB session insert/IP use
src/http/cacheinfo.h Update handle_cacheinfo signature to remove IP parameter
src/http/cacheinfo.cpp Refactor cacheinfo handler to drop IP
src/http/router.cpp Adjust router to call handlers without IP
src/tests/test_base64.cpp Update tests to expect unpadded Base64 output
src/http/tests/test_login.cpp Update HTTP login tests to use new handler signature
src/http/tests/test_cacheinfo.cpp Update cacheinfo tests to use new handler signature
CMakeLists.txt Add vcpkg features, HTTP compile-def, libmysql/mariadb option
config.lua.dist Introduce sessionSecret setting for JWT support
Comments suppressed due to low confidence (3)

src/http/login.cpp:34

  • [nitpick] The new JWT-based session key generation logic in handle_login lacks dedicated unit tests to verify the correct header.payload.signature structure and token expiration handling.
std::pair<status, json::value> tfs::http::handle_login(const json::object& body)

config.lua.dist:30

  • Add a note clarifying that sessionSecret should be provided as a base64-encoded string (not plaintext), so users know to encode their secret before configuring.
sessionSecret = "c2VjcmV0"  -- CHANGE THIS VALUE!

src/protocollogin.cpp:125

  • Double base64-encoding the JWT inflates the token and may break client expectations; consider passing the raw sessionKey (the header.payload.signature) directly instead of encoding it again.
	output->addString(tfs::base64::encode(sessionKey));

Comment thread src/base64.cpp
Comment thread src/rsa.cpp Outdated
@gesior

gesior commented Jul 7, 2025

Copy link
Copy Markdown
Contributor

Current issues:

  • Tibia client sends a malformed packet for login (does not decrypt), could be my client misbehaving.
  • OTClient crashes hard because the token is too long for it's expectations. Probably just a bug.

I think I know why it happens. Session key is send encrypted using RSA. Tibia uses 1024-bit RSA key, which is 128 bytes, from which 117 127 bytes are for data (11 1 for padding).
(EDIT: I tested how many bytes has RSA packet after decrypting, it has 127, as Tibia protocol does not use padding.)
I saw the same problem on some 8.x server, when some OTS acc. maker allowed very long logins and passwords.

All bytes from msg in this part of code:

// Get XTEA key
xtea::key key;
key[0] = msg.get<uint32_t>();
key[1] = msg.get<uint32_t>();
key[2] = msg.get<uint32_t>();
key[3] = msg.get<uint32_t>();
enableXTEAEncryption();
setXTEAKey(std::move(key));
// Enable extended opcode feature for otclient
if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) {
NetworkMessage opcodeMessage;
opcodeMessage.addByte(0x32);
opcodeMessage.addByte(0x00);
opcodeMessage.add<uint16_t>(0x00);
writeToOutputBuffer(opcodeMessage);
}
// Change packet verifying mode for QT clients
if (version >= 1111 && operatingSystem >= CLIENTOS_QT_LINUX && operatingSystem <= CLIENTOS_OTCLIENT_MAC) {
setChecksumMode(CHECKSUM_SEQUENCE);
}
// Web login skips the character list request so we need to check the client version again
if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) {
disconnectClient(fmt::format("Only clients with protocol {:s} allowed!", CLIENT_VERSION_STR));
return;
}
msg.skipBytes(1); // Gamemaster flag
auto sessionToken = tfs::base64::decode(msg.getString());
if (sessionToken.empty()) {
disconnectClient("Malformed session key.");
return;
}
if (operatingSystem == CLIENTOS_QT_LINUX) {
msg.getString(); // OS name (?)
msg.getString(); // OS version (?)
}
auto characterName = msg.getString();
uint32_t timeStamp = msg.get<uint32_t>();
uint8_t randNumber = msg.getByte();

must fit together into 117 bytes. IDK how many bytes are there for session token.

JWT won't work in this scenario. We need much shorter format ex. first format proposed here:
#4963 (comment)

4 lines (no JSON, as it wastes too many characters for formatting):

  1. account identifier (1-10 bytes)
  2. account password hash truncated to 20 first characters (20 bytes)
  3. 'created at' date as unix timestamp (10 bytes) - OPTIONAL, if we want sessions time limit
  4. SHA1 sign: checksum of first 3 lines (concat using ; separator) with key.pem SHA1 (SHA1 of RSA key of OTS) added at end of SHA1 sign truncated to 20 first characters (20 bytes)

I don't see any chance to put full client IP there. It would be possible, if we allow only IPv4, but with IPv6 it's too long.
If we want IP verification, we would need to add it as part of 4th line using IP as part of sign. Or add some weak IP verification by putting ex. 5 bytes of IP SHA1 hash and 5 bytes of sign of that IP.

EDIT:
Actually we don't need account ID and password as separate fields. We will know what account ID to load, because after session token, client sends character name, so all we need is some small checksum, to detect, if account ID/password changed ex. first 10 letters of SHA1.
4th line 'SHA1 sign' can be also shorter ex. 10 letters, it must be only secure enough to prevent someone from brute forcing it - we are still talking about brute forcing it by someone who knows password to account.

@ranisalt

ranisalt commented Jul 7, 2025

Copy link
Copy Markdown
Member Author

I see no value in adding a checksum at the end. However, sending the account id + timestamp and signing those with HMAC should be safe enough to assume it isn't fake and still be secure. It's a thinner version of JWT without the fixed header + JSON boilerplate

@gesior

gesior commented Jul 7, 2025

Copy link
Copy Markdown
Contributor

I see no value in adding a checksum at the end.

By checksum, I meant "4. SHA1 sign:", which is like HMAC-SHA1, but as I described, we must truncate it to 10-20 ASCII characters (from 40 in hexadecimal format). I didn't want to call it HMAC, because HMAC is known format and we will use something custom.
login.php/login.cpp returns this value as part of JSON, so it must be in format compatible with JSON string, without extra encoding (base64/hex). Otherwise it will add more bytes.

However, sending the account id + timestamp and signing those with HMAC should be safe enough to assume it isn't fake and still be secure.

Don't forget about password. We need some hash of password to make session token fail to login into game, after password to account is changed.

I checked how many bytes are used right now by Tibia 13.10, there are 50-70 free bytes (of 127 RSA-1024 bytes), even, if we allow character name with up to 30 letters.

Let's go back with discussion about format to #4963

@ranisalt ranisalt changed the title JWT tokens for HTTP login Reduced JWT tokens for HTTP login Jul 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants