acquia /
http-hmac-php
| 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
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
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
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 |