Skip to content

Commit b024829

Browse files
committed
Add Authorization header enum
1 parent c5e4c1b commit b024829

13 files changed

+1197
-0
lines changed

src/Header/Authorization.php

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of php-fast-forward/http-message.
7+
*
8+
* This source file is subject to the license bundled
9+
* with this source code in the file LICENSE.
10+
*
11+
* @link https://github.com/php-fast-forward/http-message
12+
* @copyright Copyright (c) 2025 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Http\Message\Header;
17+
18+
use FastForward\Http\Message\Header\Authorization\ApiKeyCredential;
19+
use FastForward\Http\Message\Header\Authorization\AuthorizationCredential;
20+
use FastForward\Http\Message\Header\Authorization\AwsCredential;
21+
use FastForward\Http\Message\Header\Authorization\BasicCredential;
22+
use FastForward\Http\Message\Header\Authorization\BearerCredential;
23+
use FastForward\Http\Message\Header\Authorization\DigestCredential;
24+
use Psr\Http\Message\RequestInterface;
25+
26+
/**
27+
* Enum Authorization.
28+
*
29+
* Represents supported HTTP `Authorization` header authentication schemes and
30+
* provides helpers to parse raw header values into structured credential
31+
* objects.
32+
*
33+
* The `Authorization` header is used to authenticate a user agent with a
34+
* server, as defined primarily in RFC 7235 and scheme-specific RFCs. This
35+
* utility enum MUST be used in a case-sensitive manner for its enum values
36+
* but MUST treat incoming header names and schemes according to the
37+
* specification of each scheme. Callers SHOULD use the parsing helpers to
38+
* centralize and normalize authentication handling.
39+
*/
40+
enum Authorization: string
41+
{
42+
/**
43+
* A common, non-standard scheme for API key authentication.
44+
*
45+
* This scheme is not defined by an RFC and MAY vary between APIs.
46+
* Implementations using this scheme SHOULD document how the key is
47+
* generated, scoped, and validated.
48+
*/
49+
case ApiKey = 'ApiKey';
50+
51+
/**
52+
* Basic authentication scheme using Base64-encoded "username:password".
53+
*
54+
* Credentials are transmitted in plaintext (after Base64 decoding) and
55+
* therefore MUST only be used over secure transports such as HTTPS.
56+
*
57+
* @see https://datatracker.ietf.org/doc/html/rfc7617
58+
*/
59+
case Basic = 'Basic';
60+
61+
/**
62+
* Bearer token authentication scheme.
63+
*
64+
* Commonly used with OAuth 2.0 access tokens and JWTs. Bearer tokens
65+
* MUST be treated as opaque secrets; any party in possession of a valid
66+
* token MAY use it to obtain access.
67+
*
68+
* @see https://datatracker.ietf.org/doc/html/rfc6750
69+
*/
70+
case Bearer = 'Bearer';
71+
72+
/**
73+
* Digest access authentication scheme.
74+
*
75+
* Uses a challenge-response mechanism to avoid sending passwords in
76+
* cleartext. Implementations SHOULD fully follow the RFC requirements
77+
* to avoid interoperability and security issues.
78+
*
79+
* @see https://datatracker.ietf.org/doc/html/rfc7616
80+
*/
81+
case Digest = 'Digest';
82+
83+
/**
84+
* Amazon Web Services Signature Version 4 scheme.
85+
*
86+
* Used to authenticate requests to AWS services. The credential
87+
* components MUST be constructed according to the AWS Signature Version 4
88+
* process, or validation will fail on the server side.
89+
*
90+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/signing-requests-v4.html
91+
*/
92+
case Aws = 'AWS4-HMAC-SHA256';
93+
94+
/**
95+
* Parses a raw Authorization header string into a structured credential object.
96+
*
97+
* This method MUST:
98+
* - Split the header into an authentication scheme and a credentials part.
99+
* - Resolve the scheme to a supported enum value.
100+
* - Delegate to the appropriate scheme-specific parser.
101+
*
102+
* If the header is empty, malformed, or uses an unsupported scheme,
103+
* this method MUST return null. Callers SHOULD treat a null result as
104+
* an authentication parsing failure.
105+
*
106+
* @param string $header the raw value of the `Authorization` header
107+
*
108+
* @return null|AuthorizationCredential a credential object on successful parsing, or null on failure
109+
*/
110+
public static function parse(string $header): ?AuthorizationCredential
111+
{
112+
if ('' === $header) {
113+
return null;
114+
}
115+
116+
$parts = explode(' ', $header, 2);
117+
if (2 !== \count($parts)) {
118+
return null;
119+
}
120+
121+
[$scheme, $credentials] = $parts;
122+
123+
$authScheme = self::tryFrom($scheme);
124+
if (null === $authScheme) {
125+
return null;
126+
}
127+
128+
return match ($authScheme) {
129+
self::ApiKey => self::parseApiKey($credentials),
130+
self::Basic => self::parseBasic($credentials),
131+
self::Bearer => self::parseBearer($credentials),
132+
self::Digest => self::parseDigest($credentials),
133+
self::Aws => self::parseAws($credentials),
134+
};
135+
}
136+
137+
/**
138+
* Extracts and parses the Authorization header from a collection of headers.
139+
*
140+
* This method MUST treat header names case-insensitively and SHALL use
141+
* the first `Authorization` value if multiple values are provided. If the
142+
* header is missing or cannot be parsed successfully, it MUST return null.
143+
*
144+
* @param array<string, string|string[]> $headers an associative array of HTTP headers
145+
*
146+
* @return null|AuthorizationCredential a parsed credential object or null if not present or invalid
147+
*/
148+
public static function fromHeaderCollection(array $headers): ?AuthorizationCredential
149+
{
150+
$normalizedHeaders = array_change_key_case($headers, CASE_LOWER);
151+
152+
if (!isset($normalizedHeaders['authorization'])) {
153+
return null;
154+
}
155+
156+
$authHeaderValue = $normalizedHeaders['authorization'];
157+
158+
if (\is_array($authHeaderValue)) {
159+
$authHeaderValue = $authHeaderValue[0];
160+
}
161+
162+
return self::parse($authHeaderValue);
163+
}
164+
165+
/**
166+
* Extracts and parses the Authorization header from a PSR-7 request.
167+
*
168+
* This method SHALL delegate to {@see Authorization::fromHeaderCollection()}
169+
* using the request's header collection. It MUST NOT modify the request.
170+
*
171+
* @param RequestInterface $request the PSR-7 request instance
172+
*
173+
* @return null|AuthorizationCredential a parsed credential object or null if not present or invalid
174+
*/
175+
public static function fromRequest(RequestInterface $request): ?AuthorizationCredential
176+
{
177+
return self::fromHeaderCollection($request->getHeaders());
178+
}
179+
180+
/**
181+
* Parses credentials for the ApiKey authentication scheme.
182+
*
183+
* The complete credential string MUST be treated as the API key. No
184+
* additional structure is assumed or validated here; callers MAY apply
185+
* further validation according to application rules.
186+
*
187+
* @param string $credentials the raw credentials portion of the header
188+
*
189+
* @return ApiKeyCredential the parsed API key credential object
190+
*/
191+
private static function parseApiKey(string $credentials): ApiKeyCredential
192+
{
193+
return new ApiKeyCredential($credentials);
194+
}
195+
196+
/**
197+
* Parses credentials for the Basic authentication scheme.
198+
*
199+
* This method MUST:
200+
* - Base64-decode the credentials.
201+
* - Split the decoded string into `username:password`.
202+
*
203+
* If decoding fails or the decoded value does not contain exactly one
204+
* colon separator, this method MUST return null.
205+
*
206+
* @param string $credentials the Base64-encoded "username:password" string
207+
*
208+
* @return null|BasicCredential the parsed Basic credential, or null on failure
209+
*/
210+
private static function parseBasic(string $credentials): ?BasicCredential
211+
{
212+
$decoded = base64_decode($credentials, true);
213+
if (false === $decoded) {
214+
return null;
215+
}
216+
217+
$parts = explode(':', $decoded, 2);
218+
if (2 !== \count($parts)) {
219+
return null;
220+
}
221+
222+
[$username, $password] = $parts;
223+
224+
return new BasicCredential($username, $password);
225+
}
226+
227+
/**
228+
* Parses credentials for the Bearer authentication scheme.
229+
*
230+
* The credentials MUST be treated as an opaque bearer token. This method
231+
* SHALL NOT attempt to validate or inspect the token contents.
232+
*
233+
* @param string $credentials the bearer token string
234+
*
235+
* @return BearerCredential the parsed Bearer credential object
236+
*/
237+
private static function parseBearer(string $credentials): BearerCredential
238+
{
239+
return new BearerCredential($credentials);
240+
}
241+
242+
/**
243+
* Parses credentials for the Digest authentication scheme.
244+
*
245+
* This method MUST parse comma-separated key=value pairs according to
246+
* RFC 7616. Values MAY be quoted or unquoted. If any part is malformed
247+
* or required parameters are missing, it MUST return null.
248+
*
249+
* Required parameters:
250+
* - username
251+
* - realm
252+
* - nonce
253+
* - uri
254+
* - response
255+
* - qop
256+
* - nc
257+
* - cnonce
258+
*
259+
* Optional parameters such as `opaque` and `algorithm` SHALL be included
260+
* in the credential object when present.
261+
*
262+
* @param string $credentials the raw credentials portion of the header
263+
*
264+
* @return null|DigestCredential the parsed Digest credential object, or null on failure
265+
*/
266+
private static function parseDigest(string $credentials): ?DigestCredential
267+
{
268+
$params = [];
269+
$parts = explode(',', $credentials);
270+
271+
foreach ($parts as $part) {
272+
$part = mb_trim($part);
273+
274+
$pattern = '/^(?<key>[a-zA-Z0-9_-]+)=(?<value>"[^"]*"|[^"]*)$/i';
275+
276+
if (!preg_match($pattern, $part, $match)) {
277+
return null;
278+
}
279+
280+
$key = mb_strtolower($match['key']);
281+
$value = mb_trim($match['value'], '"');
282+
$params[$key] = $value;
283+
}
284+
285+
$required = ['username', 'realm', 'nonce', 'uri', 'response', 'qop', 'nc', 'cnonce'];
286+
foreach ($required as $key) {
287+
if (!isset($params[$key])) {
288+
return null;
289+
}
290+
}
291+
292+
return new DigestCredential(
293+
username: $params['username'],
294+
realm: $params['realm'],
295+
nonce: $params['nonce'],
296+
uri: $params['uri'],
297+
response: $params['response'],
298+
qop: $params['qop'],
299+
nc: $params['nc'],
300+
cnonce: $params['cnonce'],
301+
opaque: $params['opaque'] ?? null,
302+
algorithm: $params['algorithm'] ?? null,
303+
);
304+
}
305+
306+
/**
307+
* Parses credentials for the AWS Signature Version 4 authentication scheme.
308+
*
309+
* This method MUST parse comma-separated key=value pairs and verify that
310+
* the mandatory parameters `Credential`, `SignedHeaders`, and `Signature`
311+
* are present. The `Signature` value MUST be a 64-character hexadecimal
312+
* string. If parsing or validation fails, it MUST return null.
313+
*
314+
* The `Credential` parameter contains the full credential scope in the form
315+
* `AccessKeyId/Date/Region/Service/aws4_request`, which SHALL be stored
316+
* as-is for downstream processing.
317+
*
318+
* @param string $credentials the raw credentials portion of the header
319+
*
320+
* @return null|AwsCredential the parsed AWS credential object, or null on failure
321+
*/
322+
private static function parseAws(string $credentials): ?AwsCredential
323+
{
324+
$params = [];
325+
$parts = explode(',', $credentials);
326+
327+
foreach ($parts as $part) {
328+
$part = mb_trim($part);
329+
330+
$pattern = '/^(?<key>[a-zA-Z0-9_-]+)=(?<value>[^, ]+)$/';
331+
332+
if (!preg_match($pattern, $part, $match)) {
333+
return null;
334+
}
335+
336+
$key = mb_trim($match['key']);
337+
$value = mb_trim($match['value']);
338+
$params[$key] = $value;
339+
}
340+
341+
$required = ['Credential', 'SignedHeaders', 'Signature'];
342+
foreach ($required as $key) {
343+
if (!isset($params[$key])) {
344+
return null;
345+
}
346+
}
347+
348+
if (!preg_match('/^[0-9a-fA-F]{64}$/', $params['Signature'])) {
349+
return null;
350+
}
351+
352+
return new AwsCredential(
353+
algorithm: self::Aws->value,
354+
credentialScope: $params['Credential'],
355+
signedHeaders: $params['SignedHeaders'],
356+
signature: $params['Signature'],
357+
);
358+
}
359+
}

0 commit comments

Comments
 (0)