Symfony/ Api platorm/JWT get the current user after login - symfony

Good morning to all
Please i need help. I am using JWT Authentication and all works well.But my problem is to retreive the current user after the login. I saw in the documentation that i can create a controller to do so, but after doing that i get the error of id parameter not given.
Here is my controller related to the user entity
// api/src/Controller/GetMeAction.php
namespace App\Controller;
use App\Entity\User;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
class GetMeAction
{
/**
* #param Security
*/
private $_security;
public function __construct(Security $security)
{
$this->_security = $security;
}
/**
* #Route(
* name="get_me",
* path="get/me",
* methods={"GET"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_item_operation_name"="get_me"
* }
* )
*/
public function __invoke(Request $request): User
{
return $this->_security->getUser();
}
}

Im using symfony 5.3, i wanted to use the api platform normalization and the item operation "get" to keep all the custom config, security, services, ...
So I used the forward() method in a controller :
/**
* #Route("/api/user/me", name="get_me")
*/
public function getMe(): Response
{
$router = $this->get('router')->getRouteCollection()->get('api_users_get_item');
$defaults = $router->getDefaults();
return $this->forward($router->getDefault('_controller'), array_merge($defaults, [ 'id' => $this->getUser()->getId()]));
}

Previous answer is right, but you forgot to Extend you controller from abstract one:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class AdminController extends AbstractController
{
}
If you want to get User in the service, you can Inject Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface in your __construct()
and you can get user like:
public function getUser(): ?User
{
$token = $this->tokenStorage->getToken();
if (!$token) {
return null;
}
$user = $token->getUser();
if (!$user instanceof User) {
return null;
}
return $user;
}

Related

How to return specific data using urls and routing in symfony 4 when making an API GET request?

I'm new to Symfony and trying to learn the basics. I recently saw this question and I wanted to learn how routing works. So I copied the Controller1.php from the question and changed it to UserController.php this:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class UsersController extends AbstractController
{
/**
* #Route("/listOf/Users", methods={"GET"})
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App\Entity\User')->findAll());
}
}
Which indeed, as OP claims, works fine and return the following (manually added data using Sequel Pro) list:
[
{
"id": 14,
"name": "user1 Name"
},
{
"id": 226,
"name": "user2 Name"
},
{
"id": 383,
"name": "user3 Name"
}
]
So my next step was to learn how to adjust this list of users to return a specific user with a given id. So I followed the official Symfony Docs on Routing. So I changed the code to the following:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class UsersController extends AbstractController
{
/**
* #Route("/listOf/Users/{IdUser}", requirements={"IdUser"="\d+"}, methods={"GET"})
* #param Request $request
* #param int $IdUser
* #return JsonResponse
*/
public function list(Request $request, int $IdUser)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App\Entity\User\{IdUser}')->findAll());
}
}
and tried to request the data of the user with the id 14, but this didn't work and yielded the following error:
Class App\Entity\User{IdUser} does not exist (500 Internal Server Error)
What more changes do I need to do to be able to do what I'm trying to do?
This is my User.php entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements \JsonSerializable
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function jsonSerialize()
{
return get_object_vars($this);
}
}
And my UserRepository.php has nothing beside the automatically generated code in it.
Edit: My first request which worked was of the form: http://domainName.local:80/listOf/Users and my second one was: http://domainName.local:80/listOf/Users/14
As promised earlier - here's why it does not work and how to make it work.
Let's examine the code blow:
$this->getDoctrine()->getRepository('App\Entity\User\{IdUser}')->findAll();
Basically you're saying: doctrine, give me the repository that is responsible for handling
the entity App\Entity\User\{IdUser} literally and ofc there is no such entity class.
What you really want is the repo for App\Entity\User.
The string you pass to the getRepository() method always has to be the fully qualified class name of an entity - period.
To ensure you never have any typos here, it's quite helpful to use the class constant of the entity, which looks like so
$repo = $this->getDoctrine()->getRepository(App\Entity\User::class);
Once you have the repository, you can call it's different methods as shown in the doctrine documentation here https://www.doctrine-project.org/api/orm/latest/Doctrine/ORM/EntityRepository.html
In your case, you have the variable $IdUser, which you want to be mapped to the db column/entity property id of the user class.
Since you know that you want exactly this one user with the id 14, all you have to do is tell the repo to find exactly one which looks like this.
// here's the example for your specific case
$user = $repo->findOneBy(['id' => $IdUser]);
// another example could be e.g. to search a user by their email address
$user = $repo->findOneBy(['email' => $email]);
// you can also pass multiple conditions to find*By methods
$user = $repo->findOneBy([
'first_name' => $firstName,
'last_name' => $lastName,
]);
Hopefully, this was more helpful than confusing =)

