-
-
Notifications
You must be signed in to change notification settings - Fork 897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Best Practice for adding a /me route #477
Comments
I solved this question by adding a simple eg. if you use a Bearer token to authenticate the user : api_me:
path: /me
methods: [GET]
defaults: { _controller: AppBundle:Me:get } <?php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* Class MeController
* @package AppBundle\Controller
*/
class MeController extends Controller
{
/**
* @param Request $request
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function getAction(Request $request)
{
if (!$authorization = $request->headers->get('Authorization')) {
// return an exception or a response
}
list($prefix, $token) = array_pad(explode(' ', $authorization, 2), 2, null);
if ('Bearer' !== $prefix) {
// do some things
}
// get your decoder eg: a service
$decoder = $this->get('my.decoder');
// decode the token
if(!$tokenDecoded = $decoder->decode($token)) {
// do some things or a exception:
// eg. throw new AuthenticationCredentialsNotFoundException();
}
// get the user with token infos
$user = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:User')
->findOneBy(['username' => $tokenDecoded['username']);
// redirect the user to the resource
$request->attributes->add(['_resource' => 'User']);
return $this->forward('DunglasApiBundle:Resource:get', [
'request' => $request,
'id' => $user->getId()
]);
}
} if the user ID is stored in the token information, you don't need to use EntityManager. |
@yelmontaser Ah.. so you basically side-stepped the api-platform ? Did you try implementing a solution within the platform and gave up ? |
You should provide an ID as it's not optional. A solution is to provide a fake id ( But be careful, creating endpoint like |
@dunglas That makes sense to me. However im not looking to override the /api/users/{id} endpoint.. at the moment I have been able to create a custom controller and service.. the documentation appears as such
I havent implemented the controller just yet.. however because I am authenticated im sure I can fetch the id out of the token and forward to the /api/users/{id} endpoint. is that a secure enough solution ? I guess another option is to expose the oauth entities. I could expose the AccessToken entity since I have an access token.. to return the user via AccessToken. Would this be a better alternative ? |
Is there an example of this ? |
I added both an AccessToken resource as well as a custom get service. http://i.imgur.com/nnpaTAC.png Interestingly.. I added the @Security annotation to the custom service controller..and it added the little 'keys' icon denoting that the method requires authentication. The other methods don't have that though a valid oauth token is required to access any of the service endpoints. Currently the AccessToken resource requires a numeric id.. this needs to be converted to a string identifier. It doesn't really make sense that since you need an access_token to access any of the services.. that you should need an AccessToken resource. Since in order to access /api/me requires a valid access_token.. and the token only lasts an hour.. the /api/me custom service seems like the best approach for this scenario. Thus.. here is my controller for the /api/me endpoint
thanks @yelmontaser for that bit at the end. @dunglas Your thoughts ? |
Im also wondering.. if its possible to have that little 'keys' icon.. appear next to all the methods ? |
@jayesbe already implemented but not on the path of the resource as The use the annotation |
@jayesbe if you extend class APIController extends ResourceController
{
/**
* retrieve the authenticated user
*
* @Security("is_granted('IS_AUTHENTICATED_FULLY') and user.getEmailVerified()")
*/
public function meAction(Request $request)
{
$request->attributes->add(['_resource' => 'User']);
return $this->getAction($request, $this->getUser()->getId());
}
} |
@yelmontaser thanks for the tip. |
@yelmontaser @jayesbe What about writing a bit of documentation for this and afterwards close this issue? |
I don't mind adding some doc, where is the best place to add it ? |
for the record, I solved this in a different manner with api platform 2: <?php
class UserSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['resolveMe', EventPriorities::PRE_READ],
];
}
public function resolveMe(GetResponseEvent $event)
{
$request = $event->getRequest();
if ('api_users_get_item' !== $request->attributes->get('_route')) {
return;
}
if ('me' !== $request->attributes->get('id')) {
return;
}
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof User) {
return;
}
$request->attributes->set('id', $user->getId());
}
} |
I also looked for a way to get the id so that a logged in user can access its profile. As a /me route seems to be not cachable and I use LexikJWTAuthenticationBundle there is no need for a route like this. The bundle can send additional data beside the token. (https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/2-data-customization.md#eventsjwt_authenticated---customizing-your-security-token) <?php
namespace App\AppBundle\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use App\AppBundle\Model\UserInterface;
class AuthenticationSuccessListener
{
public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $event)
{
$data = $event->getData();
$user = $event->getUser();
if (!$user instanceof UserInterface) {
return;
}
$data['id'] = $user->getId();
$event->setData($data);
}
} or put it directly in the token: <?php
namespace App\AppBundle\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use App\AppBundle\Model\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class JWTCreatedListener
{
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
* @param TokenStorageInterface $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* @param JWTCreatedEvent $event
*
* @return void
*/
public function onJWTCreated(JWTCreatedEvent $event)
{
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof UserInterface) {
return;
}
$payload = $event->getData();
$payload['id'] = $user->getId();
$event->setData($payload);
}
} |
Solution posted by @lyrixx didn't fit me because it doesn't allow you to set different serialization groups on "get" and "get_me" operations. So my solution based on custom actions: <?php
namespace App\Entity;
use App\Controller\Action\GetMeAction;
/**
* @ApiResource(
* itemOperations={
* "get"={
* "requirements"={"id"="\d+"}
* },
* "get_me"={
* "method"="GET",
* "path"="/users/me",
* "controller"=GetMeAction::class,
* "openapi_context"={
* "parameters"={}
* },
* "read"=false
* }
* }
* )
*/
class User <?php
namespace App\Controller\Action;
use App\Entity\User;
/**
* Class GetMeAction
*/
final class GetMeAction extends AbstractController
{
/**
* @return User
*/
public function __invoke(): User
{
/** @var User $user */
$user = $this->getUser();
return $user;
}
} It allows you to set different serialization groups or even disable "get" operation. The only one restriction is that users must have integer ids. Hope it helps someone. |
Another solution for this is to catch |
@soyuka could you please elaborate on your "transform it to an identifier via the identifier denormalizer" solution? Thanks. |
My gist is all you need, I consider this really hackish as it transforms the |
Do you think there is some more correct way how to do this which is not an overkill? I think I went through all the docs and relevant SymfonyCasts videos but didn't figure out how to do this in a standardized way. Thanks. |
The problem with this, is stateless impersonation. Because the original user's id is stored inside the token how do you know wich user you are impersonating? Updating the token isn't possible so you must generate a new one, but that would be weird in case of impersonation. |
@SherinBloemendaal @dunglas is right about REST rules and especially cache mechanisms. For example, user A requests the resource /resource/me. The server responds with data owned by user A. The response is cached by some cache layers (can be your browser, varnish or something else). The user B requests the same resource /resource/me. The first cached response will be returned to user B, which contains data related to user A. |
But we agree cache mechanisms do not handle only the query string, but the Headers too? Because, user A can have the rights to see /resource/123, but user B can't see it (ACL for example). In this case, we hope the cache system will not serve the /resource/123 to user B because user A built the cache by accessing before. Or you can't use any cache with web applications which rely on policies, securities... I am very curious about this thread, about REST principles. |
I managed to do it without custom controller (not recommanded now) : In the User entity : #[ApiResource(
itemOperations: [
'get',
'me' => [
'method' => 'GET',
'path' => '/me',
'defaults' => [
'id' => 0,
],
],
],
)]
class User {
// ...
} Then I use a custom DataProvider : <?php
namespace App\DataProvider;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\User;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Security;
class UserMeDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface
{
public function __construct(private Security $security)
{
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return 'me' === $operationName && User::class === $resourceClass;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): User
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException();
}
return $user;
}
} Also, if I set the 'get' before 'me' in itemOperations attribute, I will have this response (for both {
"@type": "User",
"@id": "/api/users/2"
} Otherwise, I get : {
"@type": "User",
"@id": "/api/me?id=2"
} |
Is there something wrong at setting a cookie on success login with the id of the user and then, on client side, just using this cookie value to get /api/id ? |
@storiesontheway A save way would be to store the JWT in a split cookie: https://medium.com/lightrail/getting-token-authentication-right-in-a-stateless-single-page-application-57d0c6474e3 Lexik Bundle has implemented this. |
To complete the comment of @lyrixx, here's how I've done it with API Platform v3.1.
<?php
declare(strict_types=1);
namespace App\EventListener;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Put;
use App\Entity\User\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
#[AsEventListener(KernelEvents::REQUEST, priority: 5)]
final class UserListener
{
private const ME_IDENTIFIER = 'me';
public function __construct(private readonly Security $security)
{
}
public function __invoke(RequestEvent $event): void
{
if (! $event->isMainRequest()) {
return;
}
$request = $event->getRequest();
/** @var Operation $operation */
$operation = $request->attributes->get('_api_operation');
if (! $operation instanceof Get
&& ! $operation instanceof Put
&& ! $operation instanceof Patch
) {
return;
}
if (! is_a($operation->getClass(), User::class, true)) {
return;
}
if (self::ME_IDENTIFIER !== $request->attributes->get('id')) {
return;
}
/** @var User|null $user */
$user = $this->security->getUser();
if (! $user instanceof User) {
return;
}
$request->attributes->set('id', $user->getId());
}
} What about documenting this in the docs ? |
I wouldn't add this to the docs, as it is not RESTfull. |
How about returning a redirect response that redirects the user to the correct RESTful endpoint? GET /users/me => 302 GET /users/123 ??? I know that this wouldn't work for POST/PUT/PATCH, but you can use the GET endpoint to identify where to send the write operations to. |
As of today, I have implemented a |
Not adding this to the docs without any mentioning is not a good DX - needed to come through multiple google searches -> stackoverflow -> this github issue. Even though this is not restful this is still a very often used convention, at least mentioning how to implement something like that - or why not to do this and how to solve it better - should go to the docs. |
|
I am trying to figure out something that unfortunately I haven't been able to find an example for.
I have a very simple API that currently only exposes one route
If I pass an ID to the route.. I get back a User object.. Great!
Now I have an app that is using oauth to authenticate.. get access_token, refresh_token, etc.
I now have an access token I can use to access the API. Except.. I don't know my own ID. The authentication call doesnt return it.. it only returns an access_token.
I want to create a route
is this the correct way to do this ? Would I only need a custom data provider? do I need to implement a full custom controller ?
trying to understand the best approach, best practice for this scenario.
The text was updated successfully, but these errors were encountered: