Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.71% covered (success)
85.71%
54 / 63
81.82% covered (success)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Auth
85.71% covered (success)
85.71%
54 / 63
81.82% covered (success)
81.82%
9 / 11
27.97
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 signRequest
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getAuthorizationHeader
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 tryGetValueInsensitive
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 tryGetValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 startsWith
50.00% covered (warning)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 formatHeaders
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 computeSignature
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 computeCanonicalizedHeaders
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 computeCanonicalizedResourceForTable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 computeCanonicalizedResource
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Storage\Adapter\Azure;
15
16use Pop\Http\Client\Request;
17
18/**
19 * Azure storage auth class
20 *
21 * This class is ported over from the discontinued Azure Storage PHP library at
22 * https://github.com/Azure/azure-storage-php (EOL: 3/17/2025)
23 *
24 * @category   Pop
25 * @package    Pop\Storage
26 * @author     Nick Sagona, III <dev@noladev.com>
27 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    2.1.0
30 */
31class Auth extends AbstractAuth
32{
33
34    /**
35     * The included headers
36     * @var array
37     */
38    protected array $includedHeaders = [
39        'content-encoding', 'content-language', 'content-length', 'content-md5', 'content-type', 'date',
40        'if-modified-since', 'if-match', 'if-none-match', 'if-unmodified-since', 'range',
41    ];
42
43    /**
44     * Constructor.
45     *
46     * @param string $accountName
47     * @param string $accountKey
48     */
49    public function __construct(string $accountName, string $accountKey)
50    {
51        $this->setAccountName($accountName);
52        $this->setAccountKey($accountKey);
53    }
54
55    /**
56     * Adds authentication header to the request headers.
57     *
58     * @param  Request $request
59     * @return Request
60     */
61    public function signRequest(Request $request): Request
62    {
63        $queryParams = ($request->hasQuery()) ? $request->getQuery()->toArray() : [];
64
65        $signedKey = $this->getAuthorizationHeader(
66            self::formatHeaders($request->getHeadersAsArray()), $request->getUriAsString(),
67            $queryParams, $request->getMethod()
68        );
69
70        return $request->addHeader('authorization', $signedKey);
71    }
72
73    /**
74     * Returns authorization header to be included in the request.
75     *
76     * @param  array  $headers
77     * @param  string $url
78     * @param  array  $queryParams
79     * @param  string $httpMethod
80     * @return string
81     */
82    public function getAuthorizationHeader(array $headers, string $url, array $queryParams, string $httpMethod): string
83    {
84        $signature = $this->computeSignature($headers, $url, $queryParams, $httpMethod);
85
86        return 'SharedKey ' . $this->accountName . ':' . base64_encode(
87            hash_hmac('sha256', $signature, base64_decode($this->accountKey), true)
88        );
89    }
90
91    /**
92     * Returns the specified value of the $key passed from $array and in case that
93     * this $key doesn't exist, the default value is returned. The key matching is
94     * done in a case-insensitive manner.
95     *
96     * @param  string $key
97     * @param  array  $haystack
98     * @param  mixed  $default
99     * @return mixed
100     */
101    public static function tryGetValueInsensitive(string $key, array $haystack, mixed $default = null): mixed
102    {
103        $array = array_change_key_case($haystack);
104        return self::tryGetValue($array, strtolower($key), $default);
105    }
106
107    /**
108     * Returns the specified value of the $key passed from $array and in case that
109     * this $key doesn't exist, the default value is returned.
110     *
111     * @param  array $array
112     * @param  mixed $key
113     * @param  mixed $default
114     * @return mixed
115     */
116    public static function tryGetValue(array $array, mixed $key, mixed $default = null): mixed
117    {
118        return (!empty($array) && array_key_exists($key, $array)) ? $array[$key] : $default;
119    }
120
121    /**
122     * Checks if the passed $string starts with $prefix
123     *
124     * @param  string $string
125     * @param  string $prefix
126     * @param  bool   $ignoreCase
127     * @return bool
128     */
129    public static function startsWith(string $string, string $prefix, bool $ignoreCase = false): bool
130    {
131        if ($ignoreCase) {
132            $string = strtolower($string);
133            $prefix = strtolower($prefix);
134        }
135        return (str_starts_with($string, $prefix));
136    }
137
138    /**
139     * Convert a http headers array into a uniformed format for further process
140     *
141     * @param  array $headers
142     * @return array
143     */
144    public static function formatHeaders(array $headers): array
145    {
146        $result = [];
147        foreach ($headers as $key => $value) {
148            $result[strtolower($key)] = (is_array($value) && count($value) == 1) ? $value[0] : $value;
149        }
150
151        return $result;
152    }
153
154
155    /**
156     * Computes the authorization signature for blob and queue shared key.
157     *
158     * @param  array  $headers
159     * @param  string $url
160     * @param  array  $queryParams
161     * @param  string $httpMethod
162     * @return string
163     */
164    protected function computeSignature(array $headers, string $url, array $queryParams, string $httpMethod): string
165    {
166        $canonicalizedHeaders  = $this->computeCanonicalizedHeaders($headers);
167        $canonicalizedResource = $this->computeCanonicalizedResource($url, $queryParams);
168
169        $stringToSign   = [];
170        $stringToSign[] = strtoupper($httpMethod);
171
172        foreach ($this->includedHeaders as $header) {
173            $stringToSign[] = self::tryGetValueInsensitive($header, $headers);
174        }
175
176        if (count($canonicalizedHeaders) > 0) {
177            $stringToSign[] = implode("\n", $canonicalizedHeaders);
178        }
179
180        $stringToSign[] = $canonicalizedResource;
181        $string = implode("\n", $stringToSign);
182
183        return $string;
184    }
185
186    /**
187     * Computes canonicalized headers for headers array.
188     *
189     * @param  array $headers
190     * @return array
191     */
192    protected function computeCanonicalizedHeaders(array $headers): array
193    {
194        $canonicalizedHeaders = [];
195        $normalizedHeaders    = [];
196        $validPrefix          = 'x-ms-';
197
198        foreach ($headers as $header => $value) {
199            // Convert header to lower case.
200            $header = strtolower($header);
201
202            // Retrieve all headers for the resource that begin with x-ms-,
203            // including the x-ms-date header.
204            if (self::startsWith($header, $validPrefix)) {
205                // Unfold the string by replacing any breaking white space
206                // (meaning what splits the headers, which is \r\n) with a single
207                // space.
208                $value = str_replace("\r\n", ' ', $value);
209
210                // Trim any white space around the colon in the header.
211                $value  = ltrim($value);
212                $header = rtrim($header);
213
214                $normalizedHeaders[$header] = $value;
215            }
216        }
217
218        // Sort the headers lexicographically by header name, in ascending order.
219        // Note that each header may appear only once in the string.
220        ksort($normalizedHeaders);
221
222        foreach ($normalizedHeaders as $key => $value) {
223            $canonicalizedHeaders[] = $key . ':' . $value;
224        }
225
226        return $canonicalizedHeaders;
227    }
228
229    /**
230     * Computes canonicalized resources from URL using Table formar
231     *
232     * @param  string $url
233     * @param  array  $queryParams
234     * @return string
235     */
236    protected function computeCanonicalizedResourceForTable(string $url, array $queryParams): string
237    {
238        $queryParams = array_change_key_case($queryParams);
239
240        // 1. Beginning with an empty string (""), append a forward slash (/),
241        //    followed by the name of the account that owns the accessed resource.
242        $canonicalizedResource = '/' . $this->accountName;
243
244        // 2. Append the resource's encoded URI path, without any query parameters.
245        $canonicalizedResource .= parse_url($url, PHP_URL_PATH);
246
247        // 3. The query string should include the question mark and the comp
248        //    parameter (for example, ?comp=metadata). No other parameters should
249        //    be included on the query string.
250        if (array_key_exists('comp', $queryParams)) {
251            $canonicalizedResource .= '?comp=';
252            $canonicalizedResource .= $queryParams['comp'];
253        }
254
255        return $canonicalizedResource;
256    }
257
258    /**
259     * Computes canonicalized resources from URL.
260     *
261     * @param  string $url
262     * @param  array  $queryParams
263     * @return string
264     */
265    protected function computeCanonicalizedResource(string $url, array $queryParams): string
266    {
267        $queryParams = array_change_key_case($queryParams);
268
269        // 1. Beginning with an empty string (""), append a forward slash (/),
270        //    followed by the name of the account that owns the accessed resource.
271        $canonicalizedResource = '/' . $this->accountName;
272
273        // 2. Append the resource's encoded URI path, without any query parameters.
274        $canonicalizedResource .= parse_url($url, PHP_URL_PATH);
275
276        // 3. Retrieve all query parameters on the resource URI, including the comp
277        //    parameter if it exists.
278        // 4. Sort the query parameters lexicographically by parameter name, in
279        //    ascending order.
280        if (count($queryParams) > 0) {
281            ksort($queryParams);
282        }
283
284        // 5. Convert all parameter names to lowercase.
285        // 6. URL-decode each query parameter name and value.
286        // 7. Append each query parameter name and value to the string in the
287        //    following format:
288        //      parameter-name:parameter-value
289        // 9. Group query parameters
290        // 10. Append a new line character (\n) after each name-value pair.
291        foreach ($queryParams as $key => $value) {
292            // $value must already be ordered lexicographically
293            // See: ServiceRestProxy::groupQueryValues
294            $canonicalizedResource .= "\n" . $key . ':' . $value;
295        }
296
297        return $canonicalizedResource;
298    }
299
300}