feat(auth): backward-compatible TTL check on SSO auth_ticket

Pairs with the CMS-side change introducing auth_ticket_expires_at (60s
expiry written on every ticket issuance). Without an emulator-side
verification the column was advisory only — this commit gates every
SELECT that resolves a user by auth_ticket on

    auth_ticket = ?
    AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW())

The NULL branch preserves backward-compatibility: CMS deployments that
do not yet populate the column keep working exactly like before
(every ticket passes the WHERE clause as soon as auth_ticket matches),
and the TTL takes effect automatically the moment a CMS starts writing
the expiry value.

Five SELECTs touched:
- SessionEndpoints.java (cms-issued SSO + remember-token flow)
- HabboManager.loadHabbo (game client login by ticket)
- SecureLoginEvent (legacy handshake path)

DB schema delivered both ways:
- Database Updates/Own_Database_RunFirst/020_auth_ticket_ttl.sql:
  idempotent ALTER, skips if column already present (information_schema
  guard so re-running the bundle is safe).
- Default Database/FullDatabase.sql: column added to the `users` table
  definition for fresh installs.

Bumps the emulator version to 4.2.7.
This commit is contained in:
medievalshell
2026-05-19 00:46:58 +02:00
parent 53b7dba185
commit e334a3e0ac
6 changed files with 43 additions and 6 deletions
@@ -0,0 +1,36 @@
-- ============================================================================
-- 020_auth_ticket_ttl.sql
--
-- Adds an explicit expiry timestamp to the SSO auth_ticket on `users`.
--
-- The CMS issuing the ticket is expected to populate auth_ticket_expires_at
-- (e.g. NOW() + INTERVAL 60 SECOND) on every login redirect. The emulator-
-- side SELECT queries that look up a user by auth_ticket have been changed to
--
-- WHERE auth_ticket = ?
-- AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW())
--
-- The NULL branch keeps backward-compatibility with CMS deployments that do
-- not populate the column yet: existing rows continue to authenticate the
-- same way they always did, and the TTL kicks in only once the CMS starts
-- writing the expiry value.
--
-- Idempotent: skips the ALTER if the column already exists.
-- ============================================================================
SET @col_exists = (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND COLUMN_NAME = 'auth_ticket_expires_at'
);
SET @ddl = IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `auth_ticket_expires_at` TIMESTAMP NULL DEFAULT NULL AFTER `auth_ticket`',
'SELECT ''auth_ticket_expires_at already present, skipping'' AS info'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
+1
View File
@@ -30682,6 +30682,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`points` int(11) NOT NULL DEFAULT 10, `points` int(11) NOT NULL DEFAULT 10,
`online` enum('0','1','2') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0', `online` enum('0','1','2') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0',
`auth_ticket` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '', `auth_ticket` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '',
`auth_ticket_expires_at` timestamp NULL DEFAULT NULL,
`remember_token_hash` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', `remember_token_hash` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`remember_token_expires_at` int(11) unsigned NOT NULL DEFAULT 0, `remember_token_expires_at` int(11) unsigned NOT NULL DEFAULT 0,
`ip_register` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, `ip_register` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId> <groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId> <artifactId>Habbo</artifactId>
<version>4.2.6</version> <version>4.2.7</version>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -95,7 +95,7 @@ public class HabboManager {
int userId = 0; int userId = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) { PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
statement.setString(1, sso); statement.setString(1, sso);
try (ResultSet s = statement.executeQuery()) { try (ResultSet s = statement.executeQuery()) {
if (s.next()) { if (s.next()) {
@@ -121,7 +121,7 @@ public class HabboManager {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? LIMIT 1")) { PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
statement.setString(1, sso); statement.setString(1, sso);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
if (set.next()) { if (set.next()) {
@@ -104,7 +104,7 @@ public class SecureLoginEvent extends MessageHandler {
// First, look up the user ID to check for ghost sessions // First, look up the user ID to check for ghost sessions
int lookupUserId = 0; int lookupUserId = 0;
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection(); try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) { java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
stmt.setString(1, sso); stmt.setString(1, sso);
try (java.sql.ResultSet rs = stmt.executeQuery()) { try (java.sql.ResultSet rs = stmt.executeQuery()) {
if (rs.next()) { if (rs.next()) {
@@ -50,7 +50,7 @@ final class SessionEndpoints {
if (ssoTicket != null && !ssoTicket.isEmpty()) { if (ssoTicket != null && !ssoTicket.isEmpty()) {
try (PreparedStatement lookup = conn.prepareStatement( try (PreparedStatement lookup = conn.prepareStatement(
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) { "SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
lookup.setString(1, ssoTicket); lookup.setString(1, ssoTicket);
try (ResultSet rs = lookup.executeQuery()) { try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) userId = rs.getInt("id"); if (rs.next()) userId = rs.getInt("id");
@@ -134,7 +134,7 @@ final class SessionEndpoints {
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection(); try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement lookup = conn.prepareStatement( PreparedStatement lookup = conn.prepareStatement(
"SELECT id, username FROM users WHERE auth_ticket = ? LIMIT 1")) { "SELECT id, username FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
lookup.setString(1, ssoTicket); lookup.setString(1, ssoTicket);
try (ResultSet rs = lookup.executeQuery()) { try (ResultSet rs = lookup.executeQuery()) {
if (!rs.next()) { if (!rs.next()) {