--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -44,9 +_,16 @@
 import org.apache.commons.lang3.Validate;
 import org.slf4j.Logger;
 
+// CraftBukkit start
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.util.Waitable;
+import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
+import org.bukkit.event.player.PlayerPreLoginEvent;
+
 public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener {
     private static final AtomicInteger UNIQUE_THREAD_ID = new AtomicInteger(0);
     static final Logger LOGGER = LogUtils.getLogger();
+    private static final java.util.concurrent.ExecutorService authenticatorPool = java.util.concurrent.Executors.newCachedThreadPool(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("User Authenticator #%d").setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER)).build()); // Paper - Cache authenticator threads
     private static final int MAX_TICKS_BEFORE_LOGIN = 600;
     private final byte[] challenge;
     final MinecraftServer server;
@@ -54,21 +_,36 @@
     public volatile ServerLoginPacketListenerImpl.State state = ServerLoginPacketListenerImpl.State.HELLO;
     private int tick;
     @Nullable
-    String requestedUsername;
+    public String requestedUsername; // Paper
     @Nullable
     public GameProfile authenticatedProfile;
     private final String serverId = "";
-    private final boolean transferred;
+    public final boolean transferred; // Paper
+    public boolean iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation = false; // Paper - username validation overriding
+    private int velocityLoginMessageId = -1; // Paper - Add Velocity IP Forwarding Support
+    public @Nullable java.util.UUID requestedUuid; // Paper
+    private final io.papermc.paper.connection.PaperPlayerLoginConnection paperLoginConnection; // Paper - Config API
+    private volatile boolean disconnecting = false; // Paper - Fix disconnect still ticking login
 
     public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection, boolean transferred) {
         this.server = server;
         this.connection = connection;
         this.challenge = Ints.toByteArray(RandomSource.create().nextInt());
         this.transferred = transferred;
+        this.paperLoginConnection  = new io.papermc.paper.connection.PaperPlayerLoginConnection(this); // Paper
     }
 
     @Override
     public void tick() {
+        if (this.disconnecting) return; // Paper - Fix disconnect being called multiple times due to not awaiting for disconnect
+        // Paper start - login cookie API
+        // Don't block the connection
+        if (this.paperLoginConnection.isAwaitingCookies()) {
+            this.tickTimeout(); // Ensure we tick timeout logic
+            return;
+        }
+        // Paper end - login cookie API
+
         if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) {
             this.verifyLoginAndFinishConnectionSetup(Objects.requireNonNull(this.authenticatedProfile));
         }
@@ -78,11 +_,23 @@
             this.finishLoginAndWaitForClient(this.authenticatedProfile);
         }
 
+    // Paper start - login cookie API
+        this.tickTimeout();
+    }
+    public void tickTimeout() {
+    // Paper end - login cookie API
         if (this.tick++ == 600) {
             this.disconnect(Component.translatable("multiplayer.disconnect.slow_login"));
         }
     }
 
+    // CraftBukkit start
+    @Deprecated
+    public void disconnect(String reason) {
+        this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(reason))); // Paper - Fix hex colors not working in some kick messages
+    }
+    // CraftBukkit end
+
     @Override
     public boolean isAcceptingMessages() {
         return this.connection.isConnected();
@@ -93,6 +_,7 @@
             LOGGER.info("Disconnecting {}: {}", this.getUserName(), reason.getString());
             this.connection.send(new ClientboundLoginDisconnectPacket(reason));
             this.connection.disconnect(reason);
+            this.disconnecting = true; // Paper - Fix disconnect still ticking login
         } catch (Exception var3) {
             LOGGER.error("Error whilst disconnecting player", (Throwable)var3);
         }
