Domain · Application · Adapter
Auth
Verify signed requests, passwords, and tokens through narrow ports while keeping credentials and replay policy at trusted boundaries.
Authentication supplies separate contracts for HMAC request verification, password hashing, and token encoding. These prove identity evidence; they do not decide what the authenticated actor may do.
Keep private HMAC keys and token signing material outside source control, compare signatures through the shipped verifier, rotate credentials deliberately, and treat decoded JWT claims as untrusted until issuer, audience, time, and application-specific claims have been accepted by your policy.
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
Authentication verifies identity evidence; authorization remains an application policy. HMAC
validation can optionally consume request nonces through a Domain\Auth\NonceRepository. Use a
repository shared by all accepting instances for replay-sensitive traffic. The shipped in-memory
repository is process-local; the Doctrine repository provides a durable shared boundary.
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,
private ?NonceRepository $nonceRepository = null,
) {}
}
| 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 the canonical request string via
HmacMethodsand compares it withhash_equals(). ThrowsAuthException(401) on mismatch. - Replay consumption — when a
NonceRepositoryis supplied, consumes the signed nonce with an expiry derived from the request timestamp and tolerance. Duplicate or storage failures are wrapped asAuthException. Without a repository, timestamp validation alone does not prevent a request from being replayed within the tolerance window.
$authenticator = new HmacAuthenticator($publicKey, $privateKey, 300, $nonceRepository);
$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)
Laravel applications can instead bind LaravelPasswordHasher and LaravelPasswordValidator to
Laravel's configured Illuminate\Contracts\Hashing\Hasher. These adapters preserve the same Fight
ports; algorithm and rehash policy remain in the Laravel hasher configuration.
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 parsing or signature failure
JwtDecoder does not validate expiration, not-before, issuer, audience, subject, or token ID.
The consuming authentication policy must validate every required claim and time constraint before
trusting the returned array. Decoding is signature verification, not a complete login decision.
$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
}