Authentication Flows

This page walks through every flow in AuthCore, step by step, and names the functions involved at each point. It is written for server owners who want to understand what the mod does and why, without reading the source. Each flow ends with a short "why it exists" note.

How to read this page

  • Class.method names point at the actual code so you can jump in and look around.
  • "Lobby" and "limbo" are the same thing: the restricted waiting area every unauthenticated player is held in.
  • "Premium" means a paid (Mojang) Minecraft account. "Standard" means a cracked/offline account.

1. The join flow

Goal: decide, for every incoming player, whether they are premium or standard, and then either let them in or lock them into the limbo.

  1. The login packet arrives. ServerLoginNetworkHandlerMixin.handleHello reads the player name and UUID from the hello packet (the shape differs between Minecraft versions, so it is read reflectively).
  2. Server mode is detected. AuthCoreServer.detectServerOnlineMode + Compat.serverUsesAuthentication read the real server.properties online-mode setting. This decides whether the server verifies Mojang sessions at all (see flow 7).
  3. Optional premium verification starts. On offline-mode servers with premium auto-login enabled, ServerPremiumVerificationMixin starts the vanilla encryption handshake so the client hands over its session token (flow 7 explains the details).
  4. The play connection becomes ready. ServerEvents.onPlayerJoin is the main gate. In order it checks:
    • duplicate login (user.isActive, blockDuplicateRegister, blockDuplicateSession),
    • maintenance mode, join rate limits (RateLimiter), IP rules (IpRules.isDenied), proxy/VPN blocking (McApiManager.geoIp + user.isProxy),
    • the same-IP session rule and the persistent account lock (user.isLocked),
    • then user.connect(connection) wires the connection to the account.
  5. Three ways to get in:
    • Session resume (same IP + still-valid session) calls user.login(player) directly (flow 5).
    • Premium auto-login (verified premium, or a brand-new account on a server that verified the session) calls user.login(player) directly - auto-login players are never given a generated password (their stored password stays null). A player who explicitly switched their account to password login (/account set-mode offline) is not auto-logged-in: they are asked to /register or /login like any offline account.
    • Everyone else is locked into the limbo with user.lobby.lock() (flow 2) and asked to /register or /login.

Why it exists: without this gate, anyone could walk onto the server, claim a name, and play or grief before proving who they are. The join flow is the single place where every account is classified and every security rule is applied.

2. The limbo (lockdown) flow

Goal: hold unauthenticated players in a tiny, fully restricted bubble until they prove their identity.

  1. Locking: Lobby.lock() takes a Lobby.Snapshot (inventory, effects, health, food, XP, position, game mode, operator status) so everything can be restored later. Then Lobby.handleTeleport() picks the anchor position: the configured limbo spawn for new players, or the player's own position for returning players when limbo-config.only-on-first-time is on. Blindness, invisibility and adventure mode are applied, and the player is dismounted from any vehicle.
  2. Enforcement: every restriction is enforced at the packet or game-mode level by a mixin:
    • movement: ServerPlayNetworkHandlerMixin cancels move packets and snaps the client back with Lobby.teleportBack(); EntityMixin stops server-side movement; the per-tick guard in ServerEvents.onEndServerTick (Lobby.isFarFromLobbyPos) re-teleports any drift,
    • chat: ServerPlayNetworkHandlerChatMixin,
    • commands: the CommandManager* mixins enforce the whitelist (login/register/account stay allowed),
    • blocks and items: ServerPlayerInteractionManagerMixin blocks breaking, placing and item use; ServerPlayerMixin blocks item drops; InventoryMixin, ItemEntityMixin and the ScreenHandler* mixins block inventory/pickup tricks,
    • attacks and interactions: EntityEvents plus MobEntityMixin (mobs cannot target you), ServerPlayerRestrictionsMixin blocks riding and sleeping,
    • damage: EntityEvents.onEntityDamage makes the player invulnerable in the lobby.
  3. Reminders: Lobby.handleTimeout schedules periodic "use /register or /login" action-bar messages and the login-timeout kick.
  4. Unlocking: after a successful login, Lobby.unlock() calls Snapshot.reset(player), which restores everything: inventory, effects, health, position, game mode, operator status, the previous vehicle, and lands airborne players safely (no fall damage, no suffocation).

Why it exists: the limbo is the core of a secure server: attackers, bots and name-squatters have nothing to interact with until they authenticate, and legitimate players get back exactly what they had when they leave.

3. The register flow

Goal: let a brand-new player create an account with a password.

  1. /register <password> <confirm> runs Register.registerCommand. It re-verifies the prerequisites (the player must be in the lobby and unregistered), applies the command cooldown, and checks the account lock.
  2. The password is validated against the policy with Security.Password.check (upper/lower/ digit counts and total length from password-rules).
  3. User.register(player, password) hashes the password with Encrypter.hash (argon2 by default, self-contained salt), writes the account to the database (User.db.insert), and either logs the player straight in (allowLoginAfterRegistration) or keeps them in the lobby.
  4. If hashing ever fails, the player is locked into the lobby instead of being left in a broken half-registered state.

Why it exists: registration gives each player a secret only they know, so later logins can prove it is really them.

4. The login flow