@@ -115,7 +_,14 @@
     @Override
     public void handleHello(ServerboundHelloPacket packet) {
         Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet");
-        Validate.validState(StringUtil.isValidPlayerName(packet.name()), "Invalid characters in username");
+        // Paper start - Validate usernames
+        if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()
+            && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.performUsernameValidation
+            && !this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation) {
+            Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username");
+        }
+        this.requestedUuid = packet.profileId();
+        // Paper end - Validate usernames
         this.requestedUsername = packet.name();
         GameProfile singleplayerProfile = this.server.getSingleplayerProfile();
         if (singleplayerProfile != null && this.requestedUsername.equalsIgnoreCase(singleplayerProfile.getName())) {
@@ -125,7 +_,32 @@
                 this.state = ServerLoginPacketListenerImpl.State.KEY;
                 this.connection.send(new ClientboundHelloPacket("", this.server.getKeyPair().getPublic().getEncoded(), this.challenge, true));
             } else {
-                this.startClientVerification(UUIDUtil.createOfflineProfile(this.requestedUsername));
+                // Paper start - Add Velocity IP Forwarding Support
+                if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) {
+                    this.velocityLoginMessageId = java.util.concurrent.ThreadLocalRandom.current().nextInt();
+                    net.minecraft.network.FriendlyByteBuf buf = new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.buffer());
+                    buf.writeByte(com.destroystokyo.paper.proxy.VelocityProxy.MAX_SUPPORTED_FORWARDING_VERSION);
+                    net.minecraft.network.protocol.login.ClientboundCustomQueryPacket packet1 = new net.minecraft.network.protocol.login.ClientboundCustomQueryPacket(this.velocityLoginMessageId, new net.minecraft.network.protocol.login.ClientboundCustomQueryPacket.PlayerInfoChannelPayload(com.destroystokyo.paper.proxy.VelocityProxy.PLAYER_INFO_CHANNEL, buf));
+                    this.connection.send(packet1);
+                    return;
+                }
+                // Paper end - Add Velocity IP Forwarding Support
+                // CraftBukkit start
+                // Paper start - Cache authenticator threads
+                authenticatorPool.execute(() -> {
+                    try {
+                        GameProfile gameprofile = ServerLoginPacketListenerImpl.this.createOfflineProfile(ServerLoginPacketListenerImpl.this.requestedUsername); // Spigot
+
+                        gameprofile = ServerLoginPacketListenerImpl.this.callPlayerPreLoginEvents(gameprofile); // Paper - Add more fields to AsyncPlayerPreLoginEvent
+                        ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
+                        ServerLoginPacketListenerImpl.this.startClientVerification(gameprofile);
+                    } catch (Exception ex) {
+                        ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
+                        ServerLoginPacketListenerImpl.this.server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + ServerLoginPacketListenerImpl.this.requestedUsername, ex);
+                    }
+                });
+                // Paper end - Cache authenticator threads
+                // CraftBukkit end
             }
         }
     }
@@ -137,7 +_,7 @@
 
     private void verifyLoginAndFinishConnectionSetup(GameProfile profile) {
         PlayerList playerList = this.server.getPlayerList();
-        Component component = playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile);
+        Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile), this.paperLoginConnection, this.connection, profile, this.server, true); // Paper
         if (component != null) {
             this.disconnect(component);
         } else {
@@ -184,7 +_,8 @@
             throw new IllegalStateException("Protocol error", var7);
         }
 
