<?php
namespace App\EventSubscriber;
use App\Entity\User;
use App\Service\IdTokenGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token\Parser as TokenParser;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class OAuth2TokenResponseSubscriber implements EventSubscriberInterface
{
public function __construct(
private IdTokenGenerator $idTokenGenerator,
private LoggerInterface $logger,
private EntityManagerInterface $entityManager,
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => 'onResponse',
];
}
public function onResponse(ResponseEvent $event): void
{
$request = $event->getRequest();
$response = $event->getResponse();
// Only intercept /token endpoint
if ($request->getPathInfo() !== '/token') {
return;
}
// Only intercept successful responses
if ($response->getStatusCode() !== 200) {
return;
}
$this->logger->info('[OAuth2TokenResponseSubscriber] Token endpoint intercepted');
try {
$content = $response->getContent();
$data = json_decode($content, true);
if (!is_array($data) || !isset($data['access_token'])) {
$this->logger->warning('[OAuth2TokenResponseSubscriber] No access_token in response');
return;
}
$accessToken = $data['access_token'];
// Parse JWT to extract claims and scopes
$parser = new TokenParser(new JoseEncoder());
$token = $parser->parse($accessToken);
if (!$token instanceof \Lcobucci\JWT\UnencryptedToken) {
$this->logger->warning('[OAuth2TokenResponseSubscriber] Token is not unencrypted');
return;
}
$claims = [];
$scopes = [];
$clientId = null;
$allClaims = $token->claims()->all();
foreach ($allClaims as $key => $value) {
if ($key === 'scopes') {
$scopes = is_array($value) ? $value : (array) $value;
} elseif ($key === 'aud') {
// audience = client_id
$clientId = is_array($value) ? $value[0] : $value;
$claims[$key] = $value;
} else {
$claims[$key] = $value;
}
}
$this->logger->info('[OAuth2TokenResponseSubscriber] Scopes:', ['scopes' => $scopes]);
$this->logger->info('[OAuth2TokenResponseSubscriber] Client ID:', ['client_id' => $clientId]);
$this->logger->info('[OAuth2TokenResponseSubscriber] Original sub (email):', ['sub' => $claims['sub'] ?? 'none']);
// Get user from database using email (sub contains email in access_token)
$userEmail = $claims['sub'] ?? null;
if ($userEmail) {
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $userEmail]);
if ($user) {
// Replace sub with user ID for id_token generation
$claims['sub'] = (string) $user->getId();
$this->logger->info('[OAuth2TokenResponseSubscriber] Replaced sub with user ID:', ['sub' => $claims['sub']]);
} else {
$this->logger->warning('[OAuth2TokenResponseSubscriber] User not found for email:', ['email' => $userEmail]);
}
}
// Only generate client_id token if openid scope is requested
if (!in_array('openid', $scopes)) {
$this->logger->info('[OAuth2TokenResponseSubscriber] openid scope not requested, skipping client_id generation');
// Return response without modification
$newResponse = new JsonResponse($data);
$newResponse->headers->set('Content-Type', 'application/json');
$event->setResponse($newResponse);
return;
}
$this->logger->info('[OAuth2TokenResponseSubscriber] Generating id_token...', ['claims' => $claims]);
// Generate id_token with user claims
$idToken = $this->idTokenGenerator->generateIdToken($claims, $scopes);
if ($idToken) {
$this->logger->info('[OAuth2TokenResponseSubscriber] id_token generated successfully');
$data['id_token'] = $idToken;
} else {
$this->logger->warning('[OAuth2TokenResponseSubscriber] Failed to generate id_token');
}
// Return updated response
$newResponse = new JsonResponse($data);
$newResponse->headers->set('Content-Type', 'application/json');
$event->setResponse($newResponse);
} catch (\Exception $e) {
$this->logger->error('[OAuth2TokenResponseSubscriber] Error: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString()
]);
}
}
}