203 lines
6.0 KiB
PHP
203 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXC\Service;
|
|
|
|
use KTXC\Http\Request\Request;
|
|
use KTXC\Models\Identity\User;
|
|
use KTXC\SessionTenant;
|
|
|
|
/**
|
|
* Security Service
|
|
*
|
|
* Handles request-level authentication (token validation).
|
|
* Authentication orchestration is handled by AuthenticationManager.
|
|
*
|
|
* This service is used by the Kernel to authenticate incoming requests.
|
|
*/
|
|
class SecurityService
|
|
{
|
|
private string $securityCode;
|
|
|
|
public function __construct(
|
|
private readonly TokenService $tokenService,
|
|
private readonly UserService $userService,
|
|
private readonly SessionTenant $sessionTenant
|
|
) {
|
|
$this->securityCode = $this->sessionTenant->configuration()->security()->code();
|
|
}
|
|
|
|
/**
|
|
* Authenticate a request and return the user if valid
|
|
*
|
|
* @param Request $request The HTTP request to authenticate
|
|
* @return User|null The authenticated user, or null if not authenticated
|
|
*/
|
|
public function authenticate(Request $request): ?User
|
|
{
|
|
$authorization = $request->headers->get('Authorization');
|
|
$cookieToken = $request->cookies->get('accessToken');
|
|
|
|
// Cookie token takes precedence
|
|
if ($cookieToken) {
|
|
return $this->authenticateJWT($cookieToken);
|
|
}
|
|
|
|
if ($authorization) {
|
|
if (str_starts_with($authorization, 'Bearer ')) {
|
|
$token = substr($authorization, 7);
|
|
return $this->authenticateBearer($token);
|
|
}
|
|
|
|
if (str_starts_with($authorization, 'Basic ')) {
|
|
$decoded = base64_decode(substr($authorization, 6) ?: '', true);
|
|
if ($decoded !== false) {
|
|
[$identity, $secret] = array_pad(explode(':', $decoded, 2), 2, null);
|
|
if ($identity !== null && $secret !== null) {
|
|
return $this->authenticateBasicHeader($identity, $secret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Authenticate JWT token from cookie or header
|
|
*/
|
|
public function authenticateJWT(string $token): ?User
|
|
{
|
|
$payload = $this->tokenService->validateToken($token, $this->securityCode);
|
|
|
|
if (!$payload) {
|
|
return null;
|
|
}
|
|
|
|
// Verify user still exists
|
|
if ($this->userService->fetchByIdentifier($payload['identifier']) === null) {
|
|
return null;
|
|
}
|
|
|
|
$user = new User();
|
|
$user->populate($payload, 'jwt');
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Authenticate Bearer token
|
|
*/
|
|
public function authenticateBearer(string $token): ?User
|
|
{
|
|
return $this->authenticateJWT($token);
|
|
}
|
|
|
|
/**
|
|
* Authenticate HTTP Basic header (for API access)
|
|
* Note: This is for request authentication, not login
|
|
*/
|
|
private function authenticateBasicHeader(string $identity, string $credentials): ?User
|
|
{
|
|
// For Basic auth headers, we need to validate against the provider
|
|
// This is a simplified flow for API access
|
|
$provider = $this->providerRegistry->resolve('default');
|
|
if ($provider === null) {
|
|
return null;
|
|
}
|
|
|
|
$result = $provider->authenticate($identity, $credentials);
|
|
if (!$result->isSuccess()) {
|
|
return null;
|
|
}
|
|
|
|
return $this->getUserByIdentity($identity);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Token Operations (delegated to AuthenticationManager for new flows)
|
|
// These are kept for backwards compatibility during transition
|
|
// =========================================================================
|
|
|
|
/**
|
|
* @deprecated Use AuthenticationManager::createTokens() instead
|
|
*/
|
|
public function createAccessToken(array $payload): string
|
|
{
|
|
return $this->tokenService->createToken($payload, $this->securityCode, 900);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use AuthenticationManager::createTokens() instead
|
|
*/
|
|
public function createRefreshToken(array $payload): string
|
|
{
|
|
$refreshPayload = [
|
|
'tenant' => $payload['tenant'] ?? null,
|
|
'identifier' => $payload['identifier'],
|
|
'identity' => $payload['identity'],
|
|
'type' => 'refresh'
|
|
];
|
|
|
|
return $this->tokenService->createToken($refreshPayload, $this->securityCode, 604800);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use AuthenticationManager::refreshAccessToken() instead
|
|
*/
|
|
public function validateRefreshToken(string $refreshToken): ?User
|
|
{
|
|
$payload = $this->tokenService->validateToken($refreshToken, $this->securityCode);
|
|
|
|
if (!$payload) {
|
|
return null;
|
|
}
|
|
|
|
if (!isset($payload['type']) || $payload['type'] !== 'refresh') {
|
|
return null;
|
|
}
|
|
|
|
$identifier = $payload['identifier'] ?? null;
|
|
if (!$identifier || $this->providerRegistry->validateUser($identifier) === false) {
|
|
return null;
|
|
}
|
|
|
|
$user = new User();
|
|
$user->populate([
|
|
'identifier' => $payload['identifier'],
|
|
'identity' => $payload['identity'],
|
|
'tenant' => $payload['tenant'] ?? null,
|
|
], 'jwt');
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use AuthenticationManager::logout() instead
|
|
*/
|
|
public function logout(?string $jti = null, ?int $exp = null): void
|
|
{
|
|
if ($jti !== null) {
|
|
$expiresAt = $exp ?? (time() + 86400);
|
|
$this->tokenService->blacklist($jti, $expiresAt);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use AuthenticationManager::logoutAll() instead
|
|
*/
|
|
public function logoutAllDevices(string $identity): void
|
|
{
|
|
$this->tokenService->blacklistUserTokensBefore($identity, time());
|
|
}
|
|
|
|
/**
|
|
* Extract token claims (for logout to get jti/exp)
|
|
*/
|
|
public function extractTokenClaims(string $token): ?array
|
|
{
|
|
return $this->tokenService->validateToken($token, $this->securityCode, checkBlacklist: false);
|
|
}
|
|
}
|