--- a/net/minecraft/server/network/LoginListener.java
+++ b/net/minecraft/server/network/LoginListener.java
@@ -44,8 +44,38 @@
 import org.apache.commons.lang3.Validate;
 import org.slf4j.Logger;
 
-public class LoginListener implements PacketLoginInListener, TickablePacketListener {
+// CraftBukkit start
+import net.minecraft.network.EnumProtocol;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.PlayerConnectionUtils;
+import net.minecraft.server.level.EntityPlayer;
+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 LoginListener implements PacketLoginInListener, TickablePacketListener, CraftPlayer.TransferCookieConnection {
+
+    @Override
+    public boolean isTransferred() {
+        return this.transferred;
+    }
+
+    @Override
+    public EnumProtocol getProtocol() {
+        return EnumProtocol.LOGIN;
+    }
+
+    @Override
+    public void sendPacket(Packet<?> packet) {
+        this.connection.send(packet);
+    }
 
+    @Override
+    public void kickPlayer(IChatBaseComponent reason) {
+        disconnect(reason);
+    }
+    // CraftBukkit end
     private static final AtomicInteger UNIQUE_THREAD_ID = new AtomicInteger(0);
     static final Logger LOGGER = LogUtils.getLogger();
     private static final int MAX_TICKS_BEFORE_LOGIN = 600;
@@ -60,6 +90,7 @@
     private GameProfile authenticatedProfile;
     private final String serverId;
     private final boolean transferred;
+    private EntityPlayer player; // CraftBukkit
 
     public LoginListener(MinecraftServer minecraftserver, NetworkManager networkmanager, boolean flag) {
         this.state = LoginListener.EnumProtocolState.HELLO;
@@ -76,6 +107,12 @@
             this.verifyLoginAndFinishConnectionSetup((GameProfile) Objects.requireNonNull(this.authenticatedProfile));
         }
 
+        // CraftBukkit start
+        if (this.state == LoginListener.EnumProtocolState.WAITING_FOR_COOKIES && !this.player.getBukkitEntity().isAwaitingCookies()) {
+            this.postCookies(this.authenticatedProfile);
+        }
+        // CraftBukkit end
+
         if (this.state == LoginListener.EnumProtocolState.WAITING_FOR_DUPE_DISCONNECT && !this.isPlayerAlreadyInWorld((GameProfile) Objects.requireNonNull(this.authenticatedProfile))) {
             this.finishLoginAndWaitForClient(this.authenticatedProfile);
         }
@@ -86,6 +123,13 @@
 
     }
 
+    // CraftBukkit start
+    @Deprecated
+    public void disconnect(String s) {
+        disconnect(IChatBaseComponent.literal(s));
+    }
+    // CraftBukkit end
+
     @Override
     public boolean isAcceptingMessages() {
         return this.connection.isConnected();
@@ -131,7 +175,27 @@
                 this.state = LoginListener.EnumProtocolState.KEY;
                 this.connection.send(new PacketLoginOutEncryptionBegin("", this.server.getKeyPair().getPublic().getEncoded(), this.challenge, true));
             } else {
-                this.startClientVerification(UUIDUtil.createOfflineProfile(this.requestedUsername));
+                // CraftBukkit start
+                Thread thread = new Thread("User Authenticator #" + LoginListener.UNIQUE_THREAD_ID.incrementAndGet()) {
+
+                    @Override
+                    public void run() {
+                        try {
+                            GameProfile gameprofile = UUIDUtil.createOfflineProfile(LoginListener.this.requestedUsername);
+
+                            LoginListener.this.callPlayerPreLoginEvents(gameprofile);
+                            LoginListener.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
+                            LoginListener.this.startClientVerification(gameprofile);
+                        } catch (Exception ex) {
+                            disconnect("Failed to verify username!");
+                            server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + LoginListener.this.requestedUsername, ex);
+                        }
+                    }
+                };
+
+                thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LoginListener.LOGGER));
+                thread.start();
+                // CraftBukkit end
             }
 
         }
