- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
361 lines
12 KiB
PHP
361 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Cryptography\SecureToken;
|
|
use App\Framework\Cryptography\SecureTokenGenerator;
|
|
|
|
it('creates secure token with valid parameters', function () {
|
|
$token = new SecureToken(
|
|
value: 'test-token-value',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: 'ak',
|
|
rawBytes: 'raw-bytes-data',
|
|
metadata: ['purpose' => 'testing']
|
|
);
|
|
|
|
expect($token->getValue())->toBe('test-token-value');
|
|
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_API_KEY);
|
|
expect($token->getFormat())->toBe(SecureTokenGenerator::FORMAT_BASE64_URL);
|
|
expect($token->getLength())->toBe(32);
|
|
expect($token->getPrefix())->toBe('ak');
|
|
expect($token->getRawBytes())->toBe('raw-bytes-data');
|
|
expect($token->getMetadata())->toBe(['purpose' => 'testing']);
|
|
});
|
|
|
|
it('throws exception for empty token value', function () {
|
|
expect(fn () => new SecureToken(
|
|
value: '',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
))->toThrow(InvalidArgumentException::class, 'Token value cannot be empty');
|
|
});
|
|
|
|
it('provides string representation', function () {
|
|
$tokenValue = 'test-token-value';
|
|
$token = new SecureToken(
|
|
value: $tokenValue,
|
|
type: SecureTokenGenerator::TYPE_SESSION,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
expect($token->toString())->toBe($tokenValue);
|
|
expect((string)$token)->toBe($tokenValue);
|
|
});
|
|
|
|
it('extracts value without prefix', function () {
|
|
$token = new SecureToken(
|
|
value: 'ak_abcd1234efgh5678',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: 'ak',
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
expect($token->getValueWithoutPrefix())->toBe('abcd1234efgh5678');
|
|
expect($token->hasPrefix())->toBeTrue();
|
|
});
|
|
|
|
it('handles token without prefix', function () {
|
|
$token = new SecureToken(
|
|
value: 'abcd1234efgh5678',
|
|
type: SecureTokenGenerator::TYPE_SESSION,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
expect($token->getValueWithoutPrefix())->toBe('abcd1234efgh5678');
|
|
expect($token->hasPrefix())->toBeFalse();
|
|
});
|
|
|
|
it('provides raw bytes in different formats', function () {
|
|
$rawBytes = "\x01\x02\x03\x04";
|
|
$token = new SecureToken(
|
|
value: 'test-token',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: $rawBytes,
|
|
metadata: []
|
|
);
|
|
|
|
expect($token->getRawBytesHex())->toBe('01020304');
|
|
expect($token->getRawBytesBase64())->toBe(base64_encode($rawBytes));
|
|
});
|
|
|
|
it('checks equality correctly with timing-safe comparison', function () {
|
|
$tokenValue = 'same-token-value';
|
|
|
|
$token1 = new SecureToken(
|
|
value: $tokenValue,
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
$token2 = new SecureToken(
|
|
value: $tokenValue,
|
|
type: SecureTokenGenerator::TYPE_SESSION,
|
|
format: SecureTokenGenerator::FORMAT_HEX,
|
|
length: 64,
|
|
prefix: 'test',
|
|
rawBytes: 'different-raw',
|
|
metadata: ['different' => 'metadata']
|
|
);
|
|
|
|
$token3 = new SecureToken(
|
|
value: 'different-token-value',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
expect($token1->equals($token2))->toBeTrue(); // Same value
|
|
expect($token1->equals($token3))->toBeFalse(); // Different value
|
|
});
|
|
|
|
it('verifies token value with timing-safe comparison', function () {
|
|
$tokenValue = 'secure-token-value';
|
|
$token = new SecureToken(
|
|
value: $tokenValue,
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
expect($token->verify($tokenValue))->toBeTrue();
|
|
expect($token->verify('wrong-token-value'))->toBeFalse();
|
|
});
|
|
|
|
it('identifies token types correctly', function () {
|
|
$apiKeyToken = new SecureToken(
|
|
value: 'test-token',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
$sessionToken = new SecureToken(
|
|
value: 'test-token',
|
|
type: SecureTokenGenerator::TYPE_SESSION,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
expect($apiKeyToken->isApiKey())->toBeTrue();
|
|
expect($apiKeyToken->isSessionToken())->toBeFalse();
|
|
expect($sessionToken->isSessionToken())->toBeTrue();
|
|
expect($sessionToken->isApiKey())->toBeFalse();
|
|
});
|
|
|
|
it('checks token properties from metadata', function () {
|
|
$singleUseToken = new SecureToken(
|
|
value: 'test-token',
|
|
type: SecureTokenGenerator::TYPE_VERIFICATION,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: ['single_use' => true, 'long_lived' => false]
|
|
);
|
|
|
|
$longLivedToken = new SecureToken(
|
|
value: 'test-token',
|
|
type: SecureTokenGenerator::TYPE_REFRESH,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 64,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: ['single_use' => false, 'long_lived' => true]
|
|
);
|
|
|
|
expect($singleUseToken->isSingleUse())->toBeTrue();
|
|
expect($singleUseToken->isLongLived())->toBeFalse();
|
|
expect($longLivedToken->isLongLived())->toBeTrue();
|
|
expect($longLivedToken->isSingleUse())->toBeFalse();
|
|
});
|
|
|
|
it('gets specific metadata values', function () {
|
|
$token = new SecureToken(
|
|
value: 'test-token',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: ['purpose' => 'api_auth', 'expires' => 3600]
|
|
);
|
|
|
|
expect($token->getMetadataValue('purpose'))->toBe('api_auth');
|
|
expect($token->getMetadataValue('expires'))->toBe(3600);
|
|
expect($token->getMetadataValue('nonexistent', 'default'))->toBe('default');
|
|
});
|
|
|
|
it('calculates token age', function () {
|
|
$token = new SecureToken(
|
|
value: 'test-token',
|
|
type: SecureTokenGenerator::TYPE_SESSION,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
sleep(1);
|
|
$age = $token->getAgeInSeconds();
|
|
expect($age)->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('exports to and imports from array', function () {
|
|
$originalToken = new SecureToken(
|
|
value: 'test-token-value',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: 'ak',
|
|
rawBytes: 'raw-bytes-data',
|
|
metadata: ['purpose' => 'testing', 'scope' => 'read']
|
|
);
|
|
|
|
$array = $originalToken->toArray();
|
|
$restoredToken = SecureToken::fromArray($array);
|
|
|
|
expect($restoredToken->getValue())->toBe($originalToken->getValue());
|
|
expect($restoredToken->getType())->toBe($originalToken->getType());
|
|
expect($restoredToken->getFormat())->toBe($originalToken->getFormat());
|
|
expect($restoredToken->getLength())->toBe($originalToken->getLength());
|
|
expect($restoredToken->getPrefix())->toBe($originalToken->getPrefix());
|
|
expect($restoredToken->getRawBytes())->toBe($originalToken->getRawBytes());
|
|
expect($restoredToken->getMetadata())->toBe($originalToken->getMetadata());
|
|
});
|
|
|
|
it('provides safe summary without sensitive data', function () {
|
|
$token = new SecureToken(
|
|
value: 'very-secret-token-value',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: 'ak',
|
|
rawBytes: 'raw-bytes',
|
|
metadata: ['purpose' => 'api_auth', 'scope' => 'admin']
|
|
);
|
|
|
|
$summary = $token->getSafeSummary();
|
|
|
|
expect($summary)->toHaveKey('type');
|
|
expect($summary)->toHaveKey('format');
|
|
expect($summary)->toHaveKey('length');
|
|
expect($summary)->toHaveKey('prefix');
|
|
expect($summary)->toHaveKey('has_prefix');
|
|
expect($summary)->toHaveKey('metadata_keys');
|
|
expect($summary)->not->toHaveKey('value'); // Should not contain sensitive value
|
|
expect($summary)->not->toHaveKey('raw_bytes'); // Should not contain raw bytes
|
|
|
|
expect($summary['metadata_keys'])->toBe(['purpose', 'scope']);
|
|
});
|
|
|
|
it('generates fingerprint for identification', function () {
|
|
$token = new SecureToken(
|
|
value: 'test-token-value',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
$fingerprint = $token->getFingerprint();
|
|
$shortFingerprint = $token->getShortFingerprint();
|
|
|
|
expect($fingerprint)->toHaveLength(64); // SHA-256 hex
|
|
expect($shortFingerprint)->toHaveLength(16);
|
|
expect($fingerprint)->toStartWith($shortFingerprint);
|
|
});
|
|
|
|
it('masks token value for safe logging', function () {
|
|
$longToken = new SecureToken(
|
|
value: 'this-is-a-very-long-token-value-for-testing',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
$shortToken = new SecureToken(
|
|
value: 'short',
|
|
type: SecureTokenGenerator::TYPE_API_KEY,
|
|
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
length: 32,
|
|
prefix: null,
|
|
rawBytes: 'raw-bytes',
|
|
metadata: []
|
|
);
|
|
|
|
$longMasked = $longToken->getMaskedValue();
|
|
$shortMasked = $shortToken->getMaskedValue();
|
|
|
|
expect($longMasked)->toStartWith('this');
|
|
expect($longMasked)->toEndWith('ting');
|
|
expect($longMasked)->toContain('*');
|
|
expect($shortMasked)->toBe('***'); // Short tokens fully masked
|
|
});
|
|
|
|
it('throws exception for missing required fields in fromArray', function () {
|
|
$incompleteData = [
|
|
'value' => 'test-token',
|
|
'type' => SecureTokenGenerator::TYPE_API_KEY,
|
|
// Missing format, length, raw_bytes
|
|
];
|
|
|
|
expect(fn () => SecureToken::fromArray($incompleteData))
|
|
->toThrow(InvalidArgumentException::class, 'Missing required field');
|
|
});
|
|
|
|
it('throws exception for invalid base64 raw bytes in fromArray', function () {
|
|
$invalidData = [
|
|
'value' => 'test-token',
|
|
'type' => SecureTokenGenerator::TYPE_API_KEY,
|
|
'format' => SecureTokenGenerator::FORMAT_BASE64_URL,
|
|
'length' => 32,
|
|
'raw_bytes' => 'invalid-base64!@#',
|
|
];
|
|
|
|
expect(fn () => SecureToken::fromArray($invalidData))
|
|
->toThrow(InvalidArgumentException::class, 'Invalid Base64 raw bytes');
|
|
});
|