Saeven / zf3-circlical-user

Turnkey Authentication, Identity, and RBAC for Laminas and Zend Framework 3. Supports Doctrine and Middleware.
Mozilla Public License 2.0
36 stars 15 forks source link

Better documentation for User API Tokens #84

Open CreativeNative opened 3 years ago

CreativeNative commented 3 years ago

I don't get it how to use the User API Tokens and the tests doesn't really help. To my User Entity I added

/**
  * @ORM\OneToMany(targetEntity="CirclicalUser\Entity\UserApiToken", mappedBy="user");
  * @var ArrayCollection
  */
private iterable $apiTokens;

public function __construct()
{
    $this->roles = new ArrayCollection();
    $this->apiTokens = new ArrayCollection();
}

public function getApiTokens(): ?Collection
{
    return $this->apiTokens;
}

How do I get the token because like this it doesn't work. $token = $this->userApiTokenMapper->get('d0cad39b-f269-405e-b3f9-d45b349c0587');

I think I'm missing a configuration. Do I?

I got also an error by doctrine validation.

[FAIL] The entity-class TmiUser\Entity\User mapping is invalid:

Saeven commented 3 years ago

Hello!

The relationship on the User entity would look something like this:

    /**
     * @ORM\OneToMany(targetEntity="CirclicalUser\Entity\UserApiToken", mappedBy="user", cascade={"all"});
     */
    private $api_tokens;

You would also likely build some getters/setters on the User, like so:

    public function getApiTokens(): ?Collection
    {
        return $this->api_tokens;
    }

    public function addApiToken(UserApiToken $token): void
    {
        $this->api_tokens->add($token);
    }

    public function getApiTokenWithId(string $uuid): ?UserApiToken
    {
        foreach ($this->api_tokens as $token) {
            if ($token->getToken() === $uuid) {
                return $token;
            }
        }
        return null;
    }

In the primary platform I've developed that uses tokens, the admin area has an action that allows people with necessary rights to create tokens:

    public function createTokenAction(): JsonModel
    {
        return $this->json()->wrap(function () {

            if (!$this->auth()->isAllowed($this->getPluralType(), AccessModel::ACTION_AUTHOR)) {
                throw new NoAccessException();
            }

            /** @var User $user */
            $user = $this->auth()->requireIdentity();

            $token = new UserApiToken($user, ApiScopeInterface::API_SCOPE_CONTEST);
            $this->userApiTokenMapper->getEntityManager()->persist($token);
            $user->addApiToken($token);
            $this->userMapper->update($user);

            return [
                'token' => $token->getUuid()->toString(),
            ];
        });
    }

Tokens are indeed fetched via logic such as this:

$tokenParameter = $this->params()->fromPost('auth_api_token');
$token = $this->userApiTokenMapper->get($tokenParameter);

Lib is being used in some prod apps that are working very well! Should be able to get it going 👍🏼

Saeven commented 2 years ago

Hey @CreativeNative! Wanted to check in to see if this helped! Cheers!

CreativeNative commented 2 years ago

Thanks a lot!

Question 1:

In my fork I had to add in the entity file UserApiToken this , inversedBy="api_tokens" to get doctrine validation passed.

   /**
     * @ORM\ManyToOne(targetEntity="CirclicalUser\Entity\User", inversedBy="api_tokens")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
     */
    private UserInterface $user;

Is that right?

Question 2:

How do I validate the token? Is it enough when I get the token like so:

$token= $this->userApiTokenMapper->get($tokenParameter);

Or do I have to check if the user really has this token?

 $userApiToken= $user->getApiTokenWithId($token->getUuid()->toString());

Or do I have to check if the uuids are equal like so?

$token->getUuid()->equals($userApiToken->getUuid());

I'm confused.

For explanation I create a token:

$userApiToken = new UserApiToken($user, TokenScopeInterface::REGISTRATION);
$this->entityManager->persist($userApiToken);
$user->addApiToken($userApiToken);

Than I create a link with a token:

'tokenHash' => $userApiToken->getToken(),

When the user clicks the link I want to validate the token and want to get, if possibile, also the information of the user like e-mail-address. But the following makes no sense in terms of validation:

$userApiToken = $this->userApiTokenMapper->get($tokenHash);
$user = $userApiToken->getUser();        
$token = $user->getApiTokenWithId($userApiToken->getUuid()->toString());
$token->getUuid()->equals($userApiToken->getUuid());

So how do I do it the right way?

Question 3:

I can tag the token as used, but after the registration is completed, wouldn't it be better to delete the token? I can't find nothing to delete token. Don't want to fill my database with tokens that I never use again. I'm not quite sure why I can tag the token as used. Can you give me an example?

By now we have in the user entity the functions:

Wouldn't it be cool to have also a removeApiToken function?

I would like ti update the template file for the user entity to integrate the token system in a right way, but before I want to discuss everything with you.

Question 4:

When I try to get the token via $userApiTokenMapper->get($tokenHash); I get the following error.

Typed property CirclicalUser\Mapper\AbstractDoctrineMapper::$entityManager must not be accessed before initialization
File: vendor/saeven/zf3-circlical-user/src/CirclicalUser/Mapper/AbstractDoctrineMapper.php:39

