I'm trying to set a response on an eventsubscriber that checks if an API authorization token it's correct
class TokenSubscriber implements EventSubscriberInterface
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ($controller[0] instanceof TokenAuthenticatedController) {
$apiKey = $this->em->getRepository('AppBundle:ApiKey')->findOneBy(['enabled' => true, 'name' => 'apikey'])->getApiKey();
$token = $event->getRequest()->headers->get('x-auth-token');
if ($token !== $apiKey) {
//send response
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
}
But I cant stop the current request and return a respone as a controller, what is the correct way to send a response with an error message and stop the current request
You can not do that using the FilterControllerEvent Event. On that moment, symfony already decided which controller to execute. I think you might want to look into the Symfony Security component. It can protect routes like what you want, but in a slightly different way (access_control and/or annotations).
If you want to block access to an API (eg. JSON), you easily follow this doc. You can also mix it using the Security annotations on your controllers or actions using this doc
I think you can throw an error here
throw new AccessDeniedHttpException('Your message here!');
Related
Consider the following request to a Symfony controller:
http http://127.0.0.1:8000/index x-token:1000
#[Route('/index', name: 'index')]
public function index(HttpClientInterface $client, Request $request): Response
{
$client->request('GET', 'http://0.0.0.0:3001', ['headers' => ['x-token' => $request->headers->get('x-token')]]);
return new JsonResponse();
}
This code snippet is a minimal example for the usage in a controller. The controller accepts a Request, and uses the x-token header for authenticating against the 3rd Party Api (here: localhost:3001).
Is there a way, to automate this process? So basically - listen to incoming requests and inject the x-token header into a specific Scoped Client or the default client in Symfony.
The goal is, not to do this in every usage of the Http Client, but have a configured client service.
The client will be used all over the codebase, not just in a controller like in this minimal example.
I know that I can use Service Decoration and extend the clients in use. I fail how to connect the dots and make this work.
Have you tried using symfony kernel events?
First of all, if you are calling some 3rd-party api, I'd suggest you to create a separate class at the infrastructure layer, for example MyApiProvider. Using HttpClient right from your controller is not smart, because you may also want to adjust something (for example api host, etc). So it's gonna look like this:
<?php
namespace App\Infrastructure\Provider;
class MyApiProvider
{
// Of course, this also be better configurable via your .env file
private const HOST = 'http://0.0.0.0:3001';
private HttpClientInterface $client;
private ?string $token = null;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function getSomething(): array
{
$response = $this->client->request(
'GET',
self::HOST,
['headers' => $this->getHeaders()]
);
return $response->toArray();
}
private function getHeaders(): array
{
$headers = [];
if ($this->token !== null) {
$headers['x-token'] = $this->token;
}
return $headers;
}
}
Then you need to use symfony's kernel.request event to inject token to your provider from the request:
<?php
namespace App\Event;
use Symfony\Component\HttpKernel\Event\KernelEvent;
class RequestTokenEventListener
{
private MyApiProvider $provider;
public function __construct(MyApiProvider $provider)
{
$this->provider = $provider;
}
public function onKernelController(KernelEvent $event): void
{
$request = $event->getRequest();
$token = $request->headers->get('x-token');
if ($token !== null) {
$this->provider->setToken($token);
}
}
}
And finally your controller:
#[Route('/index', name: 'index')]
public function index(MyApiProvider $provider): Response
{
$provider->getSomething();
return new JsonResponse();
}
So your provider is gonna have token context during each request, if the token is passed.
I am working on a new Symfony 5.3.6 project and want to implement authentication, based on the new system as stated in:
https://symfony.com/doc/current/security/authenticator_manager.html#creating-a-custom-authenticator
I do not have any users and just want to check if the sent api token is correct, so when implementing this method:
public function authenticate(Request $request): PassportInterface
{
$apiToken = $request->headers->get('X-AUTH-TOKEN');
if (null === $apiToken) {
// The token header was empty, authentication fails with HTTP Status Code 401 "Unauthorized"
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(new UserBadge($apiToken));
}
where exactly is the checking done? Have i forgotten to implement another Class somewhere?
If I leave the code as is it lands directly in onAuthenticationFailure.
I understand, that I could implement Users/UserProvider with an attribute $apiToken and then the system would check if the database entry corresponds with the token in the request. But i do not have users.
It should be possible without having users, because on the above URL, it says:
Self Validating Passport
If you don’t need any credentials to be checked (e.g. when using API
tokens), you can use the
Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport.
This class only requires a UserBadge object and optionally Passport
Badges.
But that is a little thin. How do I "use" it?
Ok, I think I got the point, in any case, you need to handle some User & then you need to create a customer Userprovider.
Here my logic:
App\Security\UserProvider:
class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
public function loadUserByIdentifier($identifier): UserInterface
{
if ($identifier === 'YOUR_API_KEY') {
return new User();
}
throw new UserNotFoundException('API Key is not correct');
}
...
App\Security\ApiKeyAuthenticator:
class ApiKeyAuthenticator extends AbstractAuthenticator
{
private UserProvider $userProvider;
public function __construct(UserProvider $userProvider)
{
$this->userProvider = $userProvider;
}
public function supports(Request $request): ?bool
{
// allow api docs page
return trim($request->getPathInfo(), '/') !== 'docs';
}
public function authenticate(Request $request): Passport
{
$apiToken = $request->headers->get('X-API-KEY');
if (null === $apiToken) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(
new UserBadge($apiToken, function () use ($apiToken) {
return $this->userProvider->loadUserByIdentifier($apiToken);
})
);
}
It works for me, my API is protected by a basic API Key in the header. I don't know if it's the best way, but seems ok.
And define in your security.yaml:
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
id: App\Security\UserProvider
You can use next validation
return new SelfValidatingPassport(
new UserBadge($apiToken, function() use ($apiToken) {
// TODO: here you can implement any check
})
);
Using Symfony 4.4, I'd like to add on the fly to all my a route parameter if it's found in the on request. E.g. for the user Id:
PUT | DELETE | POST mywebsite.com/users/{userid}/some-action
I could add my users ids each time I log but it's kind of cumbersome.
So I created a service:
//src/
// |__Services/
// |___UserProcessor.php
use Monolog\Processor\ProcessorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
// In my config.yaml this monolog.processor
final class UserProcessor implements ProcessorInterface
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function __invoke(array $record): array
{
dd($this->requestStack->getMasterRequest()->attributes);
}
}
When I run my postman on POST /users/{userid}/some-action I get this output:
[
"media_type" => "application/json"
]
From my understanding, symfony route parameters request attributes are not built yet at the moment my processor runs.
What should I do to make my processor access the attribute userid?
Check how the Symfony\Bridge\Monolog\Processor\RouteProcessor does it:
It's implemented as an EventSubscriber
class RouteProcessor implements EventSubscriberInterface, ResetInterface
{
private $routeData;
private $includeParams;
public function __construct(bool $includeParams = true)
{
$this->includeParams = $includeParams;
$this->reset();
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['addRouteData', 1],
KernelEvents::FINISH_REQUEST => ['removeRouteData', 1],
];
}
// rest of the implementation
}
During KernelEvents::REQUEST it adds the required request data (including request parameters, if so desired) to the internal state of the object, so when the processor is run it can access the data from the internal RouteProcessor::$routeData, and not from the request directly.
public function addRouteData(RequestEvent $event)
{
if ($event->isMainRequest()) {
$this->reset();
}
$request = $event->getRequest();
if (!$request->attributes->has('_controller')) {
return;
}
$currentRequestData = [
'controller' => $request->attributes->get('_controller'),
'route' => $request->attributes->get('_route'),
];
if ($this->includeParams) {
$currentRequestData['route_params'] = $request->attributes->get('_route_params');
}
$this->routeData[spl_object_id($request)] = $currentRequestData;
}
You could modify this approach to suit yourself, or even just use this processor directly (although it adds more data than what you are looking for in your question).
I am making simple api wrapper, so all requests to https://example.comA/api/me must be catched on kernel response level and forwarded to https://api.example.comB/me and all was fine however I cannot get the currently logged in User in that kernel response because it returns null:
namespace App\Manager\Api\Event;
use App\Provider\Core\Api\CoreApi;
use GuzzleHttp\Exception\BadResponseException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\Security\Core\Security;
class ApiWrapperEventListener
{
private $coreApi;
private $security;
public function __construct(CoreApi $coreApi, Security $security)
{
$this->coreApi = $coreApi;
$this->security = $security;
}
public function onKernelResponse(ResponseEvent $event)
{
if (!$event->isMasterRequest()) return;
$request = $event->getRequest();
if ('/api' === substr($request->getPathInfo(), 0, 4)) {
dump($this->security->getUser()); // returns NULL
die;
try {
$response = $this->coreApi->call($request->getMethod(), $request->getPathInfo(), json_decode($request->getContent(), true) ?? []);
$event->setResponse(new JsonResponse($response));
} catch (BadResponseException $error) {
dump($error);
die;
}
}
}
}
I guess Symfony is firing those events before I get the User, is there a way to get this right?
I have to note that in other places like controllers or services I get the User right.
Ok i know what was the problem.
The response controller event has no User when 404 or 500 given.
In my case I was catching 404, passing to listener and modifying the request to 200.
This approach wasn't good, so i decided to move this to Controller itself.
/**
* #Route("/api/v1/{uri}", name="api", requirements={"uri"=".+"})
*/
public function api(
Request $request,
CoreApi $coreApi
):Response
{
try
{
$response = $coreApi->call($request->getMethod(), $request->getPathInfo(), json_decode($request->getContent(), true) ?? []);
return new JsonResponse($response);
}
catch(BadResponseException $error)
{
return new Response($error->getResponse()->getBody()->getContents(), $error->getResponse()->getStatusCode());
}
}
I have implemented following code to run a code on before any action of any controller. However, the beforeFilter() function not redirecting to the route I have specified. Instead it takes the user to the location where the user clicked.
//My Listener
namespace Edu\AccountBundle\EventListener;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class BeforeControllerListener
{
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
{
//not a controller do nothing
return;
}
$controllerObject = $controller[0];
if (is_object($controllerObject) && method_exists($controllerObject, "beforeFilter"))
//Set a predefined function to execute Before any controller Executes its any method
{
$controllerObject->beforeFilter();
}
}
}
//I have registered it already
//My Controller
class LedgerController extends Controller
{
public function beforeFilter()
{
$commonFunction = new CommonFunctions();
$dm = $this->getDocumentManager();
if ($commonFunction->checkFinancialYear($dm) == 0 ) {
$this->get('session')->getFlashBag()->add('error', 'Sorry');
return $this->redirect($this->generateUrl('financialyear'));//Here it is not redirecting
}
}
}
public function indexAction() {}
Please help, What is missing in it.
Thanks Advance
I would suggest you follow the Symfony suggestions for setting up before and after filters, where you perform your functionality within the filter itself, rather than trying to create a beforeFilter() function in your controller that is executed. It will allow you to achieve what you want - the function being called before every controller action - as well as not having to muddy up your controller(s) with additional code. In your case, you would also want to inject the Symfony session to the filter:
# app/config/services.yml
services:
app.before_controller_listener:
class: AppBundle\EventListener\BeforeControllerListener
arguments: ['#session', '#router', '#doctrine_mongodb.odm.document_manager']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Then you'll create your before listener, which will need the Symony session and routing services, as well as the MongoDB document manager (making that assumption based on your profile).
// src/AppBundle/EventListener/BeforeControllerListener.php
namespace AppBundle\EventListener;
use Doctrine\ODM\MongoDB\DocumentManager;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use AppBundle\Controller\LedgerController;
use AppBundle\Path\To\Your\CommonFunctions;
class BeforeControllerListener
{
private $session;
private $router;
private $documentManager;
private $commonFunctions;
public function __construct(Session $session, Router $router, DocumentManager $dm)
{
$this->session = $session;
$this->router = $router;
$this->dm = $dm;
$this->commonFunctions = new CommonFunctions();
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof LedgerController) {
if ($this->commonFunctions->checkFinancialYear($this->dm) !== 0 ) {
return;
}
$this->session->getFlashBag()->add('error', 'Sorry');
$redirectUrl= $this->router->generate('financialyear');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
If you are in fact using the Symfony CMF then the Router might actually be ChainRouter and your use statement for the router would change to use Symfony\Cmf\Component\Routing\ChainRouter;
There are a few additional things here you might want to reconsider - for instance, if the CommonFunctions class needs DocumentManager, you might just want to make your CommonFunctions class a service that injects the DocumentManager automatically. Then in this service you would only have to inject your common functions service instead of the document manager.
Either way what is happening here is that we are checking that we are in the LedgerController, then checking whether or not we want to redirect, and if so we overwrite the entire Controller via a callback. This sets the redirect response to your route and performs the redirect.
If you want this check on every single controller you could simply eliminate the check for LedgerController.
.
$this->redirect() controller function simply creates an instance of RedirectResponse. As with any other response, it needs to be either returned from a controller, or set on an event. Your method is not a controller, therefore you have to set the response on the event.
However, you cannot really set a response on the FilterControllerEvent as it is meant to either update the controller, or change it completely (setController). You can do it with other events, like the kernel.request. However, you won't have access to the controller there.
You might try set a callback with setController which would call your beforeFilter(). However, you wouldn't have access to controller arguments, so you won't really be able to call the original controller if beforeFilter didn't return a response.
Finally you might try to throw an exception and handle it with an exception listener.
I don't see why making things this complex if you can simply call your method in the controller:
public function myAction()
{
if ($response = $this->beforeFilter()) {
return $response;
}
// ....
}
public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
$response = new Response();
// Matched route
$_route = $request->attributes->get('_route');
// Matched controller
$_controller = $request->attributes->get('_controller');
$params = array(); //Your params
$route = $event->getRequest()->get('_route');
$redirectUrl = $url = $this->container->get('router')->generate($route,$params);
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
Cheers !!