73        private \Closure $playerInfoConsumer,
 
   74        private \Closure $authCallback
 
 
   77    private static function calculateUuidFromXuid(
string $xuid) : UuidInterface{
 
   78        $hash = md5(
"pocket-auth-1-xuid:" . $xuid, binary: true);
 
   79        $hash[6] = chr((ord($hash[6]) & 0x0f) | 0x30); 
 
   80        $hash[8] = chr((ord($hash[8]) & 0x3f) | 0x80); 
 
   82        return Uuid::fromBytes($hash);
 
   85    public function handleLogin(LoginPacket $packet) : bool{
 
   86        $authInfo = $this->parseAuthInfo($packet->authInfoJson);
 
   88        if($authInfo->AuthenticationType === AuthenticationType::FULL->value){
 
   90                [$headerArray, $claimsArray,] = JwtUtils::parse($authInfo->Token);
 
   92                throw PacketHandlingException::wrap($e, 
"Error parsing authentication token");
 
   94            $header = $this->mapXboxTokenHeader($headerArray);
 
   95            $claims = $this->mapXboxTokenBody($claimsArray);
 
   97            $legacyUuid = self::calculateUuidFromXuid($claims->xid);
 
   98            $username = $claims->xname;
 
  101            $authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
 
  102            if($authRequired === 
null){
 
  106            $this->processOpenIdLogin($authInfo->Token, $header->kid, $packet->clientDataJwt, $authRequired);
 
  108        }elseif($authInfo->AuthenticationType === AuthenticationType::SELF_SIGNED->value){
 
  110                $chainData = json_decode($authInfo->Certificate, flags: JSON_THROW_ON_ERROR);
 
  111            }
catch(\JsonException $e){
 
  112                throw PacketHandlingException::wrap($e, 
"Error parsing self-signed certificate chain");
 
  114            if(!is_object($chainData)){
 
  115                throw new PacketHandlingException(
"Unexpected type for self-signed certificate chain: " . gettype($chainData) . 
", expected object");
 
  118                $chain = $this->defaultJsonMapper(
"Self-signed auth chain JSON")->map($chainData, 
new LegacyAuthChain());
 
  119            }
catch(\JsonMapper_Exception $e){
 
  120                throw PacketHandlingException::wrap($e, 
"Error mapping self-signed certificate chain");
 
  122            if(count($chain->chain) > 1 || !isset($chain->chain[0])){
 
  123                throw new PacketHandlingException(
"Expected exactly one certificate in self-signed certificate chain, got " . count($chain->chain));
 
  127                [, $claimsArray, ] = JwtUtils::parse($chain->chain[0]);
 
  128            }
catch(JwtException $e){
 
  129                throw PacketHandlingException::wrap($e, 
"Error parsing self-signed certificate");
 
  131            if(!isset($claimsArray[
"extraData"]) || !is_array($claimsArray[
"extraData"])){
 
  132                throw new PacketHandlingException(
"Expected \"extraData\" to be present in self-signed certificate");
 
  136                $claims = $this->defaultJsonMapper(
"Self-signed auth JWT 'extraData'")->map($claimsArray[
"extraData"], 
new LegacyAuthIdentityData());
 
  137            }
catch(\JsonMapper_Exception $e){
 
  138                throw PacketHandlingException::wrap($e, 
"Error mapping self-signed certificate extraData");
 
  141            if(!Uuid::isValid($claims->identity)){
 
  142                throw new PacketHandlingException(
"Invalid UUID string in self-signed certificate: " . $claims->identity);
 
  144            $legacyUuid = Uuid::fromString($claims->identity);
 
  145            $username = $claims->displayName;
 
  148            $authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
 
  149            if($authRequired === 
null){
 
  153            $this->processSelfSignedLogin($chain->chain, $packet->clientDataJwt, $authRequired);
 
  155            throw new PacketHandlingException(
"Unsupported authentication type: $authInfo->AuthenticationType");
 
  161    private function processLoginCommon(LoginPacket $packet, 
string $username, UuidInterface $legacyUuid, 
string $xuid) : ?bool{
 
  162        if(!Player::isValidUserName($username)){
 
  163            $this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
 
  168        $clientData = $this->parseClientData($packet->clientDataJwt);
 
  171            $skin = $this->session->getTypeConverter()->getSkinAdapter()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData));
 
  172        }
catch(\InvalidArgumentException | InvalidSkinException $e){
 
  173            $this->session->disconnectWithError(
 
  174                reason: 
"Invalid skin: " . $e->getMessage(),
 
  175                disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin()
 
  182            $playerInfo = 
new XboxLivePlayerInfo(
 
  187                $clientData->LanguageCode,
 
  191            $playerInfo = 
new PlayerInfo(
 
  195                $clientData->LanguageCode,
 
  199        ($this->playerInfoConsumer)($playerInfo);
 
  201        $ev = 
new PlayerPreLoginEvent(
 
  203            $this->session->getIp(),
 
  204            $this->session->getPort(),
 
  205            $this->server->requiresAuthentication()
 
  207        if($this->
server->getNetwork()->getValidConnectionCount() > $this->server->getMaxPlayers()){
 
  208            $ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_SERVER_FULL, KnownTranslationFactory::disconnectionScreen_serverFull());
 
  210        if(!$this->
server->isWhitelisted($playerInfo->getUsername())){
 
  211            $ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_SERVER_WHITELISTED, KnownTranslationFactory::pocketmine_disconnect_whitelisted());
 
  215        if(($banEntry = $this->
server->getNameBans()->getEntry($playerInfo->getUsername())) !== 
null){
 
  216            $banReason = $banEntry->getReason();
 
  217            $banMessage = $banReason === 
"" ? KnownTranslationFactory::pocketmine_disconnect_ban_noReason() : KnownTranslationFactory::pocketmine_disconnect_ban($banReason);
 
  218        }elseif(($banEntry = $this->
server->getIPBans()->getEntry($this->session->getIp())) !== 
null){
 
  219            $banReason = $banEntry->getReason();
 
  220            $banMessage = KnownTranslationFactory::pocketmine_disconnect_ban($banReason !== 
"" ? $banReason : KnownTranslationFactory::pocketmine_disconnect_ban_ip());
 
  222        if($banMessage !== 
null){
 
  223            $ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_BANNED, $banMessage);
 
  227        if(!$ev->isAllowed()){
 
  228            $this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage());
 
  232        return $ev->isAuthRequired();
 
  240            $authInfoJson = json_decode($authInfo, associative: false, flags: JSON_THROW_ON_ERROR);
 
  241        }
catch(\JsonException $e){
 
  242            throw PacketHandlingException::wrap($e);
 
  244        if(!is_object($authInfoJson)){
 
  245            throw new PacketHandlingException(
"Unexpected type for auth info data: " . gettype($authInfoJson) . 
", expected object");
 
  248        $mapper = $this->defaultJsonMapper(
"Root authentication info JSON");
 
  250            $clientData = $mapper->map($authInfoJson, 
new AuthenticationInfo());
 
  251        }
catch(\JsonMapper_Exception $e){
 
  252            throw PacketHandlingException::wrap($e);
 
 
  262        $mapper = $this->defaultJsonMapper(
"OpenID JWT header");
 
  265        }
catch(\JsonMapper_Exception $e){
 
  266            throw PacketHandlingException::wrap($e);
 
 
  276        $mapper = $this->defaultJsonMapper(
"OpenID JWT body");
 
  279        }
catch(\JsonMapper_Exception $e){
 
  280            throw PacketHandlingException::wrap($e);
 
 
  290            [, $clientDataClaims, ] = 
JwtUtils::parse($clientDataJwt);
 
  292            throw PacketHandlingException::wrap($e);
 
  295        $mapper = $this->defaultJsonMapper(
"ClientData JWT body");
 
  297            $clientData = $mapper->map($clientDataClaims, 
new ClientData());
 
  298        }
catch(\JsonMapper_Exception $e){
 
  299            throw PacketHandlingException::wrap($e);
 
 
  310    protected function processOpenIdLogin(
string $token, 
string $keyId, 
string $clientData, 
bool $authRequired) : void{
 
  311        $this->session->setHandler(null); 
 
  313        $authKeyProvider = $this->
server->getAuthKeyProvider();
 
  315        $authKeyProvider->getKey($keyId)->onCompletion(
 
  316            function(array $issuerAndKey) use ($token, $clientData, $authRequired) : 
void{
 
  317                [$issuer, $mojangPublicKeyPem] = $issuerAndKey;
 
  318                $this->
server->getAsyncPool()->submitTask(
new ProcessOpenIdLoginTask($token, $issuer, $mojangPublicKeyPem, $clientData, $authRequired, $this->authCallback));
 
  320            fn() => ($this->authCallback)(
false, $authRequired, 
"Unrecognized authentication key ID: $keyId", 
null)
 
 
  328        $this->session->setHandler(null); 
 
  330        $this->
server->getAsyncPool()->submitTask(
new ProcessLegacyLoginTask($legacyCertificate, $clientDataJwt, rootAuthKeyDer: 
null, authRequired: $authRequired, onCompletion: $this->authCallback));
 
 
  333    private function defaultJsonMapper(
string $logContext) : \JsonMapper{
 
  334        $mapper = new \JsonMapper();
 
  335        $mapper->bExceptionOnMissingData = 
true;
 
  336        $mapper->undefinedPropertyHandler = $this->warnUndefinedJsonPropertyHandler($logContext);
 
  337        $mapper->bStrictObjectTypeChecking = 
true;
 
  338        $mapper->bEnforceMapType = 
false;
 
  345    private function warnUndefinedJsonPropertyHandler(
string $context) : \Closure{
 
  346        return fn(object $object, string $name, mixed $value) => $this->session->getLogger()->warning(
 
  347            "$context: Unexpected JSON property for " . (new \ReflectionClass($object))->getShortName() . 
": " . $name . 
" = " . var_export($value, return: true)