Procházet zdrojové kódy

Merge remote-tracking branch 'cs278/github-otp-support'

Jordi Boggiano před 11 roky
rodič
revize
9db2a537e5

+ 2 - 2
doc/04-schema.md

@@ -684,8 +684,8 @@ The following options are supported:
   `{"github.com": "oauthtoken"}` as the value of this option will use `oauthtoken`
   to access private repositories on github and to circumvent the low IP-based
   rate limiting of their API.
-  [Read more](articles/troubleshooting.md#api-rate-limit-and-two-factor-authentication)
-  on how to get an oauth token for GitHub.
+  [Read more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens)
+  on how to get an OAuth token for GitHub.
 * **vendor-dir:** Defaults to `vendor`. You can install dependencies into a
   different directory if you want to.
 * **bin-dir:** Defaults to `vendor/bin`. If a project includes binaries, they

+ 5 - 4
doc/articles/troubleshooting.md

@@ -105,14 +105,15 @@ Or, you can increase the limit with a command-line argument:
    or ```HKEY_CURRENT_USER\Software\Microsoft\Command Processor```.
 3. Check if it contains any path to non-existent file, if it's the case, just remove them.
 
-## API rate limit and two factor authentication
+## API rate limit and OAuth tokens
 
 Because of GitHub's rate limits on their API it can happen that Composer prompts
 for authentication asking your username and password so it can go ahead with its work.
-Unfortunately this will not work if you enabled two factor authentication on
-your GitHub account and to solve this issue you need to:
 
-1. [Create](https://github.com/settings/applications) an oauth token on GitHub.
+If you would prefer not to provide your GitHub credentials to Composer you can
+manually create a token using the following procedure:
+
+1. [Create](https://github.com/settings/applications) an OAuth token on GitHub.
 [Read more](https://github.com/blog/1509-personal-api-tokens) on this.
 
 2. Add it to the configuration running `composer config -g github-oauth.github.com <oauthtoken>`

+ 43 - 4
src/Composer/Util/GitHub.php

@@ -87,9 +87,13 @@ class GitHub
         $this->io->write('To revoke access to this token you can visit https://github.com/settings/applications');
         while ($attemptCounter++ < 5) {
             try {
-                $username = $this->io->ask('Username: ');
-                $password = $this->io->askAndHideAnswer('Password: ');
-                $this->io->setAuthentication($originUrl, $username, $password);
+                if (empty($otp) || !$this->io->hasAuthentication($originUrl)) {
+                    $username = $this->io->ask('Username: ');
+                    $password = $this->io->askAndHideAnswer('Password: ');
+                    $otp      = null;
+
+                    $this->io->setAuthentication($originUrl, $username, $password);
+                }
 
                 // build up OAuth app name
                 $appName = 'Composer';
@@ -97,11 +101,18 @@ class GitHub
                     $appName .= ' on ' . trim($output);
                 }
 
+                $headers = array('Content-Type: application/json');
+
+                if ($otp) {
+                    $headers[] = 'X-GitHub-OTP: ' . $otp;
+                }
+
                 $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array(
+                    'retry-auth-failure' => false,
                     'http' => array(
                         'method' => 'POST',
                         'follow_location' => false,
-                        'header' => "Content-Type: application/json\r\n",
+                        'header' => $headers,
                         'content' => json_encode(array(
                             'scopes' => array('repo'),
                             'note' => $appName,
@@ -111,6 +122,34 @@ class GitHub
                 )));
             } catch (TransportException $e) {
                 if (in_array($e->getCode(), array(403, 401))) {
+                    // 401 when authentication was supplied, handle 2FA if required.
+                    if ($this->io->hasAuthentication($originUrl)) {
+                        $headerNames = array_map(function($header) {
+                            return strtolower(strstr($header, ':', true));
+                        }, $e->getHeaders());
+
+                        if ($key = array_search('x-github-otp', $headerNames)) {
+                            $headers = $e->getHeaders();
+                            list($required, $method) = array_map('trim', explode(';', substr(strstr($headers[$key], ':'), 1)));
+
+                            if ('required' === $required) {
+                                $this->io->write('Two-factor Authentication');
+
+                                if ('app' === $method) {
+                                    $this->io->write('Open the two-factor authentication app on your device to view your authentication code and verify your identity.');
+                                }
+
+                                if ('sms' === $method) {
+                                    $this->io->write('You have been sent an SMS message with an authentication code to verify your identity.');
+                                }
+
+                                $otp = $this->io->ask('Authentication Code: ');
+
+                                continue;
+                            }
+                        }
+                    }
+
                     $this->io->write('Invalid credentials.');
                     continue;
                 }

+ 13 - 0
src/Composer/Util/RemoteFilesystem.php

@@ -33,6 +33,7 @@ class RemoteFilesystem
     private $progress;
     private $lastProgress;
     private $options;
+    private $retryAuthFailure;
 
     /**
      * Constructor.
@@ -109,12 +110,19 @@ class RemoteFilesystem
         $this->fileName = $fileName;
         $this->progress = $progress;
         $this->lastProgress = null;
+        $this->retryAuthFailure = true;
 
         // capture username/password from URL if there is one
         if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) {
             $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2]));
         }
 
+        if (isset($additionalOptions['retry-auth-failure'])) {
+            $this->retryAuthFailure = (bool) $additionalOptions['retry-auth-failure'];
+
+            unset($additionalOptions['retry-auth-failure']);
+        }
+
         $options = $this->getOptionsForUrl($originUrl, $additionalOptions);
 
         if ($this->io->isDebug()) {
@@ -260,6 +268,11 @@ class RemoteFilesystem
                         throw new TransportException($message, 401);
                     }
 
+                    // Bail if the caller is going to handle authentication failures itself.
+                    if (!$this->retryAuthFailure) {
+                        throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', 401);
+                    }
+
                     $this->promptAuthAndRetry();
                     break;
                 }