Symfony 4 How to autowire strings when making a general class?

I would first like to say that I saw the other questions on here relating to this error I'm having and none solved my problems.
I have the following code for a controller to check an APIkey before sending data from the backend to the frontend.
file1Controller.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class file1Controller extends AbstractController
{
/**
* #Route("/Some/URI", methods={"GET"}) // "/Some/URI" here
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:Something')->findAll()); //Something here
}
}
Which works exactly as intended (tested it with Postman and with my browser) for my simple learning example. I would like to generalize it so that I can use it in other places. Almost everything should stay the same except the parts where there are comments. This is what it becomes when making it general:
General.php
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class General extends AbstractController
{
private $route;
private $entity;
/**
* General constructor.
* #param String $route
* #param String $entity
*/
function __construct(String $route, String $entity)
{
$this->route = $route;
$this->entity = $entity;
}
/**
* #Route({$this->route}, methods={"GET"})
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:{$this->entity}')->findAll());
}
}
And the file file1Controller.php changes to:
<?php
namespace App\Controller;
use App\General;
use Symfony\Component\HttpFoundation\Request;
class SubscriptionController
{
/**
* #return General
*/
public function AuthenticateAPI()
{
$generalObject = new General("/Some/URI", 'Something');
return $generalObject;
}
}
This new setup gives no compiler errors but of course, do give the following error (when testing it):
Cannot autowire service "App\General": argument "$route" of method "__construct()" is type-hinted "string", you should configure its value explicitly.
I understand that this error occurs because Symfony doesn't know which String to inject. But there must be a way to get around this? Because I can't specify the value explicitly in my case because I'll be making another file file2Controller.php which will be the exact same but with different $route and $entity.

How can I generalize an ApiKeyAuthenticator in Symfony 4?

