--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -32,30 +32,42 @@
 import net.minecraft.network.protocol.login.ServerboundKeyPacket;
 import net.minecraft.network.protocol.login.ServerboundLoginAcknowledgedPacket;
 import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.server.players.PlayerList;
 import net.minecraft.util.Crypt;
 import net.minecraft.util.CryptException;
 import net.minecraft.util.RandomSource;
 import org.apache.commons.lang3.Validate;
 import org.slf4j.Logger;
+import org.bukkit.craftbukkit.v1_20_R2.util.Waitable;
+import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
+import org.bukkit.event.player.PlayerPreLoginEvent;
+// CraftBukkit end
 
 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 int MAX_TICKS_BEFORE_LOGIN = 600;
     private static final Component DISCONNECT_UNEXPECTED_QUERY = Component.translatable("multiplayer.disconnect.unexpected_query_response");
+    private static final RandomSource RANDOM = new org.bukkit.craftbukkit.v1_20_R2.util.RandomSourceWrapper(new java.util.Random()); // Paper - This is called across threads, make safe
     private final byte[] challenge;
     final MinecraftServer server;
     public final Connection connection;
-    public volatile ServerLoginPacketListenerImpl.State state = ServerLoginPacketListenerImpl.State.HELLO;
+    public volatile ServerLoginPacketListenerImpl.State state;
     private int tick;
     @Nullable
     String requestedUsername;
     @Nullable
-    private GameProfile authenticatedProfile;
-    private final String serverId = "";
+    public GameProfile authenticatedProfile; // Paper - public
+    private final String serverId;
+    private ServerPlayer player; // CraftBukkit
+    public boolean iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation = false; // Paper - username validation overriding
+    private int velocityLoginMessageId = -1; // Paper - Velocity support
 
     public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection) {
+        this.state = ServerLoginPacketListenerImpl.State.HELLO;
+        this.serverId = "";
         this.server = server;
         this.connection = connection;
         this.challenge = Ints.toByteArray(RandomSource.create().nextInt());
@@ -63,11 +75,19 @@
 
     @Override
     public void tick() {
+        // Paper start - Do not allow logins while the server is shutting down
+        if (!MinecraftServer.getServer().isRunning()) {
+            this.disconnect(org.bukkit.craftbukkit.v1_20_R2.util.CraftChatMessage.fromString(org.spigotmc.SpigotConfig.restartMessage)[0]);
+            return;
+        }
+        // Paper end
         if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) {
-            this.verifyLoginAndFinishConnectionSetup(Objects.requireNonNull(this.authenticatedProfile));
+            if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called
+            this.verifyLoginAndFinishConnectionSetup((GameProfile) Objects.requireNonNull(this.authenticatedProfile));
+            } // Paper
         }
 
-        if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT && !this.isPlayerAlreadyInWorld(Objects.requireNonNull(this.authenticatedProfile))) {
+        if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT && !this.isPlayerAlreadyInWorld((GameProfile) Objects.requireNonNull(this.authenticatedProfile))) {
             this.finishLoginAndWaitForClient(this.authenticatedProfile);
         }
 
@@ -77,6 +97,13 @@
 
     }
 
