Files
michaelschiemer/tests/Security/AuthenticationTests/TokenValidationTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

373 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Security\AuthenticationTests;
use Tests\Security\SecurityTestCase;
/**
* Token Validation tests
*
* Tests JWT/Bearer token validation, expiration, and signature verification
*/
final readonly class TokenValidationTest extends SecurityTestCase
{
/**
* Test JWT token structure validation
*/
public function testValidatesJwtStructure(): void
{
$validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
if (!$this->hasValidJwtStructure($validToken)) {
throw new \RuntimeException('Valid JWT structure rejected');
}
echo "✅ Valid JWT structure accepted\n";
// Test invalid structures
$invalidTokens = [
'invalid', // Not base64
'header.payload', // Missing signature
'header.payload.signature.extra', // Too many parts
'', // Empty
];
foreach ($invalidTokens as $token) {
if ($this->hasValidJwtStructure($token)) {
throw new \RuntimeException("Invalid JWT structure accepted: {$token}");
}
}
echo "✅ Invalid JWT structures rejected\n";
}
/**
* Test token expiration validation
*/
public function testValidatesTokenExpiration(): void
{
// Expired token (exp claim in the past)
$expiredToken = [
'sub' => '1234567890',
'name' => 'John Doe',
'exp' => time() - 3600 // Expired 1 hour ago
];
if (!$this->isTokenExpired($expiredToken)) {
throw new \RuntimeException('Expired token not detected');
}
echo "✅ Expired token detected\n";
// Valid token (exp claim in the future)
$validToken = [
'sub' => '1234567890',
'name' => 'John Doe',
'exp' => time() + 3600 // Expires in 1 hour
];
if ($this->isTokenExpired($validToken)) {
throw new \RuntimeException('Valid token marked as expired');
}
echo "✅ Valid token accepted\n";
}
/**
* Test token signature verification
*/
public function testVerifiesTokenSignature(): void
{
$secret = 'test-secret-key';
$header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64_encode(json_encode(['sub' => '123', 'exp' => time() + 3600]));
// Valid signature
$validSignature = base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true));
$validToken = "$header.$payload.$validSignature";
if (!$this->verifyTokenSignature($validToken, $secret)) {
throw new \RuntimeException('Valid token signature rejected');
}
echo "✅ Valid token signature verified\n";
// Invalid signature
$invalidSignature = base64_encode('invalid-signature');
$invalidToken = "$header.$payload.$invalidSignature";
if ($this->verifyTokenSignature($invalidToken, $secret)) {
throw new \RuntimeException('Invalid token signature accepted');
}
echo "✅ Invalid token signature rejected\n";
}
/**
* Test Bearer token format
*/
public function testValidatesBearerTokenFormat(): void
{
$validBearerTokens = [
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature',
'Bearer valid-api-key-12345',
];
foreach ($validBearerTokens as $token) {
if (!$this->hasValidBearerFormat($token)) {
throw new \RuntimeException("Valid Bearer token rejected: {$token}");
}
}
echo "✅ Valid Bearer token formats accepted\n";
$invalidBearerTokens = [
'bearer token', // Lowercase 'bearer'
'Token xyz', // Wrong prefix
'eyJhbGci...', // Missing 'Bearer' prefix
'', // Empty
];
foreach ($invalidBearerTokens as $token) {
if ($this->hasValidBearerFormat($token)) {
throw new \RuntimeException("Invalid Bearer token accepted: {$token}");
}
}
echo "✅ Invalid Bearer token formats rejected\n";
}
/**
* Test token claims validation
*/
public function testValidatesTokenClaims(): void
{
// Valid claims
$validClaims = [
'sub' => '1234567890',
'name' => 'John Doe',
'iat' => time(),
'exp' => time() + 3600,
'iss' => 'https://example.com',
'aud' => 'https://api.example.com'
];
$validationErrors = $this->validateTokenClaims($validClaims);
if (!empty($validationErrors)) {
throw new \RuntimeException(
"Valid claims rejected: " . implode(', ', $validationErrors)
);
}
echo "✅ Valid token claims accepted\n";
// Missing required claims
$invalidClaims = [
'name' => 'John Doe',
// Missing 'sub' and 'exp'
];
$validationErrors = $this->validateTokenClaims($invalidClaims);
if (empty($validationErrors)) {
throw new \RuntimeException('Missing required claims not detected');
}
echo "✅ Missing required claims detected\n";
}
/**
* Test token issued at (iat) validation
*/
public function testValidatesIssuedAtClaim(): void
{
// Future iat (token issued in the future)
$futureIat = [
'sub' => '123',
'iat' => time() + 3600, // Issued 1 hour in the future
'exp' => time() + 7200
];
if (!$this->isIssuedInFuture($futureIat)) {
throw new \RuntimeException('Future iat not detected');
}
echo "✅ Future iat claim detected\n";
// Valid iat (issued in the past)
$validIat = [
'sub' => '123',
'iat' => time() - 60, // Issued 1 minute ago
'exp' => time() + 3600
];
if ($this->isIssuedInFuture($validIat)) {
throw new \RuntimeException('Valid iat marked as future');
}
echo "✅ Valid iat claim accepted\n";
}
/**
* Test token not before (nbf) validation
*/
public function testValidatesNotBeforeClaim(): void
{
// Token not yet valid (nbf in the future)
$notYetValid = [
'sub' => '123',
'nbf' => time() + 3600, // Valid in 1 hour
'exp' => time() + 7200
];
if (!$this->isNotYetValid($notYetValid)) {
throw new \RuntimeException('Not-yet-valid token not detected');
}
echo "✅ Not-yet-valid token detected\n";
// Token already valid (nbf in the past)
$alreadyValid = [
'sub' => '123',
'nbf' => time() - 60, // Valid since 1 minute ago
'exp' => time() + 3600
];
if ($this->isNotYetValid($alreadyValid)) {
throw new \RuntimeException('Valid token marked as not-yet-valid');
}
echo "✅ Valid token accepted (nbf check)\n";
}
/**
* Run all token validation tests
*/
public function runAllTests(): array
{
$results = [];
try {
$this->testValidatesJwtStructure();
$results['jwt_structure'] = 'PASS';
} catch (\Exception $e) {
$results['jwt_structure'] = 'FAIL: ' . $e->getMessage();
}
try {
$this->testValidatesTokenExpiration();
$results['token_expiration'] = 'PASS';
} catch (\Exception $e) {
$results['token_expiration'] = 'FAIL: ' . $e->getMessage();
}
try {
$this->testVerifiesTokenSignature();
$results['signature_verification'] = 'PASS';
} catch (\Exception $e) {
$results['signature_verification'] = 'FAIL: ' . $e->getMessage();
}
try {
$this->testValidatesBearerTokenFormat();
$results['bearer_format'] = 'PASS';
} catch (\Exception $e) {
$results['bearer_format'] = 'FAIL: ' . $e->getMessage();
}
try {
$this->testValidatesTokenClaims();
$results['claims_validation'] = 'PASS';
} catch (\Exception $e) {
$results['claims_validation'] = 'FAIL: ' . $e->getMessage();
}
try {
$this->testValidatesIssuedAtClaim();
$results['iat_validation'] = 'PASS';
} catch (\Exception $e) {
$results['iat_validation'] = 'FAIL: ' . $e->getMessage();
}
try {
$this->testValidatesNotBeforeClaim();
$results['nbf_validation'] = 'PASS';
} catch (\Exception $e) {
$results['nbf_validation'] = 'FAIL: ' . $e->getMessage();
}
return $results;
}
private function hasValidJwtStructure(string $token): bool
{
$parts = explode('.', $token);
return count($parts) === 3 && !empty($parts[0]) && !empty($parts[1]) && !empty($parts[2]);
}
private function isTokenExpired(array $claims): bool
{
if (!isset($claims['exp'])) {
return true; // No expiration claim = treat as expired
}
return time() >= $claims['exp'];
}
private function verifyTokenSignature(string $token, string $secret): bool
{
$parts = explode('.', $token);
if (count($parts) !== 3) {
return false;
}
[$header, $payload, $signature] = $parts;
$validSignature = base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true));
return hash_equals($validSignature, $signature);
}
private function hasValidBearerFormat(string $authHeader): bool
{
return str_starts_with($authHeader, 'Bearer ') && strlen($authHeader) > 7;
}
private function validateTokenClaims(array $claims): array
{
$errors = [];
// Required claims
$requiredClaims = ['sub', 'exp'];
foreach ($requiredClaims as $claim) {
if (!isset($claims[$claim])) {
$errors[] = "Missing required claim: {$claim}";
}
}
return $errors;
}
private function isIssuedInFuture(array $claims): bool
{
if (!isset($claims['iat'])) {
return false;
}
return $claims['iat'] > time();
}
private function isNotYetValid(array $claims): bool
{
if (!isset($claims['nbf'])) {
return false;
}
return time() < $claims['nbf'];
}
}