- 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.
203 lines
5.5 KiB
PHP
203 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\OAuth\ValueObjects;
|
|
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
|
|
/**
|
|
* OAuth Token Value Object
|
|
*
|
|
* Immutable composite representation of OAuth access/refresh tokens
|
|
* Uses Value Objects for maximum type safety
|
|
*/
|
|
final readonly class OAuthToken
|
|
{
|
|
public function __construct(
|
|
public AccessToken $accessToken,
|
|
public ?RefreshToken $refreshToken,
|
|
public TokenType $tokenType,
|
|
public ?TokenScope $scope,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Create from provider response
|
|
*
|
|
* @param array<string, mixed> $response
|
|
*/
|
|
public static function fromProviderResponse(array $response): self
|
|
{
|
|
if (! isset($response['access_token'])) {
|
|
throw new \InvalidArgumentException('Provider response missing access_token');
|
|
}
|
|
|
|
$expiresIn = (int) ($response['expires_in'] ?? 3600);
|
|
|
|
return new self(
|
|
accessToken: AccessToken::fromProviderResponse(
|
|
$response['access_token'],
|
|
$expiresIn
|
|
),
|
|
refreshToken: isset($response['refresh_token'])
|
|
? RefreshToken::create($response['refresh_token'])
|
|
: null,
|
|
tokenType: TokenType::fromString($response['token_type'] ?? 'Bearer'),
|
|
scope: isset($response['scope']) && ! empty($response['scope'])
|
|
? TokenScope::fromString($response['scope'])
|
|
: null,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if token is expired
|
|
*/
|
|
public function isExpired(): bool
|
|
{
|
|
return $this->accessToken->isExpired();
|
|
}
|
|
|
|
/**
|
|
* Check if token is still valid
|
|
*/
|
|
public function isValid(): bool
|
|
{
|
|
return $this->accessToken->isValid();
|
|
}
|
|
|
|
/**
|
|
* Check if token can be refreshed
|
|
*/
|
|
public function canRefresh(): bool
|
|
{
|
|
return $this->refreshToken !== null;
|
|
}
|
|
|
|
/**
|
|
* Get authorization header value
|
|
*/
|
|
public function getAuthorizationHeader(): string
|
|
{
|
|
return $this->tokenType->getHeaderPrefix() . ' ' . $this->accessToken->toString();
|
|
}
|
|
|
|
/**
|
|
* Get seconds until expiration
|
|
*/
|
|
public function getSecondsUntilExpiration(): int
|
|
{
|
|
return $this->accessToken->getSecondsUntilExpiration();
|
|
}
|
|
|
|
/**
|
|
* Get expiration timestamp
|
|
*/
|
|
public function getExpiresAt(): Timestamp
|
|
{
|
|
return $this->accessToken->getExpiresAt();
|
|
}
|
|
|
|
/**
|
|
* Check if token has specific scope
|
|
*/
|
|
public function hasScope(string $scope): bool
|
|
{
|
|
return $this->scope !== null && $this->scope->includes($scope);
|
|
}
|
|
|
|
/**
|
|
* Check if token has all required scopes
|
|
*
|
|
* @param array<string> $requiredScopes
|
|
*/
|
|
public function hasScopes(array $requiredScopes): bool
|
|
{
|
|
return $this->scope !== null && $this->scope->includesAll($requiredScopes);
|
|
}
|
|
|
|
/**
|
|
* Create new token with refreshed access token
|
|
*/
|
|
public function withRefreshedAccessToken(
|
|
AccessToken $accessToken,
|
|
?RefreshToken $refreshToken = null
|
|
): self {
|
|
return new self(
|
|
accessToken: $accessToken,
|
|
refreshToken: $refreshToken ?? $this->refreshToken,
|
|
tokenType: $this->tokenType,
|
|
scope: $this->scope,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convert to array for storage
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
$array = [
|
|
'access_token' => $this->accessToken->toString(),
|
|
'expires_at' => $this->accessToken->getExpiresAt()->toTimestamp(),
|
|
'token_type' => $this->tokenType->value,
|
|
];
|
|
|
|
if ($this->refreshToken !== null) {
|
|
$array['refresh_token'] = $this->refreshToken->toString();
|
|
}
|
|
|
|
if ($this->scope !== null) {
|
|
$array['scope'] = $this->scope->toString();
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
|
|
/**
|
|
* Convert to array for logging (with masked tokens)
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function toArrayMasked(): array
|
|
{
|
|
return [
|
|
'access_token' => $this->accessToken->getMasked(),
|
|
'refresh_token' => $this->refreshToken?->getMasked(),
|
|
'expires_at' => $this->accessToken->getExpiresAt()->format('Y-m-d H:i:s'),
|
|
'expires_in' => $this->getSecondsUntilExpiration() . 's',
|
|
'token_type' => $this->tokenType->value,
|
|
'scope' => $this->scope?->toString(),
|
|
'is_expired' => $this->isExpired(),
|
|
'can_refresh' => $this->canRefresh(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create from stored array
|
|
*
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
public static function fromArray(array $data): self
|
|
{
|
|
if (! isset($data['access_token'], $data['expires_at'])) {
|
|
throw new \InvalidArgumentException('Stored token data missing required fields');
|
|
}
|
|
|
|
return new self(
|
|
accessToken: AccessToken::create(
|
|
$data['access_token'],
|
|
Timestamp::fromTimestamp((int) $data['expires_at'])
|
|
),
|
|
refreshToken: isset($data['refresh_token'])
|
|
? RefreshToken::create($data['refresh_token'])
|
|
: null,
|
|
tokenType: TokenType::fromString($data['token_type'] ?? 'Bearer'),
|
|
scope: isset($data['scope']) && ! empty($data['scope'])
|
|
? TokenScope::fromString($data['scope'])
|
|
: null,
|
|
);
|
|
}
|
|
}
|