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
Bug
introduced
by
![]() |
|||
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
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 For 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
![]() |
|||
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 |