+    // CraftBukkit start
+    @Deprecated
+    public void disconnect(String s) {
+        this.disconnect(org.bukkit.craftbukkit.v1_20_R2.util.CraftChatMessage.fromString(s, true)[0]); // Paper - Fix hex colors not working in some kick messages
+    }
+    // CraftBukkit end
+
     @Override
     public boolean isAcceptingMessages() {
         return this.connection.isConnected();
@@ -84,11 +111,11 @@
 
     public void disconnect(Component reason) {
         try {
-            LOGGER.info("Disconnecting {}: {}", this.getUserName(), reason.getString());
+            ServerLoginPacketListenerImpl.LOGGER.info("Disconnecting {}: {}", this.getUserName(), reason.getString());
             this.connection.send(new ClientboundLoginDisconnectPacket(reason));
             this.connection.disconnect(reason);
-        } catch (Exception var3) {
-            LOGGER.error("Error whilst disconnecting player", (Throwable)var3);
+        } catch (Exception exception) {
+            ServerLoginPacketListenerImpl.LOGGER.error("Error whilst disconnecting player", exception);
         }
 
     }
@@ -99,28 +126,82 @@
 
     @Override
     public void onDisconnect(Component reason) {
-        LOGGER.info("{} lost connection: {}", this.getUserName(), reason.getString());
+        ServerLoginPacketListenerImpl.LOGGER.info("{} lost connection: {}", this.getUserName(), reason.getString());
     }
 
     public String getUserName() {
-        String string = this.connection.getLoggableAddress(this.server.logIPs());
-        return this.requestedUsername != null ? this.requestedUsername + " (" + string + ")" : string;
+        String s = this.connection.getLoggableAddress(this.server.logIPs());
+
+        return this.requestedUsername != null ? this.requestedUsername + " (" + s + ")" : s;
     }
 
+    // Paper start - validate usernames
+    public static boolean validateUsername(String in) {
+        if (in == null || in.isEmpty() || in.length() > 16) {
+            return false;
+        }
+
+        for (int i = 0, len = in.length(); i < len; ++i) {
+            char c = in.charAt(i);
+
+            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_' || c == '.')) {
+                continue;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+    // Paper end - validate usernames
+
     @Override
     public void handleHello(ServerboundHelloPacket packet) {
-        Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet");
-        Validate.validState(isValidUsername(packet.name()), "Invalid characters in username");
+        Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet", new Object[0]);
+        Validate.validState(ServerLoginPacketListenerImpl.isValidUsername(packet.name()), "Invalid characters in username", new Object[0]);
+        // Paper start - validate usernames
+        if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.performUsernameValidation) {
+            if (!this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation && !validateUsername(packet.name())) {
+                ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
+                return;
+            }
+        }
+        // Paper end - validate usernames
         this.requestedUsername = packet.name();
-        GameProfile gameProfile = this.server.getSingleplayerProfile();
-        if (gameProfile != null && this.requestedUsername.equalsIgnoreCase(gameProfile.getName())) {
-            this.startClientVerification(gameProfile);
+        GameProfile gameprofile = this.server.getSingleplayerProfile();
+
+        if (gameprofile != null && this.requestedUsername.equalsIgnoreCase(gameprofile.getName())) {
+            this.startClientVerification(gameprofile);
         } else {
             if (this.server.usesAuthentication() && !this.connection.isMemoryConnection()) {
                 this.state = ServerLoginPacketListenerImpl.State.KEY;
                 this.connection.send(new ClientboundHelloPacket("", this.server.getKeyPair().getPublic().getEncoded(), this.challenge));
             } else {
-                this.startClientVerification(createOfflineProfile(this.requestedUsername));
+                // Paper start - Velocity 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
+                // Spigot start
+            // Paper start - Cache authenticator threads
+            authenticatorPool.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            new LoginHandler().fireEvents(ServerLoginPacketListenerImpl.this.createOfflineProfile(ServerLoginPacketListenerImpl.this.requestedUsername));
+                        } catch (Exception ex) {
+                            ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
+                            server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + ServerLoginPacketListenerImpl.this.requestedUsername, ex);
+                        }
+                    }
+            });
+            // Paper end
+                // Spigot end
             }
 
         }
