Skip to content
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

Closed
jayesbe opened this issue Mar 19, 2016 · 32 comments
Closed

Best Practice for adding a /me route #477

jayesbe opened this issue Mar 19, 2016 · 32 comments

Comments

@jayesbe
Copy link

jayesbe commented Mar 19, 2016

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

/api/users/{id}

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

/api/users/me

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.

@yelmontaser
Copy link

I solved this question by adding a simple MeController

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.

@jayesbe
Copy link
Author

jayesbe commented Mar 19, 2016

@yelmontaser Ah.. so you basically side-stepped the api-platform ? Did you try implementing a solution within the platform and gave up ?

@dunglas
Copy link
Member

dunglas commented Mar 19, 2016

You should provide an ID as it's not optional. A solution is to provide a fake id (me for instance).

But be careful, creating endpoint like /api/users/me isn't stateless, can create cache problem if you rely on some reverse proxies and break the REST pattern. It's better from a design POV to keep with /api/users/{id} and for instance use the Symfony Security Component to ensure that only the current user can access the endpoint corresponding to its id.

@jayesbe
Copy link
Author

jayesbe commented Mar 19, 2016

@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

GET /api/me Get Authenticated User
GET /api/users/{id} Retrieves User resource.

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 ?

@jayesbe
Copy link
Author

jayesbe commented Mar 19, 2016

use the Symfony Security Component to ensure that only the current user can access the endpoint corresponding to its id.

Is there an example of this ?

@jayesbe
Copy link
Author

jayesbe commented Mar 19, 2016

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

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->forward('DunglasApiBundle:Resource:get', ['request' => $request, 'id' => $this->getUser()->getId()]); 
    }
}

thanks @yelmontaser for that bit at the end.

@dunglas Your thoughts ?

@jayesbe
Copy link
Author

jayesbe commented Mar 19, 2016

Im also wondering.. if its possible to have that little 'keys' icon.. appear next to all the methods ?

@yelmontaser
Copy link

@jayesbe already implemented but not on the path of the resource as /api/users/me rather /me like Facebook https://graph.facebook.com/me it's 100% stateless.

The use the annotation @Security is a good idea and a best practices.

@yelmontaser
Copy link

@jayesbe if you extend ResourceController you don't need forward.

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()); 
    }
}

@jayesbe
Copy link
Author

jayesbe commented Mar 19, 2016

@yelmontaser thanks for the tip.

@robertfausk
Copy link

@yelmontaser @jayesbe What about writing a bit of documentation for this and afterwards close this issue?

@jayesbe
Copy link
Author

jayesbe commented Apr 3, 2016

I don't mind adding some doc, where is the best place to add it ?

@soyuka
Copy link
Member

soyuka commented Apr 4, 2016

@lyrixx
Copy link
Contributor

lyrixx commented Dec 21, 2017

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());
    }
}

@remoteclient
Copy link
Contributor

remoteclient commented Mar 19, 2018

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)
Like described there, I set up a listener which includes the users id in the response:

<?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);
    }
}

@Retunsky
Copy link

Retunsky commented Feb 20, 2020

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.

@waspinator
Copy link

waspinator commented Apr 27, 2020

@Retunsky this seems to change the hydra member @id parameter to match the path. Do you know how to keep the /users/1 IRI path instead of changing to /users/me?id=1

I see my problem now. The order of the "get" annotations is important. works now as expected.

@soyuka
Copy link
Member

soyuka commented Jun 10, 2020

Another solution for this is to catch me and transform it to an identifier via the identifier denormalizer: https://gist.github.com/soyuka/c25aa66728439eec56419a5a20295592

@johnnypea
Copy link

@soyuka could you please elaborate on your "transform it to an identifier via the identifier denormalizer" solution? Thanks.

@soyuka
Copy link
Member

soyuka commented Oct 19, 2020

My gist is all you need, I consider this really hackish as it transforms the me string to the user id from the connected user.

@johnnypea
Copy link

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.

@SherinBloemendaal
Copy link

SherinBloemendaal commented Nov 20, 2020

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)
Like described there, I set up a listener which includes the users id in the response:

<?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);
    }
}

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.

@walva
Copy link
Contributor

walva commented Dec 31, 2020

@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.
One way to avoid such behaviour is disabling the cache mechanisms, which decrease performance and increase consumed resources (which is one of the best feature of API Platform). Because of this, it breaks the REST rules. One of the main reason of REST rules existence is to ensure cache mechanisms.

@vince83110
Copy link

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.

@coolmic
Copy link

coolmic commented Jan 13, 2022

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 /api/me and /api/users/2 ) :

{
    "@type": "User",
    "@id": "/api/users/2"
}

Otherwise, I get :

{
    "@type": "User",
    "@id": "/api/me?id=2"
}

@r-laferte
Copy link

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 ?
We can give this cookie a long lifetime value and it seems to be at no risk for me.
What do you think?

@remoteclient
Copy link
Contributor

@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.

@benjaminmal
Copy link

benjaminmal commented Mar 31, 2023

To complete the comment of @lyrixx, here's how I've done it with API Platform v3.1.

⚠️ Note the priority here: The listener must be called after the RouteListener to have the id in attributes but before the ReadListener.

<?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 ?

@remoteclient
Copy link
Contributor

I wouldn't add this to the docs, as it is not RESTfull.

@toby-griffiths
Copy link
Contributor

toby-griffiths commented Jan 24, 2024

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.

@SherinBloemendaal
Copy link

As of today, I have implemented a /me route using a simple state provider that directly retrieves the user from the Symfony SecurityBundle. Although the /me call does not adhere to RESTful principles, an alternative could be to extract the subject claim from the JWT token, if JWT is being used.

@kingschnulli
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests