39 private const ALLOWED_REFRESH_INTERVAL = 30 * 60;
46 private int $lastFetch = 0;
48 public function __construct(
49 private readonly \
Logger $logger,
51 private readonly
int $keyRefreshIntervalSeconds = self::ALLOWED_REFRESH_INTERVAL
65 $this->keyring ===
null ||
66 ($this->keyring->getKey($keyId) ===
null && $this->lastFetch < time() - $this->keyRefreshIntervalSeconds)
69 $this->fetchKeys()->onCompletion(
70 onSuccess: fn(
AuthKeyring $newKeyring) => $this->resolveKey($resolver, $newKeyring, $keyId),
71 onFailure: $resolver->reject(...)
74 $this->resolveKey($resolver, $this->keyring, $keyId);
83 private function resolveKey(
PromiseResolver $resolver, AuthKeyring $keyring,
string $keyId) : void{
84 $key = $keyring->getKey($keyId);
86 $this->logger->debug(
"Key $keyId not recognised!");
91 $this->logger->debug(
"Key $keyId found in keychain");
92 $resolver->
resolve([$keyring->getIssuer(), $key]);
99 private function onKeysFetched(?array $keys,
string $issuer, ?array $errors) : void{
100 $resolver = $this->resolver;
101 if($resolver ===
null){
102 throw new AssumptionFailedError(
"Not expecting this to be called without a resolver present");
105 if($errors !==
null){
106 $this->logger->error(
"The following errors occurred while fetching new keys:\n\t- " . implode(
"\n\t-", $errors));
111 $this->logger->critical(
"Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!");
115 foreach($keys as $keyModel){
116 if($keyModel->use !==
"sig" || $keyModel->kty !==
"RSA"){
117 $this->logger->error(
"Key ID $keyModel->kid doesn't have the expected properties: expected use=sig, kty=RSA, got use=$keyModel->use, kty=$keyModel->kty");
120 $derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e);
124 JwtUtils::parseDerPublicKey($derKey);
125 }
catch(JwtException $e){
126 $this->logger->error(
"Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage());
127 $this->logger->logException($e);
132 $pemKeys[$keyModel->kid] = $derKey;
135 if(count($keys) === 0){
136 $this->logger->critical(
"No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!");
139 $this->logger->info(
"Successfully fetched " . count($keys) .
" new authentication keys from issuer $issuer, key IDs: " . implode(
", ", array_keys($pemKeys)));
140 $this->keyring =
new AuthKeyring($issuer, $pemKeys);
141 $this->lastFetch = time();
142 $resolver->
resolve($this->keyring);
146 $this->resolver =
null;
153 private function fetchKeys() : Promise{
154 if($this->resolver !== null){
155 $this->logger->debug(
"Key refresh was requested, but it's already in progress");
156 return $this->resolver->getPromise();
159 $this->logger->notice(
"Fetching new authentication keys");
162 $resolver =
new PromiseResolver();
163 $this->resolver = $resolver;
165 $this->asyncPool->submitTask(
new FetchAuthKeysTask($this->onKeysFetched(...)));