Core: Best Practice for adding a /me route

Created on 19 Mar 2016  路  23Comments  路  Source: api-platform/core

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.

question

Most helpful comment

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

All 23 comments

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.

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

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

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 ?

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 ?

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

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

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

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.

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

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

@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 me string to the user id from the connected user.

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.

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.

What @dunglas said about breaking REST rules, this is not correct, according to Roy Thomas Fielding's dissertation, _any information that can be named can be a resource_:

5.2.1.1 Resources and Resource Identifiers
The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g. a person), and so on. In other words, any concept that might be the target of an author's hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time. [...]

source

I think its not bad at all to create a custom controller and register it like any other authentication endpoint (/register or /login_check or /forgot_password).

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

Was this page helpful?
0 / 5 - 0 ratings