@@ -132,10 +213,13 @@
     }
 
     private void verifyLoginAndFinishConnectionSetup(GameProfile profile) {
-        PlayerList playerList = this.server.getPlayerList();
-        Component component = playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile);
-        if (component != null) {
-            this.disconnect(component);
+        PlayerList playerlist = this.server.getPlayerList();
+        // CraftBukkit start - fire PlayerLoginEvent
+        this.player = playerlist.canPlayerLogin(this, profile); // CraftBukkit
+
+        if (this.player == null) {
+            // this.disconnect(ichatbasecomponent);
+            // CraftBukkit end
         } else {
             if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) {
                 this.connection.send(new ClientboundLoginCompressionPacket(this.server.getCompressionThreshold()), PacketSendListener.thenRun(() -> {
@@ -143,8 +227,9 @@
                 }));
             }
 
-            boolean bl = playerList.disconnectAllPlayersWithProfile(profile);
-            if (bl) {
+            boolean flag = playerlist.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference
+
+            if (flag) {
                 this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT;
             } else {
                 this.finishLoginAndWaitForClient(profile);
@@ -159,100 +244,236 @@
     }
 
     public static boolean isValidUsername(String name) {
-        return name.chars().filter((c) -> {
-            return c <= 32 || c >= 127;
+        return name.chars().filter((i) -> {
+            return i <= 32 || i >= 127;
         }).findAny().isEmpty();
     }
 
     @Override
     public void handleKey(ServerboundKeyPacket packet) {
-        Validate.validState(this.state == ServerLoginPacketListenerImpl.State.KEY, "Unexpected key packet");
+        Validate.validState(this.state == ServerLoginPacketListenerImpl.State.KEY, "Unexpected key packet", new Object[0]);
+
+        final String s;
 
-        final String string;
         try {
-            PrivateKey privateKey = this.server.getKeyPair().getPrivate();
-            if (!packet.isChallengeValid(this.challenge, privateKey)) {
+            PrivateKey privatekey = this.server.getKeyPair().getPrivate();
+
+            if (!packet.isChallengeValid(this.challenge, privatekey)) {
                 throw new IllegalStateException("Protocol error");
             }
 
-            SecretKey secretKey = packet.getSecretKey(privateKey);
-            Cipher cipher = Crypt.getCipher(2, secretKey);
-            Cipher cipher2 = Crypt.getCipher(1, secretKey);
-            string = (new BigInteger(Crypt.digestData("", this.server.getKeyPair().getPublic(), secretKey))).toString(16);
+            SecretKey secretkey = packet.getSecretKey(privatekey);
+            // Paper start
+//            Cipher cipher = Crypt.getCipher(2, secretkey);
+//            Cipher cipher1 = Crypt.getCipher(1, secretkey);
+            // Paper end
+
+            s = (new BigInteger(Crypt.digestData("", this.server.getKeyPair().getPublic(), secretkey))).toString(16);
             this.state = ServerLoginPacketListenerImpl.State.AUTHENTICATING;
-            this.connection.setEncryptionKey(cipher, cipher2);
-        } catch (CryptException var7) {
-            throw new IllegalStateException("Protocol error", var7);
+            this.connection.setupEncryption(secretkey); // Paper
+        } catch (CryptException cryptographyexception) {
+            throw new IllegalStateException("Protocol error", cryptographyexception);
         }
 
-        Thread thread = new Thread("User Authenticator #" + UNIQUE_THREAD_ID.incrementAndGet()) {
-            @Override
+        // Paper start - Cache authenticator threads
+        authenticatorPool.execute(new Runnable() {
             public void run() {
-                String string = Objects.requireNonNull(ServerLoginPacketListenerImpl.this.requestedUsername, "Player name not initialized");
+                String s1 = (String) Objects.requireNonNull(ServerLoginPacketListenerImpl.this.requestedUsername, "Player name not initialized");
 
                 try {
-                    ProfileResult profileResult = ServerLoginPacketListenerImpl.this.server.getSessionService().hasJoinedServer(string, string, this.getAddress());
-                    if (profileResult != null) {
-                        GameProfile gameProfile = profileResult.profile();
-                        ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameProfile.getName(), gameProfile.getId());
-                        ServerLoginPacketListenerImpl.this.startClientVerification(gameProfile);
+                    ProfileResult profileresult = ServerLoginPacketListenerImpl.this.server.getSessionService().hasJoinedServer(s1, s, this.getAddress());
+
+                    if (profileresult != null) {
+                        GameProfile gameprofile = profileresult.profile();
+
+                        // CraftBukkit start - fire PlayerPreLoginEvent
+                        if (!ServerLoginPacketListenerImpl.this.connection.isConnected()) {
+                            return;
+                        }
+
+                        new LoginHandler().fireEvents(gameprofile);
                     } else if (ServerLoginPacketListenerImpl.this.server.isSingleplayer()) {
                         ServerLoginPacketListenerImpl.LOGGER.warn("Failed to verify username but will let them in anyway!");
-                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.createOfflineProfile(string));
+                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(s1)); // Spigot
                     } else {
                         ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.unverified_username"));
-                        ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", (Object)string);
+                        ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", s1);
                     }
-                } catch (AuthenticationUnavailableException var4) {
+                } catch (AuthenticationUnavailableException authenticationunavailableexception) {
                     if (ServerLoginPacketListenerImpl.this.server.isSingleplayer()) {
                         ServerLoginPacketListenerImpl.LOGGER.warn("Authentication servers are down but will let them in anyway!");
-                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.createOfflineProfile(string));
+                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(s1)); // 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
                         ServerLoginPacketListenerImpl.LOGGER.error("Couldn't verify username because servers are unavailable");
                     }
+                    // CraftBukkit start - catch all exceptions
+                } catch (Exception exception) {
+                    ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
+                    server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + s1, exception);
+                    // CraftBukkit end
                 }
 
             }
 
             @Nullable
             private InetAddress getAddress() {
-                SocketAddress socketAddress = ServerLoginPacketListenerImpl.this.connection.getRemoteAddress();
-                return ServerLoginPacketListenerImpl.this.server.getPreventProxyConnections() && socketAddress instanceof InetSocketAddress ? ((InetSocketAddress)socketAddress).getAddress() : null;
+                SocketAddress socketaddress = ServerLoginPacketListenerImpl.this.connection.getRemoteAddress();
+
+                return ServerLoginPacketListenerImpl.this.server.getPreventProxyConnections() && socketaddress instanceof InetSocketAddress ? ((InetSocketAddress) socketaddress).getAddress() : null;
             }
-        };
-        thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER));
-        thread.start();
+        });
+        // Paper end
     }
 
