Auth¶
Two authentication subsystems: HMAC for signing and validating HTTP requests, and Security for password hashing and JWT token management. The Application layer defines ports; the Adapter layer provides concrete implementations.
Application\Auth
├── Authenticator (interface) — validate(ServerRequestInterface): bool
├── RequestService (interface) — signRequest(RequestInterface): RequestInterface
├── Security\
│ ├── PasswordHasher (interface) — hash(string): string
│ ├── PasswordValidator (interface) — validate(), needsRehash()
│ ├── TokenEncoder (interface) — encode(array, DateTimeImmutable): string
│ └── TokenDecoder (interface) — decode(string): array
└── Exception\
├── AuthException
├── TokenException
├── PasswordException
└── CredentialsException
Adapter\Auth
├── Hmac\
│ ├── HmacAuthenticator — Authenticator → HMAC request validation
│ ├── HmacRequestService — RequestService → HMAC request signing
│ ├── HmacKeyGenerator — generateSecureRandom(int): string
│ └── HmacMethods (trait) — canonical request string, derived-key signing
└── Security\
├── PhpPasswordHasher — PasswordHasher → password_hash()
├── PhpPasswordValidator — PasswordValidator → password_verify()
├── JwtEncoder — TokenEncoder → lcobucci/jwt
└── JwtDecoder — TokenDecoder → lcobucci/jwt
Table of Contents¶
- Authenticator (Interface)
- RequestService (Interface)
- HmacAuthenticator
- HmacRequestService
- HmacMethods (Trait)
- HmacKeyGenerator
- PasswordHasher / PasswordValidator (Interfaces)
- PhpPasswordHasher / PhpPasswordValidator
- TokenEncoder / TokenDecoder (Interfaces)
- JwtEncoder / JwtDecoder
- Exception Hierarchy
- Installation
- Symfony Configuration
- Usage Examples
Authenticator (Interface)¶
Fight\Common\Application\Auth\Authenticator
interface Authenticator
{
/** @throws AuthException */
public function validate(ServerRequestInterface $request): bool;
}
Single implementation: HmacAuthenticator.
RequestService (Interface)¶
Fight\Common\Application\Auth\RequestService
interface RequestService
{
/** @throws CredentialsException */
public function signRequest(RequestInterface $request): RequestInterface;
}
Single implementation: HmacRequestService.
HmacAuthenticator¶
Fight\Common\Adapter\Auth\Hmac\HmacAuthenticator
Validates an incoming PSR-7 request by reconstructing its HMAC-SHA256 signature and
comparing it against the Signature header. Uses the HmacMethods trait.
final class HmacAuthenticator implements Authenticator
{
public function __construct(
private string $public,
string $private,
private int $timeTolerance
) {}
}
| Parameter | Description |
|---|---|
$public |
Public key identifier (sent in the Credential header) |
$private |
Hex-encoded shared secret (converted to binary internally) |
$timeTolerance |
Allowed clock skew in seconds for X-Timestamp |
Validation flow¶
- Required headers — checks
Authorization,Credential,Signature,X-Timestamp,X-Nonceare all present. ThrowsAuthException(422) if any are missing. - Timestamp — validates
X-Timestampis within$timeToleranceofREQUEST_TIME. ThrowsAuthException(400) if out of bounds. - Credential — checks
Credentialheader matches$this->public. ThrowsAuthException(401) on mismatch. - Body content — if body is non-empty, validates
X-Content-SHA256header exists (422) and matchessha256(body)(400). - Signature — builds canonical request string via
HmacMethods, computes expected signature, returnstrueon match orfalseon mismatch (no exception).
$authenticator = new HmacAuthenticator($publicKey, $privateKey, 300);
$valid = $authenticator->validate($serverRequest);
HmacRequestService¶
Fight\Common\Adapter\Auth\Hmac\HmacRequestService
Signs an outgoing PSR-7 request with HMAC-SHA256 authentication headers. Uses the
HmacMethods trait.
final class HmacRequestService implements RequestService
{
public function __construct(
private string $public,
string $private
) {}
}
| Parameter | Description |
|---|---|
$public |
Public key identifier |
$private |
Hex-encoded shared secret (converted to binary internally) |
Signing flow¶
- Normalizes the URI (sorts query parameters alphabetically)
- Adds headers:
X-Timestamp(current time),X-Nonce(8 random bytes hex), andX-Content-SHA256(if body is non-empty) - Builds canonical request string via
HmacMethods - Creates signature via
HmacMethodsderived-key scheme - Adds
Authorization: HMAC-SHA256,Credential: {public},Signature: {signature} - Sorts all headers by key and returns the modified request
$service = new HmacRequestService($publicKey, $privateKey);
$signedRequest = $service->signRequest($request);
HmacMethods (Trait)¶
Fight\Common\Adapter\Auth\Hmac\HmacMethods
Shared trait used by both HmacAuthenticator and HmacRequestService.
trait HmacMethods
{
abstract protected function getSecret(): string;
protected function normalizeUri(UriInterface $uri): UriInterface;
protected function createCanonicalRequestString(
string $method,
string $authority,
string $path,
string $query,
array $headers
): string;
protected function createSignature(string $canonicalRequest, int $timestamp): string;
}
Canonical Request String¶
Derived-Key Signature¶
The signature uses a three-level HMAC-SHA256 derivation:
dateKey = HMAC-SHA256("HMAC{secret}", YYYY-MM-DD)
signingKey = HMAC-SHA256(dateKey, "signed-request")
signature = HMAC-SHA256(signingKey, "HMAC-SHA256\n{timestamp}\n{sha256(canonicalRequest)}")
The getSecret() abstract method returns the binary secret key and must be implemented by
the using class.
HmacKeyGenerator¶
Fight\Common\Adapter\Auth\Hmac\HmacKeyGenerator
Generates cryptographically secure random hex-encoded keys for HMAC authentication.
final class HmacKeyGenerator
{
/** @throws Exception */
public static function generateSecureRandom(int $bytes = 16): string;
}
Returns bin2hex(random_bytes($bytes)). Default 16 bytes produces a 32-character hex
string suitable for use as a public or private HMAC key.
$public = HmacKeyGenerator::generateSecureRandom();
$private = HmacKeyGenerator::generateSecureRandom(32); // 64 hex chars
PasswordHasher / PasswordValidator (Interfaces)¶
Fight\Common\Application\Auth\Security\PasswordHasher
interface PasswordHasher
{
/** @throws PasswordException */
public function hash(string $password): string;
}
Fight\Common\Application\Auth\Security\PasswordValidator
interface PasswordValidator
{
public function validate(string $password, string $hash): bool;
public function needsRehash(string $hash): bool;
}
PhpPasswordHasher / PhpPasswordValidator¶
Fight\Common\Adapter\Auth\Security\PhpPasswordHasher
Wraps PHP's native password_hash(). Rejects passwords containing null bytes.
final readonly class PhpPasswordHasher implements PasswordHasher
{
public function __construct(
private string $algorithm,
private ?array $options = null
) {}
}
| Constructor | Example |
|---|---|
PhpPasswordHasher(PASSWORD_BCRYPT) |
Default bcrypt cost (10) |
PhpPasswordHasher(PASSWORD_BCRYPT, ['cost' => 12]) |
Custom cost |
Throws PasswordException if the password contains a null byte.
Fight\Common\Adapter\Auth\Security\PhpPasswordValidator
Wraps PHP's native password_verify() and password_needs_rehash().
final readonly class PhpPasswordValidator implements PasswordValidator
{
public function __construct(
private string $algorithm,
private ?array $options = null
) {}
}
| Method | Delegates to |
|---|---|
validate() |
password_verify() |
needsRehash() |
password_needs_rehash() |
$hasher = new PhpPasswordHasher(PASSWORD_BCRYPT, ['cost' => 12]);
$validator = new PhpPasswordValidator(PASSWORD_BCRYPT, ['cost' => 12]);
$hash = $hasher->hash('s3cret!');
$validator->validate('s3cret!', $hash); // true
$validator->needsRehash($hash); // false (same cost)
TokenEncoder / TokenDecoder (Interfaces)¶
Fight\Common\Application\Auth\Security\TokenEncoder
interface TokenEncoder
{
/** @throws TokenException */
public function encode(array $claims, DateTimeImmutable $expiration): string;
}
Fight\Common\Application\Auth\Security\TokenDecoder
interface TokenDecoder
{
/** @throws TokenException */
public function decode(string $token): array;
}
JwtEncoder / JwtDecoder¶
Fight\Common\Adapter\Auth\Security\JwtEncoder
Creates signed JWT tokens using lcobucci/jwt. Supported algorithms: HS256, HS384, HS512.
final class JwtEncoder implements TokenEncoder
{
public function __construct(
string $hexSecret,
string $algorithm = 'HS256'
) {}
}
Registered claims (iss, sub, aud, nbf, iat, jti) are extracted from the
$claims array and set via the appropriate builder methods. All other claims use
$builder->withClaim(). The exp claim is set from the $expiration parameter.
$encoder = new JwtEncoder($hexSecret, 'HS256');
$token = $encoder->encode(
['sub' => 'user_123', 'role' => 'admin'],
new DateTimeImmutable('+1 hour')
);
Fight\Common\Adapter\Auth\Security\JwtDecoder
Validates and decodes signed JWT tokens using lcobucci/jwt.
final class JwtDecoder implements TokenDecoder
{
public function __construct(
string $hexSecret,
string $algorithm = 'HS256'
) {}
}
On construction, registers a SignedWith constraint. On decode():
- Parses the JWT string
- Validates the signature via
SignedWith - Returns all claims via
$token->claims()->all() - Throws
TokenExceptionon any failure (invalid signature, expired token, malformed string, etc.)
$decoder = new JwtDecoder($hexSecret, 'HS256');
$claims = $decoder->decode($token);
// ['sub' => 'user_123', 'role' => 'admin', 'exp' => ..., ...]
Exception Hierarchy¶
| Exception | Thrown By | Description |
|---|---|---|
AuthException |
Authenticator::validate() |
Authentication failure |
TokenException |
TokenEncoder::encode(), TokenDecoder::decode() |
JWT encoding/decoding failure |
PasswordException |
PasswordHasher::hash() |
Password hashing failure (e.g. null byte) |
CredentialsException |
RequestService::signRequest() |
Credential signing failure |
All four are empty exception classes extending AuthException which extends
SystemException.
Installation¶
The Auth component itself has no external dependencies beyond PSR-7. Optional adapter dependencies:
JWT¶
HMAC¶
No additional packages — HMAC uses PHP's native hash_hmac() and random_bytes().
Password Hashing¶
No additional packages — PhpPasswordHasher and PhpPasswordValidator use PHP's native
password_hash() and password_verify().
Symfony Configuration¶
# config/packages/common_auth.yaml
services:
_defaults:
autowire: true
autoconfigure: true
# --- HMAC Authentication ---
Fight\Common\Adapter\Auth\Hmac\HmacAuthenticator:
arguments:
$public: '%env(HMAC_PUBLIC_KEY)%'
$private: '%env(HMAC_PRIVATE_KEY)%'
$timeTolerance: 300
Fight\Common\Adapter\Auth\Hmac\HmacRequestService:
arguments:
$public: '%env(HMAC_PUBLIC_KEY)%'
$private: '%env(HMAC_PRIVATE_KEY)%'
# --- Password Hashing ---
Fight\Common\Adapter\Auth\Security\PhpPasswordHasher:
arguments:
$algorithm: !php/const PASSWORD_BCRYPT
$options:
cost: 12
Fight\Common\Adapter\Auth\Security\PhpPasswordValidator:
arguments:
$algorithm: !php/const PASSWORD_BCRYPT
$options:
cost: 12
# --- JWT ---
Fight\Common\Adapter\Auth\Security\JwtEncoder:
arguments:
$hexSecret: '%env(JWT_SECRET)%'
$algorithm: 'HS256'
Fight\Common\Adapter\Auth\Security\JwtDecoder:
arguments:
$hexSecret: '%env(JWT_SECRET)%'
$algorithm: 'HS256'
# --- Interface aliases ---
Fight\Common\Application\Auth\Authenticator:
alias: Fight\Common\Adapter\Auth\Hmac\HmacAuthenticator
Fight\Common\Application\Auth\RequestService:
alias: Fight\Common\Adapter\Auth\Hmac\HmacRequestService
Fight\Common\Application\Auth\Security\PasswordHasher:
alias: Fight\Common\Adapter\Auth\Security\PhpPasswordHasher
Fight\Common\Application\Auth\Security\PasswordValidator:
alias: Fight\Common\Adapter\Auth\Security\PhpPasswordValidator
Fight\Common\Application\Auth\Security\TokenEncoder:
alias: Fight\Common\Adapter\Auth\Security\JwtEncoder
Fight\Common\Application\Auth\Security\TokenDecoder:
alias: Fight\Common\Adapter\Auth\Security\JwtDecoder
Usage Examples¶
HMAC — Signing an Outgoing Request¶
use Fight\Common\Adapter\Auth\Hmac\HmacRequestService;
use Fight\Common\Adapter\HttpClient\Guzzle\GuzzleMessageFactory;
$signer = new HmacRequestService($publicKey, $privateKey);
$factory = new GuzzleMessageFactory();
$request = $factory->createRequest('POST', '/api/orders', [
'Content-Type' => 'application/json',
], json_encode(['product' => 'widget']));
$signed = $signer->signRequest($request);
// Now send $signed with any HTTP client
HMAC — Validating an Incoming Request¶
use Fight\Common\Adapter\Auth\Hmac\HmacAuthenticator;
$authenticator = new HmacAuthenticator($publicKey, $privateKey, 300);
if (!$authenticator->validate($serverRequest)) {
// Invalid signature — return 401
}
// Authenticated — proceed
Password Hashing and Verification¶
use Fight\Common\Adapter\Auth\Security\PhpPasswordHasher;
use Fight\Common\Adapter\Auth\Security\PhpPasswordValidator;
$hasher = new PhpPasswordHasher(PASSWORD_BCRYPT, ['cost' => 12]);
$validator = new PhpPasswordValidator(PASSWORD_BCRYPT, ['cost' => 12]);
// Registration
$hash = $hasher->hash($plaintextPassword);
// Store $hash in the database
// Login
if (!$validator->validate($plaintextPassword, $storedHash)) {
throw new RuntimeException('Invalid password');
}
// During login, check if rehashing is needed
if ($validator->needsRehash($storedHash)) {
$newHash = $hasher->hash($plaintextPassword);
// Update stored hash
}
JWT — Issue and Validate a Token¶
use Fight\Common\Adapter\Auth\Security\JwtEncoder;
use Fight\Common\Adapter\Auth\Security\JwtDecoder;
$encoder = new JwtEncoder($hexSecret, 'HS256');
$decoder = new JwtDecoder($hexSecret, 'HS256');
// Issue
$token = $encoder->encode(
['sub' => 'user_456', 'role' => 'editor'],
new DateTimeImmutable('+2 hours')
);
// Validate
try {
$claims = $decoder->decode($token);
echo $claims['sub']; // 'user_456'
} catch (TokenException $e) {
// Expired, invalid signature, or malformed
}