Browse Source

16876 - Improve Digest for HTTP Client

Alejandro Ibarra 3 years ago
parent
commit
97682942ea
2 changed files with 361 additions and 21 deletions
  1. 121 15
      src/Http/Client/Auth/Digest.php
  2. 240 6
      tests/TestCase/Http/Client/Auth/DigestTest.php

+ 121 - 15
src/Http/Client/Auth/Digest.php

@@ -17,6 +17,7 @@ namespace Cake\Http\Client\Auth;
 
 use Cake\Http\Client;
 use Cake\Http\Client\Request;
+use Cake\Utility\Hash;
 
 /**
  * Digest authentication adapter for Cake\Http\Client
@@ -27,6 +28,33 @@ use Cake\Http\Client\Request;
 class Digest
 {
     /**
+     * Algorithms
+     */
+    public const ALGO_MD5 = 'MD5';
+    public const ALGO_SHA_256 = 'SHA-256';
+    public const ALGO_SHA_512_256 = 'SHA-512-256';
+    public const ALGO_MD5_SESS = 'MD5-sess';
+    public const ALGO_SHA_256_SESS = 'SHA-256-sess';
+    public const ALGO_SHA_512_256_SESS = 'SHA-512-256-sess';
+
+    /**
+     * QOP
+     */
+    public const QOP_AUTH = 'auth';
+    public const QOP_AUTH_INT = 'auth-int';
+
+    /**
+     * Algorithms <-> Hash type
+     */
+    public const HASH_ALGORITHMS = [
+        self::ALGO_MD5 => 'md5',
+        self::ALGO_SHA_256 => 'sha256',
+        self::ALGO_SHA_512_256 => 'sha512/256',
+        self::ALGO_MD5_SESS => 'md5',
+        self::ALGO_SHA_256_SESS => 'sha256',
+        self::ALGO_SHA_512_256_SESS => 'sha512/256',
+    ];
+    /**
      * Instance of Cake\Http\Client
      *
      * @var \Cake\Http\Client
@@ -34,6 +62,27 @@ class Digest
     protected $_client;
 
     /**
+     * Algorithm
+     *
+     * @var string
+     */
+    protected $algorithm;
+
+    /**
+     * Hash type
+     *
+     * @var string
+     */
+    protected $hashType;
+
+    /**
+     * Is Sess algorithm
+     *
+     * @var bool
+     */
+    protected $isSessAlgorithm;
+
+    /**
      * Constructor
      *
      * @param \Cake\Http\Client $client Http client object.
@@ -45,6 +94,24 @@ class Digest
     }
 
     /**
+     * Set algorithm based on credentials
+     *
+     * @param array $credentials authentication params
+     * @return void
+     */
+    protected function setAlgorithm(array $credentials): void
+    {
+        $algorithm = $credentials['algorithm'] ?? self::ALGO_MD5;
+        if (!isset(self::HASH_ALGORITHMS[$algorithm])) {
+            throw new \InvalidArgumentException('Invalid Algorithm. Valid ones are: ' .
+                implode(',', array_keys(self::HASH_ALGORITHMS)));
+        }
+        $this->algorithm = $algorithm;
+        $this->isSessAlgorithm = strpos($this->algorithm, '-sess') !== false;
+        $this->hashType = Hash::get(self::HASH_ALGORITHMS, $this->algorithm);
+    }
+
+    /**
      * Add Authorization header to the request.
      *
      * @param \Cake\Http\Client\Request $request The request object.
@@ -63,6 +130,8 @@ class Digest
         if (!isset($credentials['realm'])) {
             return $request;
         }
+
+        $this->setAlgorithm($credentials);
         $value = $this->_generateHeader($request, $credentials);
 
         return $request->withHeader('Authorization', $value);
@@ -96,10 +165,12 @@ class Digest
             $matches,
             PREG_SET_ORDER
         );
+
         foreach ($matches as $match) {
-            $credentials[$match[1]] = $match[2];
+            $credentials[$match[1]] = $match[3] ?? $match[2];
         }
-        if (!empty($credentials['qop']) && empty($credentials['nc'])) {
+
+        if (($this->isSessAlgorithm || !empty($credentials['qop'])) && empty($credentials['nc'])) {
             $credentials['nc'] = 1;
         }
 
@@ -107,6 +178,14 @@ class Digest
     }
 
     /**
+     * @return string
+     */
+    protected function generateCnonce(): string
+    {
+        return uniqid();
+    }
+
+    /**
      * Generate the header Authorization
      *
      * @param \Cake\Http\Client\Request $request The request object.
@@ -115,18 +194,39 @@ class Digest
      */
     protected function _generateHeader(Request $request, array $credentials): string
     {
-        $path = $request->getUri()->getPath();
-        $a1 = md5($credentials['username'] . ':' . $credentials['realm'] . ':' . $credentials['password']);
-        $a2 = md5($request->getMethod() . ':' . $path);
-        $nc = '';
+        $path = $request->getRequestTarget();
+
+        if ($this->isSessAlgorithm) {
+            $credentials['cnonce'] = $this->generateCnonce();
+            $a1 = hash($this->hashType, $credentials['username'] . ':' .
+                    $credentials['realm'] . ':' . $credentials['password']) . ':' .
+                $credentials['nonce'] . ':' . $credentials['cnonce'];
+        } else {
+            $a1 = $credentials['username'] . ':' . $credentials['realm'] . ':' . $credentials['password'];
+        }
+        $ha1 = hash($this->hashType, $a1);
+        $a2 = $request->getMethod() . ':' . $path;
+        $nc = sprintf('%08x', $credentials['nc'] ?? 1);
 
         if (empty($credentials['qop'])) {
-            $response = md5($a1 . ':' . $credentials['nonce'] . ':' . $a2);
+            $ha2 = hash($this->hashType, $a2);
+            $response = hash($this->hashType, $ha1 . ':' . $credentials['nonce'] . ':' . $ha2);
         } else {
-            $credentials['cnonce'] = uniqid();
-            $nc = sprintf('%08x', $credentials['nc']++);
-            $response = md5(
-                $a1 . ':' . $credentials['nonce'] . ':' . $nc . ':' . $credentials['cnonce'] . ':auth:' . $a2
+            if (!in_array($credentials['qop'], [self::QOP_AUTH, self::QOP_AUTH_INT])) {
+                throw new \InvalidArgumentException('Invalid QOP parameter. Valid types are: ' .
+                    implode(',', [self::QOP_AUTH, self::QOP_AUTH_INT]));
+            }
+            if ($credentials['qop'] === self::QOP_AUTH_INT) {
+                $a2 = $request->getMethod() . ':' . $path . ':' . hash($this->hashType, (string)$request->getBody());
+            }
+            if (empty($credentials['cnonce'])) {
+                $credentials['cnonce'] = $this->generateCnonce();
+            }
+            $ha2 = hash($this->hashType, $a2);
+            $response = hash(
+                $this->hashType,
+                $ha1 . ':' . $credentials['nonce'] . ':' . $nc . ':' .
+                $credentials['cnonce'] . ':' . $credentials['qop'] . ':' . $ha2
             );
         }
 
@@ -135,13 +235,19 @@ class Digest
         $authHeader .= 'realm="' . $credentials['realm'] . '", ';
         $authHeader .= 'nonce="' . $credentials['nonce'] . '", ';
         $authHeader .= 'uri="' . $path . '", ';
-        $authHeader .= 'response="' . $response . '"';
+        $authHeader .= 'algorithm="' . $this->algorithm . '"';
+
+        if (!empty($credentials['qop'])) {
+            $authHeader .= ', qop=' . $credentials['qop'];
+        }
+        if ($this->isSessAlgorithm || !empty($credentials['qop'])) {
+            $authHeader .= ', nc=' . $nc . ', cnonce="' . $credentials['cnonce'] . '"';
+        }
+        $authHeader .= ', response="' . $response . '"';
+
         if (!empty($credentials['opaque'])) {
             $authHeader .= ', opaque="' . $credentials['opaque'] . '"';
         }
-        if (!empty($credentials['qop'])) {
-            $authHeader .= ', qop="auth", nc=' . $nc . ', cnonce="' . $credentials['cnonce'] . '"';
-        }
 
         return $authHeader;
     }

+ 240 - 6
tests/TestCase/Http/Client/Auth/DigestTest.php

@@ -32,7 +32,7 @@ class DigestTest extends TestCase
     protected $client;
 
     /**
-     * @var \Cake\Http\Client\Auth\Digest
+     * @var \PHPUnit\Framework\MockObject\MockObject|\Cake\Http\Client\Auth\Digest
      */
     protected $auth;
 
@@ -44,7 +44,23 @@ class DigestTest extends TestCase
         parent::setUp();
 
         $this->client = $this->getClientMock();
-        $this->auth = new Digest($this->client);
+        $this->auth = $this->getDigestMock();
+    }
+
+    /**
+     * @return Digest|\PHPUnit\Framework\MockObject\MockObject
+     */
+    protected function getDigestMock()
+    {
+        $digest = $this->getMockBuilder(Digest::class)
+            ->onlyMethods(['generateCnonce'])
+            ->setConstructorArgs([$this->client])
+            ->getMock();
+        $digest->expects($this->any())
+            ->method('generateCnonce')
+            ->willReturn('cnonce');
+
+        return $digest;
     }
 
     /**
@@ -86,9 +102,9 @@ class DigestTest extends TestCase
     }
 
     /**
-     * testQop method
+     * testQopAuth method
      */