Goal: let a registered player prove their password and leave the limbo.

  1. /login <password> runs Login.execute: cooldown, then it checks the attempt counter and the account lock. After a successful login the human-verification observation window starts (only bot-like players are ever challenged).
  2. The stored hash is checked with Encrypter.verify. The verifier tries the stored algorithm first and falls back through the others, so old or imported hashes still verify without console errors.
  3. If the stored hash uses a weak or outdated algorithm, Login.upgradeHashIfNeeded transparently re-hashes it with the configured algorithm (you never notice).
  4. If 2FA is enabled, a TOTP or email code is required (Security.TOTPManager, Security.EmailOtp).
  5. User.login(player) records the login, issues a fresh session token, broadcasts the auth state (AuthInterop.broadcast) so proxies and other mods know, imports any DiscordSRV link, and unlocks the lobby, which restores the player (flow 2, step 4).

Why it exists: login is the proof step: the password plus any second factor is how the server knows the player behind the name is the account owner.

5. The session resume flow

Goal: players who logged in recently should not type their password every time they rejoin.

  1. During the join flow, ServerEvents.onPlayerJoin checks: same UUID, same IP (session-from-same-ip-only) and an active session (user.isActiveSession, within session.timeout-ms).
  2. If all match, user.login(player) runs directly and the player skips the lobby.
  3. Clients that echo a valid session token over the interop channel resume with full trust (ClientGuard.verifySessionClaim); plain vanilla clients resume by IP with a small risk penalty when require-token-for-resume is enabled.

Why it exists: sessions are the convenience layer. The IP lock keeps the convenience from becoming a hijack vector: a stolen IP alone is not enough, and tokens rotate on every login.

6. The logout flow

Goal: end a session cleanly.

  1. /account logout calls User.logout(payload): the session is invalidated, the Redis session is removed, the interop channel is told the player is no longer authenticated (AuthInterop.broadcast(player, false)), and the player is disconnected.
  2. When a player simply leaves, ServerEvents.onPlayerLeave does the same cleanup: lobby unlock, in-memory caches dropped (ClientGuard, DiscordSRV), combat-log punishment applied if configured.

Why it exists: logout protects the account on shared computers and keeps every other system (proxies, Discord bridges, the web panel) in sync with reality.

7. The premium verification flow

Goal: let genuine premium (paid) accounts auto-login on online and offline-mode servers without AuthCore ever calling the Mojang web API.

  1. Online-mode servers: vanilla itself verifies every profile with Mojang (hasJoinedServer). The login mixin captures the verified profile and calls AuthCoreServer.markPremiumVerified.
  2. Offline-mode servers (premium auto-login enabled):
    • ServerPremiumVerificationMixin forces the vanilla encryption handshake during login (Compat.loginStartHandshake). The client answers with its session token.
    • Compat.loginFinishHandshake decrypts the token and activates connection encryption.
    • Compat.loginVerifyProfile calls the server's OWN MinecraftSessionService.hasJoinedServer - exactly what vanilla does in online mode, no external HTTP from AuthCore.
    • Success marks the account premium; the join flow then auto-logs-in with a null password (no generated password is ever stored for auto-login accounts).
    • Failure (cracked client, fake session, Mojang outage) simply continues the join as a standard player: lobby + register/login. Nobody is ever kicked.
    • If the client never answers the handshake, a 15 second watchdog replays the hello and continues offline.
  3. Own-mode choice wins: an account the player or an admin explicitly set to password login (/account set-mode offline / /authcore set-mode offline <player>) is never auto-logged-in, even when the server verified its Mojang session - it is asked to register or login. Switching back is just /account set-mode online.
  4. Stale flags: accounts flagged premium in the database from older builds are downgraded on standard-auth servers (flow 8).

Why it exists: online-mode players are trusted (Mojang already verified them), so they skip the password step. Doing it through the server's own session service keeps it unforgeable and removes the fragile HTTP name-lookups entirely.

8. The auto-migration flow

Goal: upgrade old configs, databases and messages without manual work.

  • Config: ConfigMigrator.migrate() runs after config load, applies registered version steps, bumps config.version and saves. New keys are added automatically by Configurate.
  • Messages: enriched multi-channel defaults are copied only for templates that still hold the old single-channel values (custom messages are preserved).
  • Database schema: Database.load() adds missing columns automatically (and suspends auth with clear instructions if that is impossible).
  • Account flags: on the first detection of a standard-auth server, Database.downgradePremiumAccounts() clears stale premium flags once per boot.
  • Password hashes: weak legacy hashes are upgraded on the next successful login (flow 4).

Why it exists: upgrades should never ask the owner to rebuild databases or hunt for new settings. Every migration is fail-safe: if it cannot run, the mod keeps working and tells you what to fix.

Failure safety guarantees

  • No code path can crash the server: every mixin uses exact descriptors with require = 0, so version-specific signatures are skipped, never fatal.
  • Every external call (GeoIP, session verification) is cached, timeout-limited and best-effort; failures downgrade the experience, never the server.
  • Unverified players are always held in the limbo, never silently admitted and never accidentally kicked.

๐Ÿš€ Scale & concurrency guarantees

  • 500k+ accounts, thousands of concurrent players: every hot path is a constant-time ConcurrentHashMap lookup (User.getUser), the database is only touched on cache miss, and bounded structures everywhere keep memory flat at any player count.
  • No resource spikes: per-user throttles (touches, teleports, reminders), cached external lookups, rate limits and a fixed-size daemon I/O pool absorb join/login bursts without a CPU, memory or thread cliff.
  • Race-condition-free: canonical single-instance-per-account cache under one lock, synchronized database access (single shared connection), volatile shared config/messages, deduped join/leave hooks and atomic counters - concurrent joins, logins, web-panel actions, Redis events and backups never interleave or deadlock.