PocketMine-MP 5.35.1 git-e32e836dad793a3a3c8ddd8927c00e112b1e576a
Loading...
Searching...
No Matches
FetchAuthKeysTask.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe\auth;
25
33use function gettype;
34use function is_array;
35use function is_object;
36use function json_decode;
37use const JSON_THROW_ON_ERROR;
38
40 private const KEYS_ON_COMPLETION = "completion";
41
42 private const MINECRAFT_SERVICES_DISCOVERY_URL = "https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/" . ProtocolInfo::MINECRAFT_VERSION_NETWORK;
43 private const AUTHORIZATION_SERVICE_URI_FALLBACK = "https://authorization.franchise.minecraft-services.net";
44 private const AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH = "/.well-known/openid-configuration";
45 private const AUTHORIZATION_SERVICE_KEYS_PATH = "/.well-known/keys";
46
48 private ?NonThreadSafeValue $keys = null;
49 private string $issuer;
50
52 private ?NonThreadSafeValue $errors = null;
53
57 public function __construct(
58 \Closure $onCompletion
59 ){
60 $this->storeLocal(self::KEYS_ON_COMPLETION, $onCompletion);
61 }
62
63 public function onRun() : void{
65 $errors = [];
66
67 try{
68 $authServiceUri = $this->getAuthServiceURI();
69 }catch(\RuntimeException $e){
70 $errors[] = $e->getMessage();
71 $authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK;
72 }
73
74 try {
75 $openIdConfig = $this->getOpenIdConfiguration($authServiceUri);
76 $jwksUri = $openIdConfig->jwks_uri;
77
78 $this->issuer = $openIdConfig->issuer;
79 } catch (\RuntimeException $e) {
80 $errors[] = $e->getMessage();
81 $jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH;
82
83 $this->issuer = $authServiceUri;
84 }
85
86 try{
87 $this->keys = new NonThreadSafeValue($this->getKeys($jwksUri));
88 }catch(\RuntimeException $e){
89 $errors[] = $e->getMessage();
90 }
91
92 $this->errors = $errors === [] ? null : new NonThreadSafeValue($errors);
93 }
94
95 private function getAuthServiceURI() : string{
96 $result = Internet::getURL(self::MINECRAFT_SERVICES_DISCOVERY_URL);
97 if($result === null || $result->getCode() !== 200){
98 throw new \RuntimeException("Failed to fetch Minecraft services discovery document");
99 }
100
101 try{
102 $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
103 }catch(\JsonException $e){
104 throw new \RuntimeException($e->getMessage(), 0, $e);
105 }
106 if(!is_object($json)){
107 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
108 }
109
110 $mapper = new \JsonMapper();
111 $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
112 $mapper->bExceptionOnMissingData = true;
113 $mapper->bStrictObjectTypeChecking = true;
114 $mapper->bEnforceMapType = false;
115 $mapper->bRemoveUndefinedAttributes = true;
116 try{
118 $discovery = $mapper->map($json, new MinecraftServicesDiscovery());
119 }catch(\JsonMapper_Exception $e){
120 throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
121 }
122
123 return $discovery->result->serviceEnvironments->auth->prod->serviceUri;
124 }
125
126 private function getOpenIdConfiguration(string $authServiceUri) : AuthServiceOpenIdConfiguration{
127 $result = Internet::getURL($authServiceUri . self::AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH);
128 if($result === null || $result->getCode() !== 200){
129 throw new \RuntimeException("Failed to fetch OpenID configuration from authorization service");
130 }
131
132 try{
133 $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
134 }catch(\JsonException $e){
135 throw new \RuntimeException($e->getMessage(), 0, $e);
136 }
137 if(!is_object($json)){
138 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
139 }
140
141 $mapper = new \JsonMapper();
142 $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
143 $mapper->bExceptionOnMissingData = true;
144 $mapper->bStrictObjectTypeChecking = true;
145 $mapper->bEnforceMapType = false;
146 $mapper->bRemoveUndefinedAttributes = true;
147 try{
149 $configuration = $mapper->map($json, new AuthServiceOpenIdConfiguration());
150 }catch(\JsonMapper_Exception $e){
151 throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
152 }
153
154 return $configuration;
155 }
156
160 private function getKeys(string $jwksUri) : array{
161 $result = Internet::getURL($jwksUri);
162 if($result === null || $result->getCode() !== 200){
163 return throw new \RuntimeException("Failed to fetch keys from authorization service");
164 }
165
166 try{
167 $json = json_decode($result->getBody(), true, flags: JSON_THROW_ON_ERROR);
168 }catch(\JsonException $e){
169 throw new \RuntimeException($e->getMessage(), 0, $e);
170 }
171
172 if(!is_array($json) || !isset($json["keys"]) || !is_array($keysArray = $json["keys"])){
173 throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
174 }
175
176 $mapper = new \JsonMapper();
177 $mapper->bExceptionOnUndefinedProperty = true;
178 $mapper->bExceptionOnMissingData = true;
179 $mapper->bStrictObjectTypeChecking = true;
180 $mapper->bEnforceMapType = false;
181 $mapper->bRemoveUndefinedAttributes = true;
182
183 $keys = [];
184 foreach($keysArray as $keyJson){
185 if(!is_array($keyJson)){
186 throw new \RuntimeException("Unexpected key type in schema file: " . gettype($keyJson) . ", expected object");
187 }
188
189 try{
191 $key = $mapper->map($keyJson, new AuthServiceKey());
192 $keys[$key->kid] = $key;
193 }catch(\JsonMapper_Exception $e){
194 throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
195 }
196 }
197
198 return $keys;
199 }
200
201 public function onCompletion() : void{
206 $callback = $this->fetchLocal(self::KEYS_ON_COMPLETION);
207 $callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize());
208 }
209}
storeLocal(string $key, mixed $complexData)