42 private const KEYS_ON_COMPLETION =
"completion";
44 private const MINECRAFT_SERVICES_DISCOVERY_URL =
"https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/" . ProtocolInfo::MINECRAFT_VERSION_NETWORK;
45 private const AUTHORIZATION_SERVICE_URI_FALLBACK =
"https://authorization.franchise.minecraft-services.net";
46 private const AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH =
"/.well-known/openid-configuration";
47 private const AUTHORIZATION_SERVICE_KEYS_PATH =
"/.well-known/keys";
51 private string $issuer;
60 \Closure $onCompletion
62 $this->
storeLocal(self::KEYS_ON_COMPLETION, $onCompletion);
70 $authServiceUri = $this->getAuthServiceURI();
71 }
catch(\RuntimeException $e){
72 $errors[] = $e->getMessage();
73 $authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK;
77 $openIdConfig = $this->getOpenIdConfiguration($authServiceUri);
78 $jwksUri = $openIdConfig->jwks_uri;
80 $this->issuer = $openIdConfig->issuer;
81 }
catch (\RuntimeException $e) {
82 $errors[] = $e->getMessage();
83 $jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH;
85 $this->issuer = $authServiceUri;
89 $this->keys =
new NonThreadSafeValue($this->getKeys($jwksUri));
90 }
catch(\RuntimeException $e){
91 $errors[] = $e->getMessage();
94 $this->errors = $errors === [] ? null :
new NonThreadSafeValue($errors);
100 private static function fetchURL(
string $url,
int $expectedCode) : InternetRequestResult{
102 $result = Internet::simpleCurl($url);
103 if($result->getCode() !== $expectedCode){
104 throw new \RuntimeException(
"Unexpected HTTP response code accessing \"$url\": " . $result->getCode());
107 }
catch(InternetException $e){
108 throw new \RuntimeException(
"Failed accessing \"$url\": " . $e->getMessage(), 0, $e);
112 private function getAuthServiceURI() : string{
113 $result = self::fetchURL(self::MINECRAFT_SERVICES_DISCOVERY_URL, 200);
115 $json = json_decode($result->getBody(),
false, flags: JSON_THROW_ON_ERROR);
116 }
catch(\JsonException $e){
117 throw new \RuntimeException($e->getMessage(), 0, $e);
119 if(!is_object($json)){
120 throw new \RuntimeException(
"Unexpected root type of schema file " . gettype($json) .
", expected object");
123 $mapper = new \JsonMapper();
124 $mapper->bExceptionOnUndefinedProperty =
false;
125 $mapper->bExceptionOnMissingData =
true;
126 $mapper->bStrictObjectTypeChecking =
true;
127 $mapper->bEnforceMapType =
false;
128 $mapper->bRemoveUndefinedAttributes =
true;
131 $discovery = $mapper->map($json,
new MinecraftServicesDiscovery());
132 }
catch(\JsonMapper_Exception $e){
133 throw new \RuntimeException(
"Invalid schema file: " . $e->getMessage(), 0, $e);
136 return $discovery->result->serviceEnvironments->auth->prod->serviceUri;
139 private function getOpenIdConfiguration(
string $authServiceUri) : AuthServiceOpenIdConfiguration{
140 $result = self::fetchURL($authServiceUri . self::AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH, 200);
143 $json = json_decode($result->getBody(),
false, flags: JSON_THROW_ON_ERROR);
144 }
catch(\JsonException $e){
145 throw new \RuntimeException($e->getMessage(), 0, $e);
147 if(!is_object($json)){
148 throw new \RuntimeException(
"Unexpected root type of schema file " . gettype($json) .
", expected object");
151 $mapper = new \JsonMapper();
152 $mapper->bExceptionOnUndefinedProperty =
false;
153 $mapper->bExceptionOnMissingData =
true;
154 $mapper->bStrictObjectTypeChecking =
true;
155 $mapper->bEnforceMapType =
false;
156 $mapper->bRemoveUndefinedAttributes =
true;
159 $configuration = $mapper->map($json,
new AuthServiceOpenIdConfiguration());
160 }
catch(\JsonMapper_Exception $e){
161 throw new \RuntimeException(
"Invalid schema file: " . $e->getMessage(), 0, $e);
164 return $configuration;
170 private function getKeys(
string $jwksUri) : array{
171 $result = self::fetchURL($jwksUri, 200);
174 $json = json_decode($result->getBody(),
true, flags: JSON_THROW_ON_ERROR);
175 }
catch(\JsonException $e){
176 throw new \RuntimeException($e->getMessage(), 0, $e);
179 if(!is_array($json) || !isset($json[
"keys"]) || !is_array($keysArray = $json[
"keys"])){
180 throw new \RuntimeException(
"Unexpected root type of schema file " . gettype($json) .
", expected object");
183 $mapper = new \JsonMapper();
184 $mapper->bExceptionOnUndefinedProperty =
true;
185 $mapper->bExceptionOnMissingData =
true;
186 $mapper->bStrictObjectTypeChecking =
true;
187 $mapper->bEnforceMapType =
false;
188 $mapper->bRemoveUndefinedAttributes =
true;
191 foreach($keysArray as $keyJson){
192 if(!is_array($keyJson)){
193 throw new \RuntimeException(
"Unexpected key type in schema file: " . gettype($keyJson) .
", expected object");
198 $key = $mapper->map($keyJson,
new AuthServiceKey());
199 $keys[$key->kid] = $key;
200 }
catch(\JsonMapper_Exception $e){
201 throw new \RuntimeException(
"Invalid schema file: " . $e->getMessage(), 0, $e);
213 $callback = $this->fetchLocal(self::KEYS_ON_COMPLETION);
214 $callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize());