1

I'm building an OAuth2 server with the great FOSOAuthServerBundle.

My Symfony app authentifies users coming from another source (actually another server).

I have created a custom UserProvider which works fine with the default Symfony security layer. I have my implementation of UserInterface but it's obviously not an Entity of the database.

Implementing the classes for AccessToken, RefreshToken & AuthCode as told in the installation instructions for Doctrine, I defined the user reference as

//...
/**
 * @ORM\Column(type="integer")
 */
protected $user_id;
//...

Does it seem logical until there ?

Now the true question is: Where do I specify the way to link my users to the different tokens ?


I've also been asking the question on the official repo for visibility purpose

1 Answer 1

1

I ended up doing 2 things:

  • Overriding the tokens setter & getter for the property user
  • Using a Doctrine LifecycleEvent listener

First extend the token classes (for example with a Trait)

namespace AppBundle\Entity;

use AppBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;

trait AppTokenTrait
{
    /**
     * @ORM\Column(type="string")
     * @var string
     */
    protected $username;

    public function setUser(UserInterface $user)
    {
        if (!$user instanceof User) {
            throw new \InvalidArgumentException(
                sprintf("User must be an instance of %s", User::class)
            );
        }

        $this->username = $user->getUsername();
        $this->user = $user;
    }

    public function getUser()
    {
        if (!$this->user) {
            throw new \RuntimeException(
                "Unable to get user - user was not loaded by postLoad"
            );
        }

        return $this->user;
    }

    /**
     * @return string
     */
    public function getUsername()
    {
        return $this->username;
    }
}

In the 3 token classes (And don't forget to update the database schema):

//...
    use AppTokenTrait;
//...

Create the listener

namespace AppBundle\Services;

use AppBundle\Entity\AccessToken;
use AppBundle\Entity\AuthCode;
use AppBundle\Entity\RefreshToken;
use AppBundle\Security\UserProvider;
use Doctrine\ORM\Event\LifecycleEventArgs;

class AppTokenListener
{
    /**
     * @var UserProvider
     */
    protected $userProvider;

    public function __construct(UserProvider $userProvider)
    {
        $this->userProvider = $userProvider;
    }

    public function postLoad(LifecycleEventArgs $event)
    {
        $object = $event->getObject();

        if (
            $object instanceof AccessToken or
            $object instanceof AuthCode or
            $object instanceof RefreshToken
        ) {
            $user = $this->userProvider->loadUserByUsername($object->getUsername());
            $object->setUser($user);
        }
    }
}

Register it as a service

app.token.listener:
    class: AppBundle\Services\AppTokenListener
    tags:
        - { name: doctrine.event_listener, event: postLoad }

The user object should now always be attached to the tokens

Sign up to request clarification or add additional context in comments.

4 Comments

Hi Pierre, your post really helped me setting up a similar configuration. I have everything up and running from the oauth side, but calling my api with the access token leads in a fatal error: "Error: Call to a member function loadUserByUsername() on a non-object" at ***/OAuthBundle/Services/SecurityTokenListener.php line 41, where line 41 is $user = $this->userProvider->loadUserByUsername($object->getUsername());" It seems, that the constructor is never called. Any idea, what's going on here? Thanks, Chris
All I remember having here is a circular reference issue when injecting the userProvider into the listener, and I saw this was a known issue that I workedaround in a quite dirty way
Got it now, the service just needs the user provider as an argument and your AppTokenListener is missing a "_" at the constructor. Copy/Paste isn't a good idea:-)
How did you get past this circular reference thing?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.