diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index daac4943904b..f49373397913 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -92,6 +92,23 @@ public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $ $message = "\n"; $rateLimited = $gitHubUtil->isRateLimited($headers); + $requiresSso = $gitHubUtil->requiresSso($headers); + + if ($requiresSso) { + $ssoUrl = $gitHubUtil->getSsoUrl($headers); + $message = sprintf( + 'GitHub API token requires SSO authorization. Authorize this token at ' . $ssoUrl, + $ssoUrl + ) . "\n"; + $this->io->writeError($message); + if (!$this->io->isInteractive()) { + throw new TransportException('Could not authenticate against ' . $origin, 403); + } + $this->io->ask('After authorizing your token, confirm that you would like to retry the request'); + + return array('retry' => true, 'storeAuth' => $storeAuth); + } + if ($rateLimited) { $rateLimit = $gitHubUtil->getRateLimit($headers); if ($this->io->hasAuthentication($origin)) { diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 1c5b9a2aa44b..2dd53e3506c6 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -171,6 +171,28 @@ public function getRateLimit(array $headers) return $rateLimit; } + /** + * Extract SSO URL from response. + * + * @param string[] $headers Headers from Composer\Downloader\TransportException. + * + * @return string|null + */ + public function getSsoUrl(array $headers) + { + foreach ($headers as $header) { + $header = trim($header); + if (false === stripos($header, 'x-github-sso: required')) { + continue; + } + if (Preg::isMatch('{\burl=(?P[^\s;]+)}', $header, $match)) { + return $match['url']; + } + } + + return null; + } + /** * Finds whether a request failed due to rate limiting * @@ -188,4 +210,24 @@ public function isRateLimited(array $headers) return false; } + + /** + * Finds whether a request failed due to lacking SSO authorization + * + * @see https://docs.github.com/en/rest/overview/other-authentication-methods#authenticating-for-saml-sso + * + * @param string[] $headers Headers from Composer\Downloader\TransportException. + * + * @return bool + */ + public function requiresSso(array $headers) + { + foreach ($headers as $header) { + if (Preg::isMatch('{^X-GitHub-SSO: required}i', trim($header))) { + return true; + } + } + + return false; + } }