-        Thread thread = new Thread("User Authenticator #" + UNIQUE_THREAD_ID.incrementAndGet()) {
+        // Paper start - Cache authenticator threads
+        authenticatorPool.execute(new Runnable() {
             @Override
             public void run() {
                 String string1 = Objects.requireNonNull(ServerLoginPacketListenerImpl.this.requestedUsername, "Player name not initialized");
@@ -195,11 +_,17 @@
                         .hasJoinedServer(string1, string, this.getAddress());
                     if (profileResult != null) {
                         GameProfile gameProfile = profileResult.profile();
+                        // CraftBukkit start - fire PlayerPreLoginEvent
+                        if (!ServerLoginPacketListenerImpl.this.connection.isConnected()) {
+                            return;
+                        }
+                        gameProfile = ServerLoginPacketListenerImpl.this.callPlayerPreLoginEvents(gameProfile); // Paper - Add more fields to AsyncPlayerPreLoginEvent
+                        // CraftBukkit end
                         ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameProfile.getName(), gameProfile.getId());
                         ServerLoginPacketListenerImpl.this.startClientVerification(gameProfile);
                     } else if (ServerLoginPacketListenerImpl.this.server.isSingleplayer()) {
                         ServerLoginPacketListenerImpl.LOGGER.warn("Failed to verify username but will let them in anyway!");
-                        ServerLoginPacketListenerImpl.this.startClientVerification(UUIDUtil.createOfflineProfile(string1));
+                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(string1)); // Spigot
                     } else {
                         ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.unverified_username"));
                         ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", string1);
@@ -207,11 +_,16 @@
                 } catch (AuthenticationUnavailableException var4) {
                     if (ServerLoginPacketListenerImpl.this.server.isSingleplayer()) {
                         ServerLoginPacketListenerImpl.LOGGER.warn("Authentication servers are down but will let them in anyway!");
-                        ServerLoginPacketListenerImpl.this.startClientVerification(UUIDUtil.createOfflineProfile(string1));
+                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(string1)); // Spigot
                     } else {
-                        ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
+                        ServerLoginPacketListenerImpl.this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.authenticationServersDown)); // Paper - Configurable kick message
                         ServerLoginPacketListenerImpl.LOGGER.error("Couldn't verify username because servers are unavailable");
                     }
+                    // CraftBukkit start - catch all exceptions
+                } catch (Exception ex) {
+                    ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
+                    ServerLoginPacketListenerImpl.LOGGER.warn("Exception verifying {}", string1, ex);
+                    // CraftBukkit end
                 }
             }
 
@@ -222,18 +_,113 @@
                     ? ((InetSocketAddress)remoteAddress).getAddress()
                     : null;
             }
