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

Better documentation for User API Tokens #84

Open
CreativeNative opened this issue Nov 5, 2021 · 15 comments
Open

Better documentation for User API Tokens #84

CreativeNative opened this issue Nov 5, 2021 · 15 comments
Labels

Comments

@CreativeNative
Copy link
Contributor

CreativeNative commented Nov 5, 2021

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:

  • The field TmiUser\Entity\User#apiTokens 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="apiTokens"' attribute.
@Saeven Saeven added the question label Nov 8, 2021
@Saeven
Copy link
Owner

Saeven commented Nov 8, 2021

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
Copy link
Owner

Saeven commented Nov 17, 2021

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

@CreativeNative
Copy link
Contributor Author

CreativeNative commented Nov 17, 2021

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:

  • addApiToken
  • getApiTokens
  • getApiTokenWithId

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
Copy link
Owner

Saeven commented Nov 19, 2021

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
Copy link
Owner

Saeven commented Nov 19, 2021

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
Copy link
Contributor Author

CreativeNative commented Nov 19, 2021

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
Copy link
Owner

Saeven commented Nov 19, 2021

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

@CreativeNative
Copy link
Contributor Author

CreativeNative commented Nov 19, 2021

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
Copy link
Owner

Saeven commented Nov 19, 2021

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
Copy link
Contributor Author

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

@CreativeNative
Copy link
Contributor Author

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
Copy link
Owner

Saeven commented Nov 22, 2021

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
Copy link
Contributor Author

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
Copy link
Owner

Saeven commented Nov 23, 2021

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
Copy link
Owner

Saeven commented Nov 25, 2021

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

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

No branches or pull requests

2 participants