+    // Spigot start
+    public class LoginHandler {
+
+        public void fireEvents(GameProfile gameprofile) throws Exception {
+                        // Paper start - Velocity support
+                        if (ServerLoginPacketListenerImpl.this.velocityLoginMessageId == -1 && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) {
+                            disconnect("This server requires you to connect with Velocity.");
+                            return;
+                        }
+                        // Paper end
+                        String playerName = gameprofile.getName();
+                        java.net.InetAddress address = ((java.net.InetSocketAddress) ServerLoginPacketListenerImpl.this.connection.getRemoteAddress()).getAddress();
+                        java.net.InetAddress rawAddress = ((java.net.InetSocketAddress) ServerLoginPacketListenerImpl.this.connection.channel.remoteAddress()).getAddress(); // Paper
+                        java.util.UUID uniqueId = gameprofile.getId();
+                        final org.bukkit.craftbukkit.v1_20_R2.CraftServer server = ServerLoginPacketListenerImpl.this.server.server;
+
+                        // Paper start
+                        com.destroystokyo.paper.profile.PlayerProfile profile = com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile);
+                        AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, rawAddress, uniqueId, profile, ServerLoginPacketListenerImpl.this.connection.hostname); // Paper - add rawAddress & hostname
+                        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
+
+                        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<PlayerPreLoginEvent.Result>() {
+                                @Override
+                                protected PlayerPreLoginEvent.Result evaluate() {
+                                    server.getPluginManager().callEvent(event);
+                                    return event.getResult();
+                                }};
+
+                            ServerLoginPacketListenerImpl.this.server.processQueue.add(waitable);
+                            if (waitable.get() != PlayerPreLoginEvent.Result.ALLOWED) {
+                                ServerLoginPacketListenerImpl.this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure
+                                return;
+                            }
+                        } else {
+                            if (asyncEvent.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
+                                ServerLoginPacketListenerImpl.this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(asyncEvent.kickMessage())); // Paper - Adventure
+                                return;
+                            }
+                        }
+                        // CraftBukkit end
+                        ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
+                        ServerLoginPacketListenerImpl.this.startClientVerification(gameprofile);
+        }
+    }
+    // Spigot end
+
     @Override
     public void handleCustomQueryPacket(ServerboundCustomQueryAnswerPacket packet) {
-        this.disconnect(DISCONNECT_UNEXPECTED_QUERY);
+        // Paper start - Velocity 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 {
+                    new LoginHandler().fireEvents(this.authenticatedProfile);
+                } 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
+        this.disconnect(ServerLoginPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
     }
 
     @Override
     public void handleLoginAcknowledgement(ServerboundLoginAcknowledgedPacket packet) {
-        Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet");
-        CommonListenerCookie commonListenerCookie = CommonListenerCookie.createInitial(Objects.requireNonNull(this.authenticatedProfile));
-        ServerConfigurationPacketListenerImpl serverConfigurationPacketListenerImpl = new ServerConfigurationPacketListenerImpl(this.server, this.connection, commonListenerCookie);
-        this.connection.setListener(serverConfigurationPacketListenerImpl);
-        serverConfigurationPacketListenerImpl.startConfiguration();
+        Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet", new Object[0]);
+        CommonListenerCookie commonlistenercookie = CommonListenerCookie.createInitial((GameProfile) Objects.requireNonNull(this.authenticatedProfile));
+        ServerConfigurationPacketListenerImpl serverconfigurationpacketlistenerimpl = new ServerConfigurationPacketListenerImpl(this.server, this.connection, commonlistenercookie, this.player); // CraftBukkit
+
+        this.connection.setListener(serverconfigurationpacketlistenerimpl);
+        serverconfigurationpacketlistenerimpl.startConfiguration();
         this.state = ServerLoginPacketListenerImpl.State.ACCEPTED;
     }
 
-    protected static GameProfile createOfflineProfile(String name) {
-        UUID uUID = UUIDUtil.createOfflinePlayerUUID(name);
-        return new GameProfile(uUID, name);
+    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
+
+    // Spigot start
+    protected GameProfile createOfflineProfile(String name) {
+        UUID uuid;
+        if ( connection.spoofedUUID != null )
+        {
+            uuid = connection.spoofedUUID;
+        } else
+        {
+            uuid = UUIDUtil.createOfflinePlayerUUID( name );
+        }
+
+        GameProfile gameProfile = new GameProfile( uuid, name );
+
+        if (connection.spoofedProfile != null)
+        {
+            for ( com.mojang.authlib.properties.Property property : 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,
-        KEY,
-        AUTHENTICATING,
-        NEGOTIATING,
-        VERIFYING,
-        WAITING_FOR_DUPE_DISCONNECT,
-        PROTOCOL_SWITCHING,
-        ACCEPTED;
+
+        HELLO, KEY, AUTHENTICATING, NEGOTIATING, VERIFYING, WAITING_FOR_DUPE_DISCONNECT, PROTOCOL_SWITCHING, ACCEPTED;
+
+        private State() {}
     }
 }
