Issues (9)

src/AuthorizationHeaderBuilder.php (2 issues)

1
<?php
2
3
namespace Acquia\Hmac;
4
5
use Acquia\Hmac\Digest\DigestInterface;
6
use Acquia\Hmac\Digest\Digest;
7
use Acquia\Hmac\Exception\MalformedRequestException;
8
use Psr\Http\Message\RequestInterface;
9
10
/**
11
 * Constructs AuthorizationHeader objects.
12
 */
13
class AuthorizationHeaderBuilder
14
{
15
    /**
16
     * @var \Psr\Http\Message\RequestInterface
17
     *   The request for which to generate the authorization header.
18
     */
19
    protected $request;
20
21
    /**
22
     * @var \Acquia\Hmac\KeyInterface
23
     *   The key with which to sign the authorization header.
24
     */
25
    protected $key;
26
27
    /**
28
     * @var \Acquia\Hmac\Digest\DigestInterface
29
     *  The message digest used to generate the header signature.
30
     */
31
    protected $digest;
32
33
    /**
34
     * @var string
35
     *   The realm/provider.
36
     */
37
    protected $realm = 'Acquia';
38
39
    /**
40
     * @var string
41
     *   The API key's unique identifier.
42
     */
43
    protected $id;
44
45
    /**
46
     * @var string
47
     *   The nonce.
48
     */
49
    protected $nonce;
50
51
    /**
52
     * @var string
53
     *   The spec version.
54
     */
55
    protected $version = '2.0';
56
57
    /**
58
     * @var string[]
59
     *   The list of custom headers.
60
     */
61
    protected $headers = [];
62
63
    /**
64
     * @var string
65
     *   The authorization signature.
66
     */
67
    protected $signature;
68
69
    /**
70
     * Initializes the builder with a message digest.
71
     *
72
     * @param \Psr\Http\Message\RequestInterface $request
73
     *   The request for which to generate the authorization header.
74
     * @param \Acquia\Hmac\KeyInterface $key
75
     *   The key with which to sign the authorization header.
76
     * @param \Acquia\Hmac\Digest\DigestInterface $digest
77
     *   The message digest to use when signing requests. Defaults to
78
     *   \Acquia\Hmac\Digest\Digest.
79
     */
80 20
    public function __construct(RequestInterface $request, KeyInterface $key, DigestInterface $digest = null)
81
    {
82 20
        $this->request = $request;
83 20
        $this->key     = $key;
84 20
        $this->digest  = $digest ?: new Digest();
85 20
        $this->nonce   = $this->generateNonce();
86 20
    }
87
88
    /**
89
     * Set the realm/provider.
90
     *
91
     * This method is optional: if not called, the realm will be "Acquia".
92
     *
93
     * @param string $realm
94
     *   The realm/provider.
95
     */
96 15
    public function setRealm($realm)
97
    {
98 15
        $this->realm = $realm;
99 15
    }
100
101
    /**
102
     * Set the API key's unique identifier.
103
     *
104
     * This method is required for an authorization header to be built.
105
     *
106
     * @param string $id
107
     *   The API key's unique identifier.
108
     */
109 19
    public function setId($id)
110
    {
111 19
        $this->id = $id;
112 19
    }
113
114
    /**
115
     * Set the nonce.
116
     *
117
     * This is optional: if not called, a nonce will be generated automatically.
118
     *
119
     * @param string $nonce
120
     *   The nonce. The nonce should be hex-based v4 UUID.
121
     */
122 17
    public function setNonce($nonce)
123
    {
124 17
        $this->nonce = $nonce;
125 17
    }
126
127
    /**
128
     * Set the spec version.
129
     *
130
     * This is optional: if not called, the version will be "2.0".
131
     *
132
     * @param string $version
133
     *   The spec version.
134
     */
135 11
    public function setVersion($version)
136
    {
137 11
        $this->version = $version;
138 11
    }
139
140
    /**
141
     * Set the list of custom headers found in a request.
142
     *
143
     * This is optional: if not called, the list of custom headers will be
144
     * empty.
145
     *
146
     * @param string[] $headers
147
     *   A list of custom header names. The values of the headers will be
148
     *   extracted from the request.
149
     */
150 11
    public function setCustomHeaders(array $headers = [])
151
    {
152 11
        $this->headers = $headers;
153 11
    }
154
155
    /**
156
     * Set the authorization signature.
157
     *
158
     * This is optional: if not called, the signature will be generated from the
159
     * other fields and the request. Calling this method manually is not
160
     * recommended outside of testing.
161
     *
162
     * @param string $signature
163
     *   The Base64-encoded authorization signature.
164
     */
165 1
    public function setSignature($signature)
166
    {
167 1
        $this->signature = $signature;
168 1
    }
169
170
    /**
171
     * Builds the authorization header.
172
     *
173
     * @throws \Acquia\Hmac\Exception\MalformedRequestException
174
     *   When a required field (ID, nonce, realm, version) is empty or missing.
175
     *
176
     * @return \Acquia\Hmac\AuthorizationHeader
177
     *   The compiled authorization header.
178
     */
179 20
    public function getAuthorizationHeader()
180
    {
181 20
        if (empty($this->realm) || empty($this->id) || empty($this->nonce) || empty($this->version)) {
182 1
            throw new MalformedRequestException(
183 1
                'One or more required authorization header fields (ID, nonce, realm, version) are missing.',
184 1
                null,
185 1
                0,
186 1
                $this->request
187
            );
188
        }
189
190 19
        $signature = !empty($this->signature) ? $this->signature : $this->generateSignature();
191
192 18
        return new AuthorizationHeader(
193 18
            $this->realm,
194 18
            $this->id,
195 18
            $this->nonce,
196 18
            $this->version,
197 18
            $this->headers,
198 18
            $signature
199
        );
200
    }
201
202
    /**
203
     * Generate a new nonce.
204
     *
205
     * The nonce is a v4 UUID.
206
     *
207
     * @see https://stackoverflow.com/a/15875555
208
     *
209
     * @return string
210
     *   The generated nonce.
211
     */
212 20
    public function generateNonce()
213
    {
214 20
        $data = function_exists('random_bytes') ? random_bytes(16) : openssl_random_pseudo_bytes(16);
215 20
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
216 20
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
217
218 20
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
0 ignored issues
show
It seems like str_split(bin2hex($data), 4) can also be of type true; however, parameter $values of vsprintf() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

218
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', /** @scrutinizer ignore-type */ str_split(bin2hex($data), 4));
Loading history...
219
    }
220
221
    /**
222
     * Generate a signature from the request.
223
     *
224
     * @throws \Acquia\Hmac\Exception\MalformedRequestException
225
     *   When a required header is missing.
226
     *
227
     * @return string
228
     *   The generated signature.
229
     */
230 19
    protected function generateSignature()
231
    {
232 19
        if (!$this->request->hasHeader('X-Authorization-Timestamp')) {
233 1
            throw new MalformedRequestException(
234 1
                'X-Authorization-Timestamp header missing from request.',
235 1
                null,
236 1
                0,
237 1
                $this->request
238
            );
239
        }
240
241 18
        $host = $this->request->getUri()->getHost();
242 18
        $port = $this->request->getUri()->getPort();
243
244 18
        if ($port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $port of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
245 1
            $host .= ':' . $port;
246
        }
247
248
        $parts = [
249 18
            strtoupper($this->request->getMethod()),
250 18
            $host,
251 18
            $this->request->getUri()->getPath(),
252 18
            $this->request->getUri()->getQuery(),
253 18
            $this->serializeAuthorizationParameters(),
254
        ];
255
256 18
        $parts = array_merge($parts, $this->normalizeCustomHeaders());
257
258 18
        $parts[] = $this->request->getHeaderLine('X-Authorization-Timestamp');
259
260 18
        $body = (string) $this->request->getBody();
261
262 18
        if (strlen($body)) {
263 4
            if ($this->request->hasHeader('Content-Type')) {
264 4
                $parts[] = $this->request->getHeaderLine('Content-Type');
265
            }
266
267 4
            $parts[] = $this->digest->hash((string) $body);
268
        }
269
270 18
        return $this->digest->sign(implode("\n", $parts), $this->key->getSecret());
271
    }
272
273
    /**
274
     * Serializes the requireed authorization parameters.
275
     *
276
     * @return string
277
     *   The serialized authorization parameter string.
278
     */
279 18
    protected function serializeAuthorizationParameters()
280
    {
281 18
        return sprintf(
282 18
            'id=%s&nonce=%s&realm=%s&version=%s',
283 18
            $this->id,
284 18
            $this->nonce,
285 18
            rawurlencode($this->realm),
286 18
            $this->version
287
        );
288
    }
289
290
    /**
291
     * Normalizes the custom headers for signing.
292
     *
293
     * @return string[]
294
     *   An array of normalized headers.
295
     */
296 18
    protected function normalizeCustomHeaders()
297
    {
298 18
        $headers = [];
299
300
        // The spec requires that headers are sorted by header name.
301 18
        sort($this->headers);
302 18
        foreach ($this->headers as $header) {
303 3
            if ($this->request->hasHeader($header)) {
304 3
                $headers[] = strtolower($header) . ':' . $this->request->getHeaderLine($header);
305
            }
306
        }
307
308 18
        return $headers;
309
    }
310
}
311