40 private const KEYS_ON_COMPLETION =
"completion";
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";
49 private string $issuer;
58 \Closure $onCompletion
60 $this->
storeLocal(self::KEYS_ON_COMPLETION, $onCompletion);
68 $authServiceUri = $this->getAuthServiceURI();
69 }
catch(\RuntimeException $e){
70 $errors[] = $e->getMessage();
71 $authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK;
75 $openIdConfig = $this->getOpenIdConfiguration($authServiceUri);
76 $jwksUri = $openIdConfig->jwks_uri;
78 $this->issuer = $openIdConfig->issuer;
79 }
catch (\RuntimeException $e) {
80 $errors[] = $e->getMessage();
81 $jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH;
83 $this->issuer = $authServiceUri;
87 $this->keys =
new NonThreadSafeValue($this->getKeys($jwksUri));
88 }
catch(\RuntimeException $e){
89 $errors[] = $e->getMessage();
92 $this->errors = $errors === [] ? null :
new NonThreadSafeValue($errors);
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");
102 $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
103 }
catch(\JsonException $e){
104 throw new \RuntimeException($e->getMessage(), 0, $e);
106 if(!is_object($json)){
107 throw new \RuntimeException(
"Unexpected root type of schema file " . gettype($json) .
", expected object");
110 $mapper = new \JsonMapper();
111 $mapper->bExceptionOnUndefinedProperty =
false;
112 $mapper->bExceptionOnMissingData =
true;
113 $mapper->bStrictObjectTypeChecking =
true;
114 $mapper->bEnforceMapType =
false;
115 $mapper->bRemoveUndefinedAttributes =
true;
118 $discovery = $mapper->map($json,
new MinecraftServicesDiscovery());
119 }
catch(\JsonMapper_Exception $e){
120 throw new \RuntimeException(
"Invalid schema file: " . $e->getMessage(), 0, $e);
123 return $discovery->result->serviceEnvironments->auth->prod->serviceUri;
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");
133 $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
134 }
catch(\JsonException $e){
135 throw new \RuntimeException($e->getMessage(), 0, $e);
137 if(!is_object($json)){
138 throw new \RuntimeException(
"Unexpected root type of schema file " . gettype($json) .
", expected object");
141 $mapper = new \JsonMapper();
142 $mapper->bExceptionOnUndefinedProperty =
false;
143 $mapper->bExceptionOnMissingData =
true;
144 $mapper->bStrictObjectTypeChecking =
true;
145 $mapper->bEnforceMapType =
false;
146 $mapper->bRemoveUndefinedAttributes =
true;
149 $configuration = $mapper->map($json,
new AuthServiceOpenIdConfiguration());
150 }
catch(\JsonMapper_Exception $e){
151 throw new \RuntimeException(
"Invalid schema file: " . $e->getMessage(), 0, $e);
154 return $configuration;
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");
167 $json = json_decode($result->getBody(), true, flags: JSON_THROW_ON_ERROR);
168 }
catch(\JsonException $e){
169 throw new \RuntimeException($e->getMessage(), 0, $e);
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");
176 $mapper = new \JsonMapper();
177 $mapper->bExceptionOnUndefinedProperty =
true;
178 $mapper->bExceptionOnMissingData =
true;
179 $mapper->bStrictObjectTypeChecking =
true;
180 $mapper->bEnforceMapType =
false;
181 $mapper->bRemoveUndefinedAttributes =
true;
184 foreach($keysArray as $keyJson){
185 if(!is_array($keyJson)){
186 throw new \RuntimeException(
"Unexpected key type in schema file: " . gettype($keyJson) .
", expected object");
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);
206 $callback = $this->fetchLocal(self::KEYS_ON_COMPLETION);
207 $callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize());