Browse Source

Merge pull request #8895 from TheFRedFox/feature/add-rsa_sha1-oauth-method

Adds RSA-SHA1 method for the OAuth Authentication (issue #8889)
Mark Story 9 years ago
parent
commit
6ef91850e9

+ 94 - 9
src/Network/Http/Auth/Oauth.php

@@ -15,6 +15,7 @@ namespace Cake\Network\Http\Auth;
 
 use Cake\Core\Exception\Exception;
 use Cake\Network\Http\Request;
+use Cake\Utility\Security;
 
 /**
  * Oauth 1 authentication strategy for Cake\Network\Http\Client
@@ -39,13 +40,7 @@ class Oauth
      */
     public function authentication(Request $request, array $credentials)
     {
-        $hasKeys = isset(
-            $credentials['consumerSecret'],
-            $credentials['consumerKey'],
-            $credentials['token'],
-            $credentials['tokenSecret']
-        );
-        if (!$hasKeys) {
+        if (!isset($credentials['consumerKey'])) {
             return;
         }
         if (empty($credentials['method'])) {
@@ -54,10 +49,33 @@ class Oauth
         $credentials['method'] = strtoupper($credentials['method']);
         switch ($credentials['method']) {
             case 'HMAC-SHA1':
+                $hasKeys = isset(
+                    $credentials['consumerSecret'],
+                    $credentials['token'],
+                    $credentials['tokenSecret']
+                );
+                if (!$hasKeys) {
+                    return;
+                }
                 $value = $this->_hmacSha1($request, $credentials);
                 break;
 
+            case 'RSA-SHA1':
+                if (!isset($credentials['privateKey'])) {
+                    return;
+                }
+                $value = $this->_rsaSha1($request, $credentials);
+                break;
+
             case 'PLAINTEXT':
+                $hasKeys = isset(
+                    $credentials['consumerSecret'],
+                    $credentials['token'],
+                    $credentials['tokenSecret']
+                );
+                if (!$hasKeys) {
+                    return;
+                }
                 $value = $this->_plaintext($request, $credentials);
                 break;
 
@@ -82,7 +100,7 @@ class Oauth
     {
         $values = [
             'oauth_version' => '1.0',
-            'oauth_nonce' => uniqid(),
+            'oauth_nonce' => Security::randomBytes(16),
             'oauth_timestamp' => time(),
             'oauth_signature_method' => 'PLAINTEXT',
             'oauth_token' => $credentials['token'],
@@ -109,7 +127,7 @@ class Oauth
      */
     protected function _hmacSha1($request, $credentials)
     {
-        $nonce = isset($credentials['nonce']) ? $credentials['nonce'] : uniqid();
+        $nonce = isset($credentials['nonce']) ? $credentials['nonce'] : Security::randomBytes(16);
         $timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time();
         $values = [
             'oauth_version' => '1.0',
@@ -135,6 +153,73 @@ class Oauth
     }
 
     /**
+     * Use RSA-SHA1 signing.
+     *
+     * This method is suitable for plain HTTP or HTTPS.
+     *
+     * @param \Cake\Network\Http\Request $request The request object.
+     * @param array $credentials Authentication credentials.
+     * @return string
+     *
+     * @throws \RuntimeException
+     */
+    protected function _rsaSha1($request, $credentials)
+    {
+        if (!function_exists('openssl_pkey_get_private')) {
+            throw new \RuntimeException('RSA-SHA1 signature method requires the OpenSSL extension.');
+        }
+
+        $nonce = isset($credentials['nonce']) ? $credentials['nonce'] : Security::randomBytes(16);
+        $timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time();
+        $values = [
+            'oauth_version' => '1.0',
+            'oauth_nonce' => $nonce,
+            'oauth_timestamp' => $timestamp,
+            'oauth_signature_method' => 'RSA-SHA1',
+            'oauth_consumer_key' => $credentials['consumerKey'],
+        ];
+        if (isset($credentials['consumerSecret'])) {
+            $values['oauth_consumer_secret'] = $credentials['consumerSecret'];
+        }
+        if (isset($credentials['token'])) {
+            $values['oauth_token'] = $credentials['token'];
+        }
+        if (isset($credentials['tokenSecret'])) {
+            $values['oauth_token_secret'] = $credentials['tokenSecret'];
+        }
+        $baseString = $this->baseString($request, $values);
+
+        if (isset($credentials['realm'])) {
+            $values['oauth_realm'] = $credentials['realm'];
+        }
+
+        if (is_resource($credentials['privateKey'])) {
+            $resource = $credentials['privateKey'];
+            $privateKey = stream_get_contents($resource);
+            rewind($resource);
+            $credentials['privateKey'] = $privateKey;
+        }
+
+        $credentials += [
+            'privateKeyPassphrase' => null,
+        ];
+        if (is_resource($credentials['privateKeyPassphrase'])) {
+            $resource = $credentials['privateKeyPassphrase'];
+            $passphrase = stream_get_line($resource, 0, PHP_EOL);
+            rewind($resource);
+            $credentials['privateKeyPassphrase'] = $passphrase;
+        }
+        $privateKey = openssl_pkey_get_private($credentials['privateKey'], $credentials['privateKeyPassphrase']);
+        $signature = '';
+        openssl_sign($baseString, $signature, $privateKey);
+        openssl_free_key($privateKey);
+
+        $values['oauth_signature'] = base64_encode($signature);
+
+        return $this->_buildAuth($values);
+    }
+
+    /**
      * Generate the Oauth basestring
      *
      * - Querystring, request data and oauth_* parameters are combined.

+ 268 - 0
tests/TestCase/Network/Http/Auth/OauthTest.php

@@ -23,6 +23,42 @@ use Cake\TestSuite\TestCase;
 class OauthTest extends TestCase
 {
 
+    private $privateKeyString = '-----BEGIN RSA PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V
+A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d
+7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ
+hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H
+X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm
+uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw
+rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z
+zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn
+qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG
+WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno
+cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+
+3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8
+AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54
+Lw03eHTNQghS0A==
+-----END RSA PRIVATE KEY-----';
+
+    private $privateKeyStringEnc = '-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-CBC,E65DB7AE7A05EF23
+
+QCXAQ/Uj1+7uQp0MyDUPlKvW/28PhbT4GxflBYmU6SxKZ2CVFPk0M8RgB6gkJyVv
+mwjo1Ch2Tlt7/VrNfLWGIh1XPhsC3gatv8Wv+g0keWWifaHlhXulgMGREJ7QeJg0
+5THvdFuIs2qQnOzPCAwONjM6yMxPb2qxvwq0UKAL5V/CYVFWS6PYdR25f9ogXxBz
+c3QjvvnhQ7ipNjpjVp/XKYMYnZPCYkNYvRX+BcsWlqYtclO3m+xPG+mPAFs9hnBI
+wHI4yC2fl52giRc7XnSl7NNjun6RpHT/Cn7JDH6ql86pgMO0dw6PDzPf0KY9DCrR
+ldQyzQ8WjN3FU55+En+8zmSnxUu7EbdqZwhVEF+UwfJ7IqJUnHll0aDTUA/qq0dk
+DqtMKIXvRnDVZJqKxHyRvARf8Zp8USsq3cVdlA9PhtcKrs4CbTDL0lJ3eWj1bDS1
+kIHXYo19lBqcS1oX+6TqvEs69oW/aG8UZIONN0Xh5TbxuJMedXD1dexV9oOA9lGR
+cS6Ye0wC7fCdnA6jfAmHFJ5t2qk7FOzcFZwap7m+EWn11z+72GVqz3BDSe5qH2m2
+XOHl59rVtJsZFtjyQEV34IFYyb2qBHHqUUdKwIwT1JOZIq+IdTJxaieIb1mnlmDw
+DDf4Kwr0C9tti1R1IsPaAmjF7eH0PGbDGAB3fJSCXbHf7EXTz1AUdknd2MHXQ7wO
+UBABkD2ETB+EotdHTly5FQt0jwbHfF2najBmezxtEjIygCnDb02Rtuei4HTansBu
+shqoyFXJvizZzje7HaTQv/eJTuA6rUOzu/sAv/eBx2YAPkA8oa3qUw==
+-----END RSA PRIVATE KEY-----';
+
     /**
      * @expectedException \Cake\Core\Exception\Exception
      */
@@ -213,4 +249,236 @@ class OauthTest extends TestCase
             urldecode($result)
         );
     }
+
+    /**
+     * Test RSA-SHA1 signing with a private key string
+     *
+     * Hash result + parameters taken from
+     * http://wiki.oauth.net/w/page/12238556/TestCases
+     *
+     * @return void
+     */
+    public function testRsaSigningString()
+    {
+        $request = new Request();
+        $request->url('http://photos.example.net/photos')
+            ->body([
+                       'file' => 'vacaction.jpg',
+                       'size' => 'original'
+                   ]);
+        $privateKey = $this->privateKeyString;
+
+        $options = [
+            'method' => 'RSA-SHA1',
+            'consumerKey' => 'dpf43f3p2l4k3l03',
+            'nonce' => '13917289812797014437',
+            'timestamp' => '1196666512',
+            'privateKey' => $privateKey
+        ];
+        $auth = new Oauth();
+        $auth->authentication($request, $options);
+
+        $result = $request->header('Authorization');
+        $expected = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE=';
+        $this->assertContains(
+            'oauth_signature="' . $expected . '"',
+            urldecode($result)
+        );
+    }
+
+    /**
+     * Test RSA-SHA1 signing with a private key file
+     *
+     * Hash result + parameters taken from
+     * http://wiki.oauth.net/w/page/12238556/TestCases
+     *
+     * @return void
+     */
+    public function testRsaSigningFile()
+    {
+        $request = new Request();
+        $request->url('http://photos.example.net/photos')
+            ->body([
+                       'file' => 'vacaction.jpg',
+                       'size' => 'original'
+                   ]);
+        $privateKey = fopen(TEST_APP . DS . 'config' . DS . 'key.pem', 'r');
+
+        $options = [
+            'method' => 'RSA-SHA1',
+            'consumerKey' => 'dpf43f3p2l4k3l03',
+            'nonce' => '13917289812797014437',
+            'timestamp' => '1196666512',
+            'privateKey' => $privateKey
+        ];
+        $auth = new Oauth();
+        $auth->authentication($request, $options);
+
+        $result = $request->header('Authorization');
+        $expected = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE=';
+        $this->assertContains(
+            'oauth_signature="' . $expected . '"',
+            urldecode($result)
+        );
+    }
+
+    /**
+     * Test RSA-SHA1 signing with a private key file passphrase string
+     *
+     * Hash result + parameters taken from
+     * http://wiki.oauth.net/w/page/12238556/TestCases
+     *
+     * @return void
+     */
+    public function testRsaSigningWithPassphraseString()
+    {
+        $request = new Request();
+        $request->url('http://photos.example.net/photos')
+            ->body([
+                       'file' => 'vacaction.jpg',
+                       'size' => 'original'
+                   ]);
+        $privateKey = fopen(TEST_APP . DS . 'config' . DS . 'key_with_passphrase.pem', 'r');
+        $passphrase = 'fancy-cakephp-passphrase';
+
+        $options = [
+            'method' => 'RSA-SHA1',
+            'consumerKey' => 'dpf43f3p2l4k3l03',
+            'nonce' => '13917289812797014437',
+            'timestamp' => '1196666512',
+            'privateKey' => $privateKey,
+            'privateKeyPassphrase' => $passphrase,
+        ];
+        $auth = new Oauth();
+        $auth->authentication($request, $options);
+
+        $result = $request->header('Authorization');
+        $expected = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE=';
+        $this->assertContains(
+            'oauth_signature="' . $expected . '"',
+            urldecode($result)
+        );
+    }
+
+    /**
+     * Test RSA-SHA1 signing with a private key string and passphrase string
+     *
+     * Hash result + parameters taken from
+     * http://wiki.oauth.net/w/page/12238556/TestCases
+     *
+     * @return void
+     */
+    public function testRsaSigningStringWithPassphraseString()
+    {
+        $request = new Request();
+        $request->url('http://photos.example.net/photos')
+            ->body([
+                       'file' => 'vacaction.jpg',
+                       'size' => 'original'
+                   ]);
+        $privateKey = $this->privateKeyStringEnc;
+        $passphrase = 'fancy-cakephp-passphrase';
+
+        $options = [
+            'method' => 'RSA-SHA1',
+            'consumerKey' => 'dpf43f3p2l4k3l03',
+            'nonce' => '13917289812797014437',
+            'timestamp' => '1196666512',
+            'privateKey' => $privateKey,
+            'privateKeyPassphrase' => $passphrase,
+        ];
+        $auth = new Oauth();
+        $auth->authentication($request, $options);
+
+        $result = $request->header('Authorization');
+        $expected = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE=';
+        $this->assertContains(
+            'oauth_signature="' . $expected . '"',
+            urldecode($result)
+        );
+    }
+
+    /**
+     * Test RSA-SHA1 signing with passphrase file
+     *
+     * Hash result + parameters taken from
+     * http://wiki.oauth.net/w/page/12238556/TestCases
+     *
+     * @return void
+     */
+    public function testRsaSigningWithPassphraseFile()
+    {
+        $this->skipIf(PHP_EOL != "\n", 'Just the line ending "\n" is supported. You can run the test again e.g. on a linux system.');
+
+        $request = new Request();
+        $request->url('http://photos.example.net/photos')
+            ->body([
+                       'file' => 'vacaction.jpg',
+                       'size' => 'original'
+                   ]);
+        $privateKey = fopen(TEST_APP . DS . 'config' . DS . 'key_with_passphrase.pem', 'r');
+        $passphrase = fopen(TEST_APP . DS . 'config' . DS . 'key_passphrase_lf', 'r');
+
+        $options = [
+            'method' => 'RSA-SHA1',
+            'consumerKey' => 'dpf43f3p2l4k3l03',
+            'nonce' => '13917289812797014437',
+            'timestamp' => '1196666512',
+            'privateKey' => $privateKey,
+            'privateKeyPassphrase' => $passphrase,
+        ];
+        $auth = new Oauth();
+        $auth->authentication($request, $options);
+
+        $result = $request->header('Authorization');
+        $expected = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE=';
+        $this->assertContains(
+            'oauth_signature="' . $expected . '"',
+            urldecode($result)
+        );
+        $expected = 0;
+        $this->assertEquals($expected, ftell($passphrase));
+    }
+
+    /**
+     * Test RSA-SHA1 signing with a private key string and passphrase file
+     *
+     * Hash result + parameters taken from
+     * http://wiki.oauth.net/w/page/12238556/TestCases
+     *
+     * @return void
+     */
+    public function testRsaSigningStringWithPassphraseFile()
+    {
+        $this->skipIf(PHP_EOL != "\n", 'Just the line ending "\n" is supported. You can run the test again e.g. on a linux system.');
+
+        $request = new Request();
+        $request->url('http://photos.example.net/photos')
+            ->body([
+                       'file' => 'vacaction.jpg',
+                       'size' => 'original'
+                   ]);
+        $privateKey = $this->privateKeyStringEnc;
+        $passphrase = fopen(TEST_APP . DS . 'config' . DS . 'key_passphrase_lf', 'r');
+
+        $options = [
+            'method' => 'RSA-SHA1',
+            'consumerKey' => 'dpf43f3p2l4k3l03',
+            'nonce' => '13917289812797014437',
+            'timestamp' => '1196666512',
+            'privateKey' => $privateKey,
+            'privateKeyPassphrase' => $passphrase,
+        ];
+        $auth = new Oauth();
+        $auth->authentication($request, $options);
+
+        $result = $request->header('Authorization');
+        $expected = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE=';
+        $this->assertContains(
+            'oauth_signature="' . $expected . '"',
+            urldecode($result)
+        );
+        $expected = 0;
+        $this->assertEquals($expected, ftell($passphrase));
+    }
 }

+ 16 - 0
tests/test_app/config/key.pem

@@ -0,0 +1,16 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V
+A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d
+7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ
+hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H
+X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm
+uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw
+rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z
+zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn
+qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG
+WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno
+cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+
+3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8
+AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54
+Lw03eHTNQghS0A==
+-----END RSA PRIVATE KEY-----

+ 1 - 0
tests/test_app/config/key_passphrase_lf

@@ -0,0 +1 @@
+fancy-cakephp-passphrase

+ 18 - 0
tests/test_app/config/key_with_passphrase.pem

@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-CBC,E65DB7AE7A05EF23
+
+QCXAQ/Uj1+7uQp0MyDUPlKvW/28PhbT4GxflBYmU6SxKZ2CVFPk0M8RgB6gkJyVv
+mwjo1Ch2Tlt7/VrNfLWGIh1XPhsC3gatv8Wv+g0keWWifaHlhXulgMGREJ7QeJg0
+5THvdFuIs2qQnOzPCAwONjM6yMxPb2qxvwq0UKAL5V/CYVFWS6PYdR25f9ogXxBz
+c3QjvvnhQ7ipNjpjVp/XKYMYnZPCYkNYvRX+BcsWlqYtclO3m+xPG+mPAFs9hnBI
+wHI4yC2fl52giRc7XnSl7NNjun6RpHT/Cn7JDH6ql86pgMO0dw6PDzPf0KY9DCrR
+ldQyzQ8WjN3FU55+En+8zmSnxUu7EbdqZwhVEF+UwfJ7IqJUnHll0aDTUA/qq0dk
+DqtMKIXvRnDVZJqKxHyRvARf8Zp8USsq3cVdlA9PhtcKrs4CbTDL0lJ3eWj1bDS1
+kIHXYo19lBqcS1oX+6TqvEs69oW/aG8UZIONN0Xh5TbxuJMedXD1dexV9oOA9lGR
+cS6Ye0wC7fCdnA6jfAmHFJ5t2qk7FOzcFZwap7m+EWn11z+72GVqz3BDSe5qH2m2
+XOHl59rVtJsZFtjyQEV34IFYyb2qBHHqUUdKwIwT1JOZIq+IdTJxaieIb1mnlmDw
+DDf4Kwr0C9tti1R1IsPaAmjF7eH0PGbDGAB3fJSCXbHf7EXTz1AUdknd2MHXQ7wO
+UBABkD2ETB+EotdHTly5FQt0jwbHfF2najBmezxtEjIygCnDb02Rtuei4HTansBu
+shqoyFXJvizZzje7HaTQv/eJTuA6rUOzu/sAv/eBx2YAPkA8oa3qUw==
+-----END RSA PRIVATE KEY-----