@@ -144,10 +208,24 @@
 
     private void verifyLoginAndFinishConnectionSetup(GameProfile gameprofile) {
         PlayerList playerlist = this.server.getPlayerList();
-        IChatBaseComponent ichatbasecomponent = playerlist.canPlayerLogin(this.connection.getRemoteAddress(), gameprofile);
+        // CraftBukkit start - fire PlayerLoginEvent
+        this.player = playerlist.canPlayerLogin(this, gameprofile); // CraftBukkit
 
-        if (ichatbasecomponent != null) {
-            this.disconnect(ichatbasecomponent);
+        if (this.player != null) {
+            if (this.player.getBukkitEntity().isAwaitingCookies()) {
+                this.state = LoginListener.EnumProtocolState.WAITING_FOR_COOKIES;
+            } else {
+                this.postCookies(gameprofile);
+            }
+        }
+    }
+
+    private void postCookies(GameProfile gameprofile) {
+        PlayerList playerlist = this.server.getPlayerList();
+
+        if (this.player == null) {
+            // this.disconnect(ichatbasecomponent);
+            // CraftBukkit end
         } else {
             if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) {
                 this.connection.send(new PacketLoginOutSetCompression(this.server.getCompressionThreshold()), PacketSendListener.thenRun(() -> {
@@ -155,7 +233,7 @@
                 }));
             }
 
-            boolean flag = playerlist.disconnectAllPlayersWithProfile(gameprofile);
+            boolean flag = playerlist.disconnectAllPlayersWithProfile(gameprofile, this.player); // CraftBukkit - add player reference
 
             if (flag) {
                 this.state = LoginListener.EnumProtocolState.WAITING_FOR_DUPE_DISCONNECT;
@@ -205,6 +283,12 @@
                     if (profileresult != null) {
                         GameProfile gameprofile = profileresult.profile();
 
+                        // CraftBukkit start - fire PlayerPreLoginEvent
+                        if (!connection.isConnected()) {
+                            return;
+                        }
+                        LoginListener.this.callPlayerPreLoginEvents(gameprofile);
+                        // CraftBukkit end
                         LoginListener.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
                         LoginListener.this.startClientVerification(gameprofile);
                     } else if (LoginListener.this.server.isSingleplayer()) {
@@ -222,6 +306,11 @@
                         LoginListener.this.disconnect(IChatBaseComponent.translatable("multiplayer.disconnect.authservers_down"));
                         LoginListener.LOGGER.error("Couldn't verify username because servers are unavailable");
                     }
+                    // CraftBukkit start - catch all exceptions
+                } catch (Exception exception) {
+                    disconnect("Failed to verify username!");
+                    server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + s1, exception);
+                    // CraftBukkit end
                 }
 
             }
@@ -238,6 +327,43 @@
         thread.start();
     }
 
+    // CraftBukkit start
+    private void callPlayerPreLoginEvents(GameProfile gameprofile) throws Exception {
+        String playerName = gameprofile.getName();
+        java.net.InetAddress address = ((java.net.InetSocketAddress) connection.getRemoteAddress()).getAddress();
+        java.util.UUID uniqueId = gameprofile.getId();
+        final org.bukkit.craftbukkit.CraftServer server = LoginListener.this.server.server;
+
+        AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, uniqueId, this.transferred);
+        server.getPluginManager().callEvent(asyncEvent);
+
+        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.getKickMessage());
+            }
+            Waitable<PlayerPreLoginEvent.Result> waitable = new Waitable<PlayerPreLoginEvent.Result>() {
+                @Override
+                protected PlayerPreLoginEvent.Result evaluate() {
+                    server.getPluginManager().callEvent(event);
+                    return event.getResult();
+                }
+            };
+
+            LoginListener.this.server.processQueue.add(waitable);
+            if (waitable.get() != PlayerPreLoginEvent.Result.ALLOWED) {
+                disconnect(event.getKickMessage());
+                return;
+            }
+        } else {
+            if (asyncEvent.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
+                disconnect(asyncEvent.getKickMessage());
+                return;
+            }
+        }
+    }
+    // CraftBukkit end
+
     @Override
     public void handleCustomQueryPacket(ServerboundCustomQueryAnswerPacket serverboundcustomqueryanswerpacket) {
         this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
@@ -245,10 +371,11 @@
 
     @Override
     public void handleLoginAcknowledgement(ServerboundLoginAcknowledgedPacket serverboundloginacknowledgedpacket) {
+        PlayerConnectionUtils.ensureRunningOnSameThread(serverboundloginacknowledgedpacket, this, this.server); // CraftBukkit
         Validate.validState(this.state == LoginListener.EnumProtocolState.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet", new Object[0]);
         this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND);
         CommonListenerCookie commonlistenercookie = CommonListenerCookie.createInitial((GameProfile) Objects.requireNonNull(this.authenticatedProfile), this.transferred);
-        ServerConfigurationPacketListenerImpl serverconfigurationpacketlistenerimpl = new ServerConfigurationPacketListenerImpl(this.server, this.connection, commonlistenercookie);
+        ServerConfigurationPacketListenerImpl serverconfigurationpacketlistenerimpl = new ServerConfigurationPacketListenerImpl(this.server, this.connection, commonlistenercookie, this.player); // CraftBukkit
 
         this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, serverconfigurationpacketlistenerimpl);
         serverconfigurationpacketlistenerimpl.startConfiguration();
@@ -264,12 +391,18 @@
 
     @Override
     public void handleCookieResponse(ServerboundCookieResponsePacket serverboundcookieresponsepacket) {
+        // CraftBukkit start
+        PlayerConnectionUtils.ensureRunningOnSameThread(serverboundcookieresponsepacket, this, this.server);
+        if (this.player != null && this.player.getBukkitEntity().handleCookieResponse(serverboundcookieresponsepacket)) {
+            return;
+        }
+        // CraftBukkit end
         this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
     }
 
     private static enum EnumProtocolState {
 
-        HELLO, KEY, AUTHENTICATING, NEGOTIATING, VERIFYING, WAITING_FOR_DUPE_DISCONNECT, PROTOCOL_SWITCHING, ACCEPTED;
+        HELLO, KEY, AUTHENTICATING, NEGOTIATING, VERIFYING, WAITING_FOR_COOKIES, WAITING_FOR_DUPE_DISCONNECT, PROTOCOL_SWITCHING, ACCEPTED; // CraftBukkit
 
         private EnumProtocolState() {}
     }