So there isn't a factory where the entityManager will be injected automatically. I have to inject it by myself like so.

$userApiTokenMapper->setEntityManager($entityManager);

Isn't that a bit over engineered? Can't we use the entityManager directly and instead of the UserApiTokenMapper create a UserApiTokenRepository for the "get"-function? In general all mapper classes are looking for me more like repositories. I think this layer with AbstractDoctrineMapper is not necessary. What do you think?

Saeven commented 2 years ago

Validation depends on context. We have a large application that uses this lib, and we've extended the mapper to suit a multitude of interesting use cases, but validation can be very rudimentary. At the simplest level, you can list tokens from the user relationship or search there with Criteria.

If you are coming in blind, and want to ensure that an auth token matches the request token (can be important), then you can run auth, plus use the mapper to fetch the token, and ensure that the authenticated user and owner line up.

I wouldn't be willing to change the current relationships, because they are being leveraged in important ways in a very large app that depends on things the way they are. With continued use, I think you will find that the current architecture reveals important advantages.

Saeven commented 2 years ago

Regarding your mapper error, the AbstractDoctrineMapper in this library would never be used directly. It is extended by your own mappers - check out AbstractDoctrineMapperFactory's canCreate method. You will see that it looks for classes that end with 'Mapper'

A basic mapper can be as simple as an entity declaration:

class FooMapper extends AbstractDoctrineMapper
{
    protected string $entityName = FooMapper::class;
}
CreativeNative commented 2 years ago

Ohh, I see. Here comes the magic from:

'service_manager' => [
    'abstract_factories' => [
        AbstractDoctrineMapperFactory::class,
    ],
],

But than in my system the AbstractDoctrineMapperFactory isn't working, because when I use $this->userApiTokenMapper->get($tokenHash); I get the error that the EntityManager wasn't injected.

You say that you "wouldn't be willing to change the current relationships". Is that pointing on question 1? I don't want to change any relations. My problem is, that the mapping isn't valid when I add this to my user entity.

/**
  * @ORM\OneToMany(targetEntity="CirclicalUser\Entity\UserApiToken", mappedBy="user", cascade={"all"});
  */
  private $api_tokens;

So where it comes from that my mapping isn't valid but yours obviously is?

Saeven commented 2 years ago

If you can share a rudimentary repo to reproduce the issue I’d gladly help out!

CreativeNative commented 2 years ago

Here you go. https://github.com/CreativeNative/zf3-circlical-user/tree/UserSample

I added the updated and very basic user entity sample that I would love to push. If you check it out and run the doctrine validation you will see the error. vendor/bin/doctrine-module orm:validate-schema

[FAIL] The entity-class CirclicalUser\Entity\User mapping is invalid:
 * The field CirclicalUser\Entity\User#api_tokens is on the inverse side of a bi-directional relationship, but the specified mappedBy association on the target-entity CirclicalUser\Entity\UserApiToken#user does not contain the required 'inversedBy="api_tokens"' attribute.

I think you want here a One-To-Many, Bidirectional relationship. Like this you can ask for the user for a token and the token for a user.

Saeven commented 2 years ago

Hi! To help. I would need a repo where you are using the lib within a project that's experiencing the issue. The lib itself is not meant to work "standalone", but is instead meant to be supplanted into a project that defines its own user, etc.

If you are a patient man, I can find some time in the next few days to set up a skeleton and load in this lib, and add a sample controller for you, to show one way of consuming tokens 👍🏼

CreativeNative commented 2 years ago

That would be awesome! Also for testing or showing you new stuff. Thank you very much.

CreativeNative commented 2 years ago

Please have a look also here. I updated the entity UserApiToken and adjusted the Bidirectional relationship.

https://github.com/CreativeNative/zf3-circlical-user/tree/UserApiToken

Saeven commented 2 years ago

Here you go @CreativeNative, using existing libs in their unmodified form. I whipped this up real fast just now, and was as lazy as possible (a nice end to a good Sunday!). Everything works - I rolled in some useful libs as well! Can have some fun with Alpine and Tailwind while you're at it if you like.

https://github.com/Saeven/laminas-mvc-skeleton

CreativeNative commented 2 years ago

I can't use your really cool skeleton by now, because I'm using PHP 7.4. It's on my list to upgrade, but first I have to resolve this. Could you verify this error in the skeleton?

[FAIL] The entity-class CirclicalUser\Entity\User mapping is invalid:
 * The field CirclicalUser\Entity\User#api_tokens is on the inverse side of a bi-directional relationship, but the specified mappedBy association on the target-entity CirclicalUser\Entity\UserApiToken#user does not contain the required 'inversedBy="api_tokens"' attribute.
Saeven commented 2 years ago

Totally understand. The skeleton was authored precisely to 'prove' in a sense, that no such error surfaces. I made a video and jammed it on YT for you, at this link: https://www.youtube.com/watch?v=96gI5wb4UlA

You can see that without any trouble, I can install and spawn database real quick. The user entity is precisely as it exists in the skeleton, and this library is unadulterated ;)

Hope this helps!

Saeven commented 2 years ago

Feel free to hit me up on Gitter btw https://gitter.im/Circlical/zf3-circlical-user