Skip to content

Best Practice for adding a /me route #477

@jayesbe

Description

@jayesbe

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.

Activity

yecraftsman

yecraftsman commented on Mar 19, 2016

@yecraftsman

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

jayesbe commented on Mar 19, 2016

@jayesbe
Author

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

dunglas

dunglas commented on Mar 19, 2016

@dunglas
Member

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

jayesbe commented on Mar 19, 2016

@jayesbe
Author

@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

jayesbe commented on Mar 19, 2016

@jayesbe
Author

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

jayesbe commented on Mar 19, 2016

@jayesbe
Author

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

jayesbe commented on Mar 19, 2016

@jayesbe
Author

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

yecraftsman

yecraftsman commented on Mar 19, 2016

@yecraftsman

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

yecraftsman

yecraftsman commented on Mar 19, 2016

@yecraftsman

@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

jayesbe commented on Mar 19, 2016

@jayesbe
Author

@yelmontaser thanks for the tip.

robertfausk

robertfausk commented on Apr 3, 2016

@robertfausk

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

jayesbe

jayesbe commented on Apr 3, 2016

@jayesbe
Author

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

soyuka

soyuka commented on Apr 4, 2016

@soyuka
Member

2 remaining items

remoteclient

remoteclient commented on Mar 19, 2018

@remoteclient
Contributor

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

Retunsky commented on Feb 20, 2020

@Retunsky

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

waspinator commented on Apr 27, 2020

@waspinator

@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

soyuka commented on Jun 10, 2020

@soyuka
Member

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

18 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @dunglas@waspinator@johnnypea@lyrixx@teohhanhui

        Issue actions

          Best Practice for adding a /me route · Issue #477 · api-platform/core