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");
104 if($errors !==
null){
105 $this->logger->error(
"The following errors occurred while fetching new keys:\n\t- " . implode(
"\n\t-", $errors));
110 $this->logger->critical(
"Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!");
114 foreach($keys as $keyModel){
115 if($keyModel->use !==
"sig" || $keyModel->kty !==
"RSA"){
116 $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");
119 $derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e);
123 JwtUtils::parseDerPublicKey($derKey);
124 }
catch(JwtException $e){
125 $this->logger->error(
"Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage());
126 $this->logger->logException($e);
131 $pemKeys[$keyModel->kid] = $derKey;
134 if(count($keys) === 0){
135 $this->logger->critical(
"No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!");
138 $this->logger->info(
"Successfully fetched " . count($keys) .
" new authentication keys from issuer $issuer, key IDs: " . implode(
", ", array_keys($pemKeys)));
139 $this->keyring =
new AuthKeyring($issuer, $pemKeys);
140 $this->lastFetch = time();
141 $resolver->
resolve($this->keyring);
149 private function fetchKeys() : Promise{
150 if($this->resolver !== null){
151 $this->logger->debug(
"Key refresh was requested, but it's already in progress");
152 return $this->resolver->getPromise();
155 $this->logger->notice(
"Fetching new authentication keys");
158 $resolver =
new PromiseResolver();
159 $this->resolver = $resolver;
161 $this->asyncPool->submitTask(
new FetchAuthKeysTask($this->onKeysFetched(...)));