๐ AuthCore Security Model
This document describes how AuthCore protects player accounts and the server: password hashing, brute-force defense, session protection, premium-API failure handling, limbo isolation, and the mitigation of OWASP Top-10 and Minecraft-specific threats.
๐ Password Hashing
- Default algorithm: Argon2id (memory-hard, GPU/ASIC-resistant): 64 MiB memory, 3 iterations, parallelism 1, 16-byte hash.
- Per-user random salt, a fresh, cryptographically random salt is generated for every
hash and embedded in the self-contained hash string (
$argon2id$...), so verification works across restarts without storing the salt separately. - Alternatives (via
password-rules.password-hash-algorithm):
| Algorithm | Verdict |
|---|---|
argon2 |
โ Recommended default, memory-hard, resistant to GPU cracking |
bcrypt |
โ Proven adaptive hash (cost factor 12) |
scrypt |
โ Memory-hard alternative (N=16384, r=8, p=1) |
pbkdf2 |
โ ๏ธ Acceptable, HMAC-SHA256, 100 000 iterations |
sha-256 / sha-512 |
โ ๏ธ Fast hashes, less ideal for passwords; kept for legacy compatibility only |
md5 |
โ Insecure, do not use. If an old account still uses MD5-class hashes it maps to SHA-256 internally and contributes to the login risk score |
- Weak-algorithm warnings: the risk scorer adds points when an account's stored hash uses
md5/sha-256/sha-512(weak or non-memory-hard), and the console/security log flags such accounts so admins can force re-hashing. - PBKDF2 is self-contained (JDK only).
pbkdf2is implemented as a pure JDKSecretKeyFactory(format$pbkdf2-sha256$<iter>$<salt>$<hash>) instead of going through password4j, the test suite caught that password4j's PBKDF2 check could hang the server thread on some inputs; the JDK implementation is predictable, interruptible and behaves identically on every supported Minecraft version. - Argon2id fallback.
Encrypter.hash(...)never stores an unusable hash: an unknownpassword-hash-algorithmvalue (or a hashing exception) logs a warning and falls back to Argon2id, so registration and password resets never fail silently.md5is never stored - it hashes through SHA-256 internally (legacy compatibility) and flags the account as weak. - The same hasher backs
register,change-password, admin resets and web-panel resets. - Every hash path is exercised by the automated test suite (
tools/security-tests/, 57 checks) - round-trips for all 6 algorithms plus unique per-hash salts; see the test suite below.
๐ก๏ธ Brute-Force Defense Layers
Defense is layered so a single failure never exposes accounts:
- Max login attempts (
session.authentication.max-login-attempts, default 8), kick after repeated failures. - Cooldown after kick (
session.cooldown-after-kick-ms, default 2 min), the kicked player cannot immediately retry. - Per-IP rate limits (
session.rate-limit), max 10 joins per IP per minute by default; sliding-window counter, memory bounded at 10 000 keys with expired-window purge. - Progressive punishment (
session.progressive-punishment), exponential kick cooldowns (5 s โ 30 s โ 3 m by default, capped at 5 min) for repeat offenders. - Persistent account lock (
session.account-lock, default on), after 8 failed logins the account locks for 10 minutes; the lock is persisted to the database (lockUntilMs) so it survives restarts and works across network instances via Redis. - Captcha (
lobby.captcha), optional text captcha on/loginand/register, TTL-bound (default 1 min) and bounded at 512 pending entries. - Login intelligence (
session.intelligence), new-IP/new-country detection raises the risk score (0โ100), triggers webhook/email/security-log alerts abovealert-risk-threshold(60), and can optionally block logins from new countries. - Command cooldowns (
session.rate-limit.command-cooldown-ms, default 1 s), minimum delay between/loginand/registerexecutions per player, throttling scripted password-spray loops that bypass per-IP limits. - Fast-rejoin alert (
session.rate-limit.alert-on-fast-rejoin, default off), a disconnectโrejoin in underfast-rejoin-window-ms(default 500 ms) is a classic bot pattern; AuthCore fires a webhook + security-log alert (alert-only, no kick) so you can spot automated clients without punishing legitimate laggy players.
๐ Password Reuse Blocking
password-rules.history-size (default 5) remembers the last N passwords per account and
blocks reuse on change. Combined with allow-reuse = false, players cannot cycle back to an
old password, so a leaked old hash can never become the current one again. Set to 0 to disable
the history.
โญ Trusted Players (captcha bypass)
After a successful login the account is marked "trusted" for bypass-captcha-hours
(default 24 h) and skips the captcha on rejoins (session.trusted). This keeps repeat,
already-verified players friction-free while guests still prove they are not bots. Trust status
is stored in the database (trustedUntilMs), so it survives restarts.
๐๏ธ Network Device Fingerprint
On every login AuthCore computes a SHA-256 fingerprint of the login origin (IP + country) -
a strong "same network/device" signal, not a hardware fingerprint (impossible server-side). It
is stored per account in the deviceFingerprint DB column.
- A login from a changed fingerprint adds +15 to the risk score.
- A "Possible Account Sharing" webhook alert fires (account sharing/trading detection) -
the same signal that backs optional
block-on-new-country. - New logins just record the fingerprint; only changes are flagged, so legitimate roaming across IPs is noticed but not punished automatically.
๐ซ IP Allow/Deny Rules
config/authcore/ip-rules.conf (session.security.ip-rules-file, default ip-rules.conf)
provides a static, first-match-wins access list applied on join:
# block a known abuser, allow a trusted build proxy
deny 5.6.7.8
allow 1.2.3.4
- One rule per line:
allow <ip>ordeny <ip>;#lines are comments. - First match wins; if no rule matches the IP is allowed (default-allow).
- Denied IPs are kicked on join, before they can reach the lobby.
- The file is loaded at startup and on
/authcore reload.
๐ป Shadow-Ban Mode
session.shadow-ban (default off) hides why players are blocked. When enabled, security
blocks (e.g. block-on-new-country) disconnect the player with a generic
disconnect-reason ("Connection lost") instead of the revealing security message, while the
webhook alert still fires, so admins keep full visibility without tipping off the blocked
player (useful against account sharers who adapt to specific block messages).
๐ Auto-Whitelist
session.auto-whitelist.enabled (default off): successfully registered accounts are added
to the vanilla server whitelist automatically (applies while the server whitelist is
enabled). Works across 1.16.0 โ 26.1-26.2 via the Compat layer, so a registered player is never
locked out by an admin-managed whitelist.
๐ฏ Honeypot (auto-banning decoy port)
session.honeypot (default off) runs a fake server listener on its own port (default
25599). Every connection to it is treated as an attacker scan:
- The connecting IP is logged with a
HONEYPOT_HITsecurity-log entry and a console warning. - A permanent
deny <ip>rule is appended toip-rules.confand immediately reloaded - the scanner is banned for good and kicked on any real join attempt. - The listener runs on a daemon thread and never touches the server thread.
Scenario: enable it on a public server so port scanners probing 25599 permanently ban themselves.
๐ ๏ธ Maintenance Mode
session.maintenance (default off) blocks all joins with a custom kick-message. Toggle it
at runtime with /authcore maintenance on|off (persisted to settings.conf), useful during
world updates, backups or security incidents. Combine with security.webhook-url so the
maintenance window is visible in your alerting. /authcore validate performs a dry-run config
check before you rely on any of these settings.
๐งต Race-Condition Hardening
1.0.0 hardened the shared state that multiple threads (server thread, web panel daemon, Redis subscriber, honeypot, backup scheduler) touch:
config/messagesand user session fields arevolatile, reloads and toggles are immediately visible to every thread.- All database access is
synchronized, the single shared JDBC connection is never used concurrently (no interleaved statements / transactions). - Cache-miss DB fetches are serialized under a dedicated canonical-cache lock, so there is
exactly one
Userinstance per account even under concurrent joins/logins.
๐ Database Migration Safety
AuthCore migrates the USERS schema automatically at startup (dialect-aware ALTER TABLE for
missing columns, SQLite PRAGMA, MySQL/PostgreSQL information_schema). If the migration
fails:
Database.migrationBlockedis set totrueand registration/login are suspended until the schema is fixed, AuthCore never writes to a half-migrated database.Database.printMigrationGuidance()prints a clear banner with fix steps: back up + delete the DB file to let AuthCore recreate it, run the missingALTER TABLEstatements manually (shown in the log), or restore a backup fromconfig/authcore/backups/.- API lookups short-circuit to "unknown account" results while blocked, so no endpoint exposes schema errors; security-log/webhook alerts keep working.
๐ฌ Scenario: corrupt hand-edited schema, an admin's manual column change breaks the migration; the server refuses new registrations, prints exact fix instructions, and only resumes after the admin repairs or restores the database.
๐ Session Protection
- IP binding,
session-from-same-ip-only(default true): a resumed session is only accepted from the IP that originally logged in, protecting against session hijacking. - Timeout, sessions expire after
timeout-ms(default 60 min); withkick-after-session-timeoutthe player is kicked on expiry. - Remote logout,
/account logout, the web panel'slogoutaction, andAuthCoreApi.logout(UUID)all end a session instantly (and propagate network-wide when Redis is enabled). - Duplicate session protection,
block-duplicate-sessionprevents the same account from being authenticated from multiple locations at once. - Bedrock / proxy / VPN gating,
allow-bedrock-players,allow-proxy-userscan completely block untrusted client classes.
๐ก Cross-Server Security Event Bus
When database.redis.enabled is on, every server in a network participates in a Redis
pub/sub security event bus (channel authcore:events), no extra configuration:
- Events:
login,logout,register,brute-force,account-locked,kick, each carrying{type, username, detail, server, ts}. - Receivers act locally: the event is written to
security.logwith anEVENT_BUS_*tag, forwarded to the local Discord webhook as a "Cross-Server Event" embed, and executed locally (kick/logoutevents kick the matching player on that backend). - Attack propagation: a brute-force lock or admin kick on one backend takes effect on every backend immediately, there is no window where a locked/compromised account can hop to another server in the network. Events from the local server are ignored (no loops).
๐ Discord Link Codes
Discord account linking (/discord link) uses short-lived, single-use codes:
- Codes are 6 chars drawn from an unambiguous charset (
AโZminusI/O,2โ9) viaSecureRandom, no visually confusable characters. - Stored in Redis (
authcore:discordlink:<code>) with a 10-minute TTL and consumed on first use, so a captured code cannot be replayed. - The command requires a registered account with user permissions; code issuance is written to
the security log (
DISCORD_LINK_CODE), the completed link (DISCORD_LINK) and unlinks (DISCORD_UNLINK) are logged, and the Discord โ account mapping is published to Redis (authcore:discord:<discordId>, 30-day TTL). - The
linkaction is only exposed on the token-protected web panel API (POST /api/action), never in-game.
๐งช Automated Test Suite
Security is enforced by code, and that code is continuously verified: tools/security-tests/
is a standalone suite (no Minecraft required) with 57 checks, run with
tools/security-tests/run-tests.ps1 after a build. It covers password hashing round-trips for
all 6 algorithms, unique per-hash salts, captcha lifecycle (generation, case-insensitive
verification, single-use consumption, expiry), email recovery codes (validation, case handling,
single-use), the rate limiter (window behavior, per-key isolation), proxy IP parsing (IPv4/IPv6,
garbage rejection), device fingerprints and time formatting. It already caught and fixed a real
vulnerability, the PBKDF2 server-thread hang (see Password Hashing) -
and any regression in these paths fails the suite.
โ๏ธ Premium API Outage Safety
premium-api-strict = false(default): when the Mojang session API is unreachable, premium checks degrade gracefully, no legitimate premium player is blocked.premium-api-strict = true(legacy): the premium-name restriction is enforced even during an outage (riskier, use only if you understand the lockout implications).premium-auto-login/premium-auto-registermake hybrid online/offline servers seamless whileallow-online-name-by-offline(default false) prevents username squatting by offline players.
๐ค Account Enumeration Prevention
- Login and registration responses are deliberately uniform; the system does not reveal whether a name exists before authentication.
block-duplicate-registerstops one account name being registered from multiple locations at once, andlook-up-by-usernamecan enforce globally unique names.- Login history records every attempt (
LOGIN_HISTORYtable) so mass enumeration attempts are visible in the web panel and security log.
๐ OP Safety
lobby.safe-operators(default true): a player's OP status is temporarily removed while unauthenticated and restored only after a successful login (and only if they were originally OP). Combined with lobby restrictions, an unauthenticated player with a known UUID cannot execute operator commands.- Admin commands require LuckPerms nodes (
authcore.admin.*) or permission level 3, and thecommandssection lets you tune every node individually.
๐ Limbo Isolation
Unauthenticated players are quarantined in the auth lobby:
- Movement, chat, commands, block/entity interaction, item use/drop/pickup and combat are disabled by default.
force-adventure-mode,prevent-damage,prevent-status-effect,prevent-player-damageandallow-mob-damage = falsemake lobby players effectively invulnerable and passive.invisible-unauthorizedhides them from authenticated players;apply-blindness-effectfocuses them on the login prompt.max-lobby-users(50) caps concurrent unauthenticated players, andrate-limitthrottles connection floods.
๐ SSRF & HTTPS-Only Outbound
- Webhook and email endpoints accept only
https://URLs in their documented configuration (Discord webhook, SMTP over STARTTLS/SSL). - The web panel's optional HTTPS mode uses a self-signed certificate generated on first start with its SHA-256 fingerprint printed to the console, or a user-supplied PKCS12/JKS keystore.
- The panel binds to
127.0.0.1by default, requires a bearer token on every request (including/metrics), setsCache-Control: no-store, and supports a separate read-only token (web-panel.readonly-token) for viewers that must never run actions - read-only tokens get403onPOST /api/action.
๐งฎ Resource Safety
- Lazy user loading (1.0.0), the server no longer loads the full user table at startup. Users are fetched from the database on demand (join/login/whois) into a bounded LRU cache (20 000 entries max) with idle eviction; online users are never evicted. A 100k-user database therefore costs no more memory than the online player count, and nobody can DoS the server by registering 100k accounts just to inflate a startup load.
- Database-backed admin queries,
list,whois,exportand the web panel player list run bounded, searchable SQL queries instead of iterating the cache, so even giant databases stay O(rows-returned). - Bounded caches, captcha entries (512), email recovery codes (256), rate-limiter windows (10 000), user cache (20 000) all purge expired entries automatically.
- No netty blocking, all security checks are synchronous, in-memory, and O(1)-ish map operations; nothing blocks netty's event loop.
- The web panel runs on a daemon thread pool (
AuthCore-WebPanel), never the server thread.
๐ฃ Webhook & Email Alerting
- Discord webhook (
session.security.webhook-url) receives join, login, failed-login, lockout and suspicious-login events. - Extra webhooks (
session.security.extra-webhook-urls, list), Slack, Telegram or custom webhook URLs that receive the same events as the Discord webhook (multi-channel alerting). - Role-sync webhook (
session.security.role-sync-webhook-url), a separate webhook used only for registration announcements, typically consumed by a Discord bot that assigns roles; falls back towebhook-urlwhen empty. - Email (
session.email) sends login alerts for new IPs/countries and supports recovery codes for forgotten passwords via/account recover(6-digit, single-use, 15-minute TTL). โ ๏ธ Completing a recovery also clears the account's 2FA (TOTP) secret, the player must re-setup 2FA, so a recovered account cannot be held hostage by a lost authenticator. - Security log file (
session.security.log-file, defaultsecurity.log) records every event with tags such asWEB_KICK,PROXY_FORWARD,NICKNAME_INVALID,DB_EXPORT, lockouts, and risk alerts. - Log rotation (
session.security.log-max-bytes, default 5 MB), when the security log exceeds the limit it rotates tosecurity.log.1โ.2โ.3(oldest discarded), so the log can never fill the disk.
๐ฎ Multi-Version & Build Compatibility
- Version-fragile mixins are
required:false,ServerHandshakeNetworkHandlerMixin,ServerLoginNetworkHandlerMixinandServerPlayNetworkHandlerChatMixinare declaredrequired:falseinauthcore.server.mixins.json. When a future Minecraft version changes a target signature, the mixin is skipped instead of crashing the server, and the rest of the mod's defenses (commands, hashing, rate limits, lobby restrictions) stay active. - Stonecraft/Stonecutter workspace, Mojang mappings everywhere, a single merged source
tree (
src/main/java+src/main/resources) is compiled per version-group ร loader variant by the Stonecraft 1.10 + Stonecutter 0.9 workspace (settings.gradle.kts,stonecutter.gradle.kts, centralbuild.gradle.ktsKotlin DSL,versions/dependencies/*.properties; variants:1.18.2-fabric/:1.18.2-forge/:1.21.11-fabric/:1.21.11-forge/:1.21.11-neoforge/:26.2-fabric/:26.2-neoforge, active1.21.11-fabric). Every group uses official (Mojang) mappings; 26.1-26.2 is additionally unobfuscated (Mojang names at runtime). Java toolchains follow the group: 17 (1.16โ1.18), 21 (1.19โ1.21), 25 (26.1-26.2). - Range jars, one jar per group ร loader:
authcore-1.16-1.18-{fabric,forge}-<v>.jar(built at 1.18.2, covers 1.16.0 โ 1.18.2),authcore-1.19-1.21-{fabric,forge,neoforge}-<v>.jar(built at 1.21.11, covers 1.19.0 โ 1.21.11) andauthcore-26.1-26.2-{fabric,neoforge}-<v>.jar(built at 26.2, covers 26.1 โ 26.2). The legacyauthcore-classic/authcore-modernsplit is gone. - Host-compatibility harness,
tools/host-tests/run-host-tests.ps1boots every range jar on its full verify matrix (1.16.5 / 1.17.1 / 1.18.2, 1.19.4 / 1.20.6 / 1.21.11, 26.1.2 / 26.2) in Docker containers, driven by theversions.jsonrange-group matrix (options-Groups,-Version,-Jar,-Smoke,-Build,-Parallel,-KeepReportsand more), with live log streaming and pruned reports (reports/latest.md|html|json). The full matrix passed on 2026-08-10. - Client companion (bundled), every range jar ships
environment: "*"with the client login-screen companion built in (1.20.2+ clients; older clients load safely and skip the screen). There is no separate build flag, the companion, server mod and the BungeeCord/Velocity proxy plugin all live in the same jar.
๐งจ Threat Model
| Threat | Category | Mitigation in AuthCore |
|---|---|---|
| Broken Authentication (password guessing) | OWASP A01 / A07 | Argon2id hashing, max attempts, cooldowns, progressive punishment, account locks, captcha |
| Credential stuffing / bots | OWASP A01 / Minecraft-specific | Per-IP rate limits, allow-proxy-users = false, captcha, risk scoring |
| Session hijacking / replay | OWASP A07 / A02 | IP-bound sessions, timeout, remote logout, duplicate-session blocking |
| Sensitive Data Exposure (password dumps) | OWASP A02 | Memory-hard hashing with per-user salt; raw passwords never stored; md5 disabled |
| Injection (SQL / command) | OWASP A03 | Parameterized queries (dialect-aware UPSERT), no string-built SQL from user input |
| Security Misconfiguration (panel exposure) | OWASP A05 | Web panel off by default, token required, 127.0.0.1 binding, HTTPS option |
| Vulnerable/legacy components (weak hashes) | OWASP A06 | Strong defaults (Argon2id); weak-algorithm risk warnings; migration columns auto-added |
| Identification failures (premium impersonation) | OWASP A07 | Premium API verification, premium-api-strict switch, risk-scored logins |
| Logging & monitoring gaps | OWASP A09 | Security log file, Discord webhook, email alerts, LOGIN_HISTORY, web panel overview |
| SSRF via webhook/panel config | OWASP A10 | HTTPS-only outbound URLs; panel bound locally; token auth |
| Account enumeration | Minecraft-specific | Uniform auth responses, blocked duplicate registrations, attempt history |
| Username squatting / theft | Minecraft-specific | allow-online-name-by-offline = false, block-duplicate-register, premium auto-register |
| OP abuse pre-auth | Minecraft-specific | safe-operators, permission-level gating of admin commands |
| Combat logging | Minecraft-specific | Combat detection + optional combat-log.kill-on-disconnect |
| Lobby grief / free-roaming exploits | Minecraft-specific | Full lobby restriction matrix, adventure mode, damage prevention, invisibility |
| Login flood / server overload | Minecraft-specific | Join rate limiting, max-lobby-users, bounded caches, no netty blocking |
| IP spoofing via proxy payloads | Minecraft-specific | Forwarded-IP validation (isValidIp), config-gated parsing, Velocity shared secret |
| Account sharing / trading | Minecraft-specific | Network device fingerprint (+15 risk, "Possible Account Sharing" alert), optional block-on-new-country |
| Targeted IP abuse | Minecraft-specific | IP allow/deny rules file (first-match-wins, default-allow, kick on join) + honeypot auto-bans (decoy port appends deny rules) |
| Adaptive attackers learning block messages | Minecraft-specific | Shadow-ban mode: generic disconnect reason + silent webhook alerts |
| Scripted auth-command spam | Minecraft-specific | Per-player command cooldowns on /login & /register |
| Fast-rejoin bot patterns | Minecraft-specific | rate-limit.alert-on-fast-rejoin webhook/security-log alert |
| Password reuse / hash cycling | OWASP A01 | password-rules.history-size blocks reuse of the last N passwords |
| DoS via huge user databases | Minecraft-specific | Lazy user loading (bounded 20k LRU, online users never evicted) + database-backed admin/panel queries |
| Race conditions on shared state | OWASP A07 | volatile config/messages/session fields, synchronized DB access, canonical cache under a lock |
| Maintenance window exposure | Minecraft-specific | session.maintenance blocks all joins with a custom kick message |
| Lost authenticator lockout | Minecraft-specific | /account recover clears the TOTP secret so 2FA can be re-setup |
| Cross-server attack hopping | Minecraft-specific | Redis event bus: locks/kicks/brute-force propagate to every backend instantly |
| Discord-link code theft / replay | Minecraft-specific | 6-char SecureRandom codes, 10-min TTL, single-use consumption, token-protected link API |
| Unauthorized panel viewing | OWASP A05 | readonly-token: view-only access; actions return 403 for read-only tokens |
Proxy & forwarding
- Handshake forwarding payloads are validated (
ProxySupport.isValidIp- IPv4/IPv6 with zone indexes) before any address rewrite; garbage handshake data is ignored. - Velocity modern forwarding (
velocity:player_info) is verified with HMAC-SHA256 using the shared secret (ProxySupport.verifyVelocityHmac/VelocitySupport.parsePlayerInfo) - tampered or unauthenticated payloads are rejected. - Web-panel tokens, captcha codes and email-recovery codes are compared in constant time (MessageDigest.isEqual); recovery codes are single-use with a 5-attempt cap and a 60s re-issue cooldown.
- All caches (users, GeoIP, premium lookups, rate limits, captcha, web-panel lockouts, proxy session cache) are bounded and self-pruning - no unbounded growth under attack.
Proxy-side full auth
- With
block-unauthenticated=truethe proxy plugin validatesauthcore:session:<uuid>in Redis BEFORE a player reaches any backend - unauthenticated players are disconnected at the proxy (BungeeCordLoginEvent/ VelocityLoginEvent). - The check is fail-open: Redis unreachable -> connections allowed with a warning, so a Redis outage can never lock a network.
- Session keys are written by backends with
database.redis.enabledand carry the configured TTL; the proxy's zero-dependency RESP client uses short timeouts (1.5s) so the login path is never stalled by a hung Redis.