From 23d35204cd28374701b79dc4190b4661b3f81c5f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:06:29 +0000 Subject: [PATCH 01/15] Bail out of the normal 401 handling routine when the origin is GitHub --- src/Composer/Util/RemoteFilesystem.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d3ecec03d..53829d03a 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -247,6 +247,11 @@ class RemoteFilesystem throw new TransportException($message, 401); } + // GitHub requests bail out early to allow 2FA to be applied if requested. + if ('github.com' === $this->originUrl) { + throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', 401); + } + $this->promptAuthAndRetry(); break; } From 3f53acc9af3a998dbddbdf59c9cc52053c9ae29d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:06:58 +0000 Subject: [PATCH 02/15] Test if the 401 was caused by 2FA and ask for OTP if appropriate --- src/Composer/Util/RemoteFilesystem.php | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 53829d03a..158ff8034 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -145,6 +145,42 @@ class RemoteFilesystem if ($e instanceof TransportException && !empty($http_response_header[0])) { $e->setHeaders($http_response_header); } + + // 401 when authentication was supplied, handle 2FA if required. + if ($e instanceof TransportException && 401 === $e->getCode() && $this->io->hasAuthentication($this->originUrl)) { + $headerNames = array_map(function($header) { + return strstr($header, ':', true); + }, $e->getHeaders()); + + if ($key = array_search('X-GitHub-OTP', $headerNames)) { + $headers = $e->getHeaders(); + list($required, $method) = explode(';', trim(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) { + // @todo + } + + $this->options['github-otp'] = trim($this->io->ask('Authentication Code: ')); + + $this->retry = true; + } + } else { + try { + $this->promptAuthAndRetry(); + } catch (TransportException $e) { + if ($e instanceof TransportException && !empty($http_response_header[0])) { + $e->setHeaders($http_response_header); + } + } + } + } } if ($errorMessage && !ini_get('allow_url_fopen')) { $errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')'; From 360df90ba59a29c1ad03c75370a46327441a5664 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:07:26 +0000 Subject: [PATCH 03/15] Add GitHub OTP to request headers --- src/Composer/Util/RemoteFilesystem.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 158ff8034..26b2774c9 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -378,6 +378,13 @@ class RemoteFilesystem } } + // Handle GitHub two factor tokens. + if (isset($options['github-otp'])) { + $headers[] = 'X-GitHub-OTP: ' . $options['github-otp']; + + unset($options['github-otp']); + } + if (isset($options['http']['header']) && !is_array($options['http']['header'])) { $options['http']['header'] = explode("\r\n", trim($options['http']['header'], "\r\n")); } From 9a0f4392da69f05b09292c2f18f86f5ea86d1e29 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:11:45 +0000 Subject: [PATCH 04/15] Trim whitepsace from each argument --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 26b2774c9..890bd5aad 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -154,7 +154,7 @@ class RemoteFilesystem if ($key = array_search('X-GitHub-OTP', $headerNames)) { $headers = $e->getHeaders(); - list($required, $method) = explode(';', trim(substr(strstr($headers[$key], ':'), 1))); + list($required, $method) = array_map('trim', explode(';', substr(strstr($headers[$key], ':'), 1))); if ('required' === $required) { $this->io->write('Two-factor Authentication'); From 20dac3e836a02b57794559bb0501c356cd140459 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:31:56 +0000 Subject: [PATCH 05/15] Remove GitHub OTP code from RFS class --- src/Composer/Util/RemoteFilesystem.php | 48 -------------------------- 1 file changed, 48 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 890bd5aad..d3ecec03d 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -145,42 +145,6 @@ class RemoteFilesystem if ($e instanceof TransportException && !empty($http_response_header[0])) { $e->setHeaders($http_response_header); } - - // 401 when authentication was supplied, handle 2FA if required. - if ($e instanceof TransportException && 401 === $e->getCode() && $this->io->hasAuthentication($this->originUrl)) { - $headerNames = array_map(function($header) { - return 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) { - // @todo - } - - $this->options['github-otp'] = trim($this->io->ask('Authentication Code: ')); - - $this->retry = true; - } - } else { - try { - $this->promptAuthAndRetry(); - } catch (TransportException $e) { - if ($e instanceof TransportException && !empty($http_response_header[0])) { - $e->setHeaders($http_response_header); - } - } - } - } } if ($errorMessage && !ini_get('allow_url_fopen')) { $errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')'; @@ -283,11 +247,6 @@ class RemoteFilesystem throw new TransportException($message, 401); } - // GitHub requests bail out early to allow 2FA to be applied if requested. - if ('github.com' === $this->originUrl) { - throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', 401); - } - $this->promptAuthAndRetry(); break; } @@ -378,13 +337,6 @@ class RemoteFilesystem } } - // Handle GitHub two factor tokens. - if (isset($options['github-otp'])) { - $headers[] = 'X-GitHub-OTP: ' . $options['github-otp']; - - unset($options['github-otp']); - } - if (isset($options['http']['header']) && !is_array($options['http']['header'])) { $options['http']['header'] = explode("\r\n", trim($options['http']['header'], "\r\n")); } From 3f6a62099dd01e728b3215f968645d0c8d2237e7 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:32:55 +0000 Subject: [PATCH 06/15] Add an option which causes reauth attempts to be bypassed --- src/Composer/Util/RemoteFilesystem.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d3ecec03d..67b429837 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/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()) { @@ -247,6 +255,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; } From be5e4b1589f74edc7414f1c8ed63b96ef9205204 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:37:07 +0000 Subject: [PATCH 07/15] Intercept auth rejections requiring an OTP token --- src/Composer/Util/GitHub.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 49e56f8c9..affae8c64 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -111,6 +111,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 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) { + // @todo + } + + $otp = $this->io->ask('Authentication Code: '); + + continue; + } + } + } + $this->io->write('Invalid credentials.'); continue; } From 7e0d8c1bc5e098209cb7ee4f236c9962b897e3bb Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:38:14 +0000 Subject: [PATCH 08/15] Do not ask for credentials again if OTP token is present --- src/Composer/Util/GitHub.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index affae8c64..e3185745d 100644 --- a/src/Composer/Util/GitHub.php +++ b/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'; From cedae88b67e5a3940063536600d9bda20cf38f8c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:38:43 +0000 Subject: [PATCH 09/15] Add OTP token to the request headers --- src/Composer/Util/GitHub.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index e3185745d..41fe63740 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -101,11 +101,17 @@ 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( '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, From 2a08f55079d85529277840e6d99d318319cf11e6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:39:05 +0000 Subject: [PATCH 10/15] Bypass RFS auth failure handling --- src/Composer/Util/GitHub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 41fe63740..6794dd507 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -108,6 +108,7 @@ class GitHub } $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( + 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', 'follow_location' => false, From bcee7a04ee54e2d9b232a702c379eb0389255656 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:26:43 +0000 Subject: [PATCH 11/15] Add message when SMS authentication code is required --- src/Composer/Util/GitHub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 6794dd507..40060395f 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -140,7 +140,7 @@ class GitHub } if ('sms' === $method) { - // @todo + $this->io->write('You have been sent an SMS message with an authentication code to verify your identity.'); } $otp = $this->io->ask('Authentication Code: '); From f1af43068ce6e04f061d0593aaea8608d1432ae6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:33:13 +0000 Subject: [PATCH 12/15] Change docs to reflect support for GitHub 2FA --- doc/04-schema.md | 2 +- doc/articles/troubleshooting.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 96641e8fe..635136e2b 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -684,7 +684,7 @@ 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) + [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. diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index 2fc7bb487..a0b4565b6 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -105,12 +105,13 @@ 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: + +If you would rather than 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. From 0858e96ac685dcf6be98cb544cc36bac5e3d9741 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:33:47 +0000 Subject: [PATCH 13/15] Correct capitalisation of OAuth --- doc/04-schema.md | 2 +- doc/articles/troubleshooting.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 635136e2b..c48955ee6 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -685,7 +685,7 @@ The following options are supported: 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-oauth-tokens) - on how to get an oauth token for GitHub. + 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 diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index a0b4565b6..aa11715fc 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -105,7 +105,7 @@ 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 oauth tokens +## 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. @@ -113,7 +113,7 @@ for authentication asking your username and password so it can go ahead with its If you would rather than 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. +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 ` From 78568b49d6cce8d282fd8d199650c0afde565c06 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:36:00 +0000 Subject: [PATCH 14/15] Correct use of English --- doc/articles/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index aa11715fc..d3637cd03 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -110,7 +110,7 @@ Or, you can increase the limit with a command-line argument: 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. -If you would rather than provide your GitHub credentials to Composer you can +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. From 8b7cdb7fb417f349eab04335d0a7bc3956343a81 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:42:49 +0000 Subject: [PATCH 15/15] Treat HTTP header as case insensitive --- src/Composer/Util/GitHub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 40060395f..942e7749c 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -125,10 +125,10 @@ class GitHub // 401 when authentication was supplied, handle 2FA if required. if ($this->io->hasAuthentication($originUrl)) { $headerNames = array_map(function($header) { - return strstr($header, ':', true); + return strtolower(strstr($header, ':', true)); }, $e->getHeaders()); - if ($key = array_search('X-GitHub-OTP', $headerNames)) { + if ($key = array_search('x-github-otp', $headerNames)) { $headers = $e->getHeaders(); list($required, $method) = array_map('trim', explode(';', substr(strstr($headers[$key], ':'), 1)));