-    public function testQop(): void
+    public function testQopAuth(): void
     {
         $headers = [
             'WWW-Authenticate: Digest realm="The batcave",nonce="4cded326c6c51",qop="auth"',
@@ -98,18 +114,61 @@ class DigestTest extends TestCase
         $this->client->expects($this->once())
             ->method('send')
             ->will($this->returnValue($response));
-
         $auth = ['username' => 'admin', 'password' => '1234'];
         $request = new Request('http://example.com/some/path', Request::METHOD_GET);
         $request = $this->auth->authentication($request, $auth);
         $result = $request->getHeaderLine('Authorization');
 
-        $this->assertStringContainsString('qop="auth"', $result);
+        $this->assertStringContainsString('qop=auth', $result);
         $this->assertStringContainsString('nc=00000001', $result);
         $this->assertMatchesRegularExpression('/cnonce="[a-z0-9]+"/', $result);
     }
 
     /**
+     * testQopAuthInt method
+     */
+    public function testQopAuthInt(): void
+    {
+        $headers = [
+            'WWW-Authenticate: Digest realm="The batcave",nonce="4cded326c6c51",qop="auth-int"',
+        ];
+
+        $response = new Response($headers, '');
+        $this->client->expects($this->once())
+            ->method('send')
+            ->will($this->returnValue($response));
+
+        $auth = ['username' => 'admin', 'password' => '1234'];
+        $request = new Request('http://example.com/some/path', Request::METHOD_GET);
+        $request = $this->auth->authentication($request, $auth);
+        $result = $request->getHeaderLine('Authorization');
+        $this->assertStringContainsString('qop=auth-int', $result);
+        $this->assertStringContainsString('nc=00000001', $result);
+        $this->assertMatchesRegularExpression('/cnonce="[a-z0-9]+"/', $result);
+    }
+
+    /**
+     * testQopAuthInt method
+     */
+    public function testQopFailure(): void
+    {
+        $headers = [
+            'WWW-Authenticate: Digest realm="The batcave",nonce="4cded326c6c51",qop="wrong"',
+        ];
+
+        $response = new Response($headers, '');
+        $this->client->expects($this->once())
+            ->method('send')
+            ->will($this->returnValue($response));
+
+        $auth = ['username' => 'admin', 'password' => '1234'];
+        $request = new Request('http://example.com/some/path', Request::METHOD_GET);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Invalid QOP parameter. Valid types are: auth,auth-int');
+        $this->auth->authentication($request, $auth);
+    }
+
+    /**
      * testOpaque method
      */
     public function testOpaque(): void
@@ -130,4 +189,179 @@ class DigestTest extends TestCase
 
         $this->assertStringContainsString('opaque="d8ea7aa61a1693024c4cc3a516f49b3c"', $result);
     }
