Skip to content

Domain · Application · Adapter

Auth

Verify signed requests, passwords, and tokens through narrow ports while keeping credentials and replay policy at trusted boundaries.

The application chooses the trust decision; adapters verify protocol-specific evidence without making authorization decisions.

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

  1. Authenticator (Interface)
  2. RequestService (Interface)
  3. HmacAuthenticator
  4. HmacRequestService
  5. HmacMethods (Trait)
  6. HmacKeyGenerator
  7. PasswordHasher / PasswordValidator (Interfaces)
  8. PhpPasswordHasher / PhpPasswordValidator
  9. TokenEncoder / TokenDecoder (Interfaces)
  10. JwtEncoder / JwtDecoder
  11. Exception Hierarchy
  12. Installation
  13. Symfony Configuration
  14. 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

  1. Required headers — checks Authorization, Credential, Signature, X-Timestamp, X-Nonce are all present. Throws AuthException (422) if any are missing.
  2. Timestamp — validates X-Timestamp is within $timeTolerance of REQUEST_TIME. Throws AuthException (400) if out of bounds.
  3. Credential — checks Credential header matches $this->public. Throws AuthException (401) on mismatch.
  4. Body content — if body is non-empty, validates X-Content-SHA256 header exists (422) and matches sha256(body) (400).
  5. Signature — builds the canonical request string via HmacMethods and compares it with hash_equals(). Throws AuthException (401) on mismatch.
  6. Replay consumption — when a NonceRepository is supplied, consumes the signed nonce with an expiry derived from the request timestamp and tolerance. Duplicate or storage failures are wrapped as AuthException. 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

  1. Normalizes the URI (sorts query parameters alphabetically)
  2. Adds headers: X-Timestamp (current time), X-Nonce (8 random bytes hex), and X-Content-SHA256 (if body is non-empty)
  3. Builds canonical request string via HmacMethods
  4. Creates signature via HmacMethods derived-key scheme
  5. Adds Authorization: HMAC-SHA256, Credential: {public}, Signature: {signature}
  6. 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

{METHOD} {authority}{path}{?query}
{header1}:{value1}
{header2}:{value2}

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():

  1. Parses the JWT string
  2. Validates the signature via SignedWith
  3. Returns all claims via $token->claims()->all()
  4. Throws TokenException on 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

SystemException
└── AuthException
    ├── TokenException
    ├── PasswordException
    └── CredentialsException
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

composer require lcobucci/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
}