-        };
-        thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER));
-        thread.start();
-    }
+        });
+        // Paper end - Cache authenticator threads
+    }
+
+    // CraftBukkit start
+    private GameProfile callPlayerPreLoginEvents(GameProfile gameprofile) throws Exception { // Paper - Add more fields to AsyncPlayerPreLoginEvent
+        // Paper start - Add Velocity IP Forwarding Support
+        if (ServerLoginPacketListenerImpl.this.velocityLoginMessageId == -1 && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) {
+            this.disconnect("This server requires you to connect with Velocity.");
+            return gameprofile;
+        }
+        // Paper end - Add Velocity IP Forwarding Support
+        String playerName = gameprofile.getName();
+        java.net.InetAddress address = ((java.net.InetSocketAddress) this.connection.getRemoteAddress()).getAddress();
+        java.util.UUID uniqueId = gameprofile.getId();
+        final org.bukkit.craftbukkit.CraftServer server = ServerLoginPacketListenerImpl.this.server.server;
+
+        // Paper start - Add more fields to AsyncPlayerPreLoginEvent
+        final InetAddress rawAddress = ((InetSocketAddress) this.connection.channel.remoteAddress()).getAddress();
+        com.destroystokyo.paper.profile.PlayerProfile profile = com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile); // Paper - setPlayerProfileAPI
+        AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, rawAddress, uniqueId, this.transferred, profile, this.connection.hostname, this.paperLoginConnection); // Paper
+        server.getPluginManager().callEvent(asyncEvent);
+        profile = asyncEvent.getPlayerProfile();
+        profile.complete(true); // Paper - setPlayerProfileAPI
+        gameprofile = com.destroystokyo.paper.profile.CraftPlayerProfile.asAuthlibCopy(profile);
+        playerName = gameprofile.getName();
+        uniqueId = gameprofile.getId();
+        // Paper end - Add more fields to AsyncPlayerPreLoginEvent
+
+        if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) {
+            final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId);
+            if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) {
+                event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure
+            }
+            Waitable<PlayerPreLoginEvent.Result> waitable = new Waitable<>() {
+                @Override
+                protected PlayerPreLoginEvent.Result evaluate() {
+                    server.getPluginManager().callEvent(event);
+                    return event.getResult();
+                }
+            };
+
+            ServerLoginPacketListenerImpl.this.server.processQueue.add(waitable);
+            if (waitable.get() != PlayerPreLoginEvent.Result.ALLOWED) {
+                this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure
+            }
+        } else {
+            if (asyncEvent.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
+                this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(asyncEvent.kickMessage())); // Paper - Adventure
+            }
+        }
+        return gameprofile; // Paper - Add more fields to AsyncPlayerPreLoginEvent
+    }
+    // CraftBukkit end
 
     @Override
     public void handleCustomQueryPacket(ServerboundCustomQueryAnswerPacket packet) {
+        // Paper start - Add Velocity IP Forwarding Support
+        if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled && packet.transactionId() == this.velocityLoginMessageId) {
+            ServerboundCustomQueryAnswerPacket.QueryAnswerPayload payload = (ServerboundCustomQueryAnswerPacket.QueryAnswerPayload)packet.payload();
+            if (payload == null) {
+                this.disconnect("This server requires you to connect with Velocity.");
+                return;
+            }
+
+            net.minecraft.network.FriendlyByteBuf buf = payload.buffer;
+            if (!com.destroystokyo.paper.proxy.VelocityProxy.checkIntegrity(buf)) {
+                this.disconnect("Unable to verify player details");
+                return;
+            }
+
+            int version = buf.readVarInt();
+            if (version > com.destroystokyo.paper.proxy.VelocityProxy.MAX_SUPPORTED_FORWARDING_VERSION) {
+                throw new IllegalStateException("Unsupported forwarding version " + version + ", wanted upto " + com.destroystokyo.paper.proxy.VelocityProxy.MAX_SUPPORTED_FORWARDING_VERSION);
+            }
+
+            java.net.SocketAddress listening = this.connection.getRemoteAddress();
+            int port = 0;
+            if (listening instanceof java.net.InetSocketAddress) {
+                port = ((java.net.InetSocketAddress) listening).getPort();
+            }
+            this.connection.address = new java.net.InetSocketAddress(com.destroystokyo.paper.proxy.VelocityProxy.readAddress(buf), port);
+
+            this.authenticatedProfile = com.destroystokyo.paper.proxy.VelocityProxy.createProfile(buf);
+
+            //TODO Update handling for lazy sessions, might not even have to do anything?
+
+            // Proceed with login
+            authenticatorPool.execute(() -> {
+                try {
+                    final GameProfile gameprofile = this.callPlayerPreLoginEvents(this.authenticatedProfile);
+                    ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
+                    ServerLoginPacketListenerImpl.this.startClientVerification(gameprofile);
+                } catch (Exception ex) {
+                    disconnect("Failed to verify username!");
+                    server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + this.authenticatedProfile.getName(), ex);
+                }
+            });
+            return;
+        }
+        // Paper end - Add Velocity IP Forwarding Support
         this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
     }
 
     @Override
     public void handleLoginAcknowledgement(ServerboundLoginAcknowledgedPacket packet) {
+        net.minecraft.network.protocol.PacketUtils.ensureRunningOnSameThread(packet, this, this.server); // CraftBukkit
         Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet");
         this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND);
         CommonListenerCookie commonListenerCookie = CommonListenerCookie.createInitial(Objects.requireNonNull(this.authenticatedProfile), this.transferred);
@@ -252,8 +_,31 @@
 
     @Override
     public void handleCookieResponse(ServerboundCookieResponsePacket packet) {
+        if (this.paperLoginConnection.handleCookieResponse(packet)) return; // Paper
         this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
     }
+
+    // Spigot start
+    protected GameProfile createOfflineProfile(String s) {
+        java.util.UUID uuid;
+        if (this.connection.spoofedUUID != null) {
+            uuid = this.connection.spoofedUUID;
+        } else {
+            uuid = UUIDUtil.createOfflinePlayerUUID(s);
+        }
+
+        GameProfile gameProfile = new GameProfile(uuid, s);
+
+        if (this.connection.spoofedProfile != null) {
+            for (com.mojang.authlib.properties.Property property : this.connection.spoofedProfile) {
+                if (!ServerHandshakePacketListenerImpl.PROP_PATTERN.matcher(property.name()).matches()) continue;
+                gameProfile.getProperties().put(property.name(), property);
+            }
+        }
+
+        return gameProfile;
+    }
+    // Spigot end
 
     public static enum State {
         HELLO,