+
+    /**
+     * Data provider for testAlgorithms
+     *
+     * @return array[]
+     */
+    public function algorithmsProvider(): array
+    {
+        return [
+            [
+                'ALGORITHM: MD5 QOP: none',
+                ['WWW-Authenticate: Digest algorithm="MD5", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="MD5", response="a21a874c0b29165929f5d24d1aad2c47", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: MD5-sess QOP: none',
+                ['WWW-Authenticate: Digest algorithm="MD5-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="MD5-sess", nc=00000001, cnonce="cnonce", response="6807a3326271bd172439d17c2d03d295", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-256 QOP: none',
+                ['WWW-Authenticate: Digest algorithm="SHA-256", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-256", response="65d00137c82412c7421ec9c8c08dccbbac667a1dedbae7db9cd888980e7af112", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-256-sess QOP: none',
+                ['WWW-Authenticate: Digest algorithm="SHA-256-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-256-sess", nc=00000001, cnonce="cnonce", response="a954ffbe615b56aa16e9a7f62ea34f4a4833bb75b670f73863e2209862d0fedf", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-512-256 QOP: none',
+                ['WWW-Authenticate: Digest algorithm="SHA-512-256", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-512-256", response="112b7ab122e7be8b9b5e7f32b8e4d9d4f651a53a783f1a1f267434b51f54e3cd", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-512-256-sess QOP: none',
+                ['WWW-Authenticate: Digest algorithm="SHA-512-256-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-512-256-sess", nc=00000001, cnonce="cnonce", response="d0a3b5b3d10b585911a9f5fd4ec4bbe691124e5920d371e699203906ba65376f", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: MD5 QOP: auth',
+                ['WWW-Authenticate: Digest algorithm="MD5", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="MD5", qop=auth, nc=00000001, cnonce="cnonce", response="716e45bf26c8abfa957d6799a34cc60f", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: MD5-sess QOP: auth',
+                ['WWW-Authenticate: Digest algorithm="MD5-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="MD5-sess", qop=auth, nc=00000001, cnonce="cnonce", response="1dfe066896bfab45282f088a390abe35", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-256 QOP: auth',
+                ['WWW-Authenticate: Digest algorithm="SHA-256", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-256", qop=auth, nc=00000001, cnonce="cnonce", response="f2bf2df206fd8b244d20540a5b294e5af7c7839615230acce240e3954bae781a", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-256-sess QOP: auth',
+                ['WWW-Authenticate: Digest algorithm="SHA-256-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-256-sess", qop=auth, nc=00000001, cnonce="cnonce", response="9e912a2d25b9ed4d3f66bdc5f011d8be04d8971a18993adc845a0f4e5c486546", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-512-256 QOP: auth',
+                ['WWW-Authenticate: Digest algorithm="SHA-512-256", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-512-256", qop=auth, nc=00000001, cnonce="cnonce", response="7569d573a117016388393fd682cbeb49a0a1af62366511a4e0d29b753ccf5e83", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-512-256-sess QOP: auth',
+                ['WWW-Authenticate: Digest algorithm="SHA-512-256-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth"'],
+                Request::METHOD_GET,
+                [],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-512-256-sess", qop=auth, nc=00000001, cnonce="cnonce", response="e100bc5a33a1c24943d5876bc2cf37cc45e1cde06069ed8b33a75fe032351e01", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: MD5 QOP: auth-int',
+                ['WWW-Authenticate: Digest algorithm="MD5", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth-int"'],
+                Request::METHOD_POST,
+                ['test' => 'test'],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="MD5", qop=auth-int, nc=00000001, cnonce="cnonce", response="476738bf56cf2f24173902adfa55d236", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: MD5-sess QOP: auth-int',
+                ['WWW-Authenticate: Digest algorithm="MD5-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth-int"'],
+                Request::METHOD_POST,
+                ['test' => 'test'],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="MD5-sess", qop=auth-int, nc=00000001, cnonce="cnonce", response="beee6427899606fb6b3e09bb71b57c79", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-256 QOP: auth-int',
+                ['WWW-Authenticate: Digest algorithm="SHA-256", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth-int"'],
+                Request::METHOD_POST,
+                ['test' => 'test'],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-256", qop=auth-int, nc=00000001, cnonce="cnonce", response="ca8f61f4d637343befeeb6282dc0302ebfc20ff974ff92b11c7e88836422f230", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-256-sess QOP: auth-int',
+                ['WWW-Authenticate: Digest algorithm="SHA-256-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth-int"'],
+                Request::METHOD_POST,
+                ['test' => 'test'],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-256-sess", qop=auth-int, nc=00000001, cnonce="cnonce", response="a2340619cd74256bc058bf2ea0c9fd62a27f9bf62fc295b8b3e94eab441a73d1", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-512-256 QOP: auth-int',
+                ['WWW-Authenticate: Digest algorithm="SHA-512-256", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth-int"'],
+                Request::METHOD_POST,
+                ['test' => 'test'],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-512-256", qop=auth-int, nc=00000001, cnonce="cnonce", response="055498e5d59601bea5c735a084fa74a0d33ebde3b5788057ce8380f892e99cec", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+            [
+                'ALGORITHM: SHA-512-256-sess QOP: auth-int',
+                ['WWW-Authenticate: Digest algorithm="SHA-512-256-sess", realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c",qop="auth-int"'],
+                Request::METHOD_POST,
+                ['test' => 'test'],
+                'Digest username="admin", realm="The batcave", nonce="4cded326c6c51", uri="/some/path", algorithm="SHA-512-256-sess", qop=auth-int, nc=00000001, cnonce="cnonce", response="9dbc89190bfe55eec14b1d444e1922d016d20dad461a1e9d25121c0db0024d3d", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"',
+            ],
+        ];
+    }
+
+    /**
+     * testAlgorithms method
+     *
+     * @dataProvider algorithmsProvider
+     * @return void
+     */
+    public function testAlgorithms($message, $headers, $method, $data, $expected)
+    {
+        $response = new Response($headers, '');
+        $this->client->expects($this->once())
+            ->method('send')
+            ->will($this->returnValue($response));
+        $auth = ['username' => 'admin', 'password' => '1234'];
+        $request = new Request('http://example.com/some/path', $method, [], $data);
+        $request = $this->auth->authentication($request, $auth);
+        $result = $request->getHeaderLine('Authorization');
+
+        $this->assertSame($expected, $result, $message);
+    }
+
+    public function testAlgorithmException()
+    {
+        $headers = [
+            'WWW-Authenticate: Digest algorithm="WRONG",realm="The batcave",nonce="4cded326c6c51"',
+        ];
+
+        $response = new Response($headers, '');
+        $this->client->expects($this->once())
+            ->method('send')
+            ->will($this->returnValue($response));
+
+        $auth = ['username' => 'admin', 'password' => '1234'];
+        $request = new Request('http://example.com/some/path', Request::METHOD_GET);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Invalid Algorithm. Valid ones are: MD5,SHA-256,SHA-512-256,MD5-sess,SHA-256-sess,SHA-512-256-sess');
+        $this->auth->authentication($request, $auth);
+    }
 }