I have the following code that checks whether the API-key is the correct one before sending data to the front end.
file1Controller.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class file1Controller extends AbstractController
{
/**
* #Route("/Some/URI", methods={"GET"}) // "/Some/URI" here
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:Something')->findAll()); //Something here
}
}
Which works excatly as intended (tested it with Postman) for my simple learning example. I would like to generalize it so that I can use it in other places. Almost everything should stay the same except the parts where there are comments. I have tried the following:
General.php
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class General extends AbstractController
{
private $request;
private $route;
private $entity;
/**
* ApiKeyAuthenticator constructor.
* #param Request $request
* #param String $route
* #param String $entity
*/
function __construct(Request $request, String $route, String $entity)
{
$this->request = $request;
$this->route = $route;
$this->entity = $entity;
}
/**
* #Route({$route}, methods={"GET"}) //notice here
* #return JsonResponse
*/
public function list()
{
if (empty($this->request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($this->request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:{$this->entity}')->findAll()); //notice here
}
}
Then I change the code of file1Controller.php to:
<?php
namespace App\Controller;
require(__DIR__.'/../General.php'); //note that there's no error accessing the file here
use Symfony\Component\HttpFoundation\Request;
class file1Controller
{
/**
* #param Request $request
*/
public function AuthenticateAPI(Request $request)
{
$AuthenticatorObject = new ApiKeyAuthenticator($request, "/Some/URI", 'Something'); //getting undefiend class
return $AuthenticatorObject;
}
}
This is unfortunately not working when testing it with Postman and I'm getting an undefiend class error on this line $AuthenticatorObject = new ApiKeyAuthenticator($request, "/Some/URI", 'Something'); in file1Controller.php
What did I do wrong and how could I fix it?
You shouldn't call your controllers like this in Symfony:
require(__DIR__.'/../General.php'); //note that there's no error accessing the file here
Please check out defining and accessing controllers as service in Symfony documentation:
How to Define Controllers as Services
How to Forward Requests to another Controller

Symfony: How to save each successful login to database table

i am developing a symfony restful api with tools like
FosRestBundle, FosUserBundle and Lexik JWT for api authentication.
I need to save each successful login in my app. So i created a Login entity
(user_id,loginDate) , but i don't know how to use it because the login in handled from Lexik.
Does anyone know how can i do this?
Thanks
You can use security.interactive_login event for that. More information can be found from the official documentation:
https://symfony.com/doc/current/components/security/authentication.html#authentication-events
Create the listener and register it:
namespace App\EventListener;
use App\Component\EntityManagerAwareTrait;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* #package App\EventListener
*/
class SecuritySubscriber implements EventSubscriberInterface
{
/**
* #param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* #return array
*/
public static function getSubscribedEvents(): array
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
];
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event): void
{
$user = $event->getAuthenticationToken()->getUser();
if ($user instanceof User) {
$user->setLoginDate(new \DateTime());
$this->em->persist($user);
$this->em->flush();
}
}
}

Symfony - Redirect user already logged in

It's possibile with security file config to redirect user already logged in to specific route (e.g homepage) if they try to access on login/register pages? One solution that I already found is to attach a listener to EventRequest, but I prefer to use security config if it's possible.
After some googling I noticed that another solution is to override the fosuserbundle controllers. But because I need that this behavior should works also for /register and /resetting pages, instead to override also those controller, I preferred to use EventListener. Maybe this's the best solution in this case. I'm using Symfony 4, so for the other versions could be different.
My code:
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class LoggedInUserListener
{
private $router;
private $authChecker;
public function __construct(RouterInterface $router, AuthorizationCheckerInterface $authChecker)
{
$this->router = $router;
$this->authChecker = $authChecker;
}
/**
* Redirect user to homepage if tryes to access in anonymously path
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if ($this->authChecker->isGranted('IS_AUTHENTICATED_REMEMBERED') && $this->isAnonymouslyPath($path)) {
$response = new RedirectResponse($this->router->generate('homepage'));
$event->setResponse($response);
}
}
/**
* Check if $path is an anonymously path
* #param string $path
* #return bool
*/
private function isAnonymouslyPath(string $path): bool
{
return preg_match('/\/login|\/register|\/resetting/', $path) ? true : false;
}
}
add this to services.yaml:
App\EventListener\LoggedInUserListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
#Mintendo, I have errors using your code:
request.CRITICAL: Exception thrown when handling an exception (Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException: The token storage contains no authentication token.
php.CRITICAL: Uncaught Exception: The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.
Besides that debug bar also showed error and was broken.
But you pushed me in the right direction, so I have modified your code a little.
And it works without errors now:
<?php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security;
class LoggedInUserListener
{
private $router;
private $security;
public function __construct(RouterInterface $router, Security $security)
{
$this->router = $router;
$this->security = $security;
}
/**
* Redirect user to homepage if tries to access in anonymously path
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if ($this->security->getUser() && $this->isAnonymouslyPath($path)) {
$response = new RedirectResponse($this->router->generate('dashboard'));
$event->setResponse($response);
}
}
/**
* Check if $path is an anonymously path
* #param string $path
* #return bool
*/
private function isAnonymouslyPath(string $path): bool
{
return preg_match('/\/login|\/register|\/resetting/', $path) ? true : false;
}
}

Resources