Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

ApiToken Entity

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to get to work on our API token authentication system! As we just learned, there are a bunch of different ways to do API auth. We're going to code through one way, which will make you plenty dangerous for whatever way you ultimately need.

For our API tokens, we're going to create an ApiToken entity in the database to store them. Find your terminal and run:

php bin/console make:entity

Call the class ApiToken. And, we need a few fields: token, a string that's not nullable, expiresAt so that we can set an expiration as a datetime, and user, which will be a relation type to our User class. In this situation, we want a ManyToOne relationship so that each ApiToken has one User and each User can have many ApiTokens. Make this not nullable: every API token must be related to a User. And, though it doesn't matter for authentication, let's map both sides of the relationship. That will allow us to easily fetch all of the API tokens for a specific user. For orphanRemoval, this is also not important, but choose yes. If we create a page where a user can manage their API tokens, this might make it easier to delete API tokens.

And... done!

... lines 1 - 2
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\ApiTokenRepository")
*/
class ApiToken
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $token;
/**
* @ORM\Column(type="datetime")
*/
private $expiresAt;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="apiTokens")
* @ORM\JoinColumn(nullable=false)
*/
private $user;
public function getId(): ?int
{
return $this->id;
}
public function getToken(): ?string
{
return $this->token;
}
public function setToken(string $token): self
{
$this->token = $token;
return $this;
}
public function getExpiresAt(): ?\DateTimeInterface
{
return $this->expiresAt;
}
public function setExpiresAt(\DateTimeInterface $expiresAt): self
{
$this->expiresAt = $expiresAt;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
}

Generate the migration with:

php bin/console make:migration

Go check it out - in the Migrations/ directory, open that file:

... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20180901171717 extends AbstractMigration
{
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE api_token (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, token VARCHAR(255) NOT NULL, expires_at DATETIME NOT NULL, INDEX IDX_7BA2F5EBA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE api_token ADD CONSTRAINT FK_7BA2F5EBA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE api_token');
}
}

Cool! CREATE TABLE api_token with id, user_id, token and expires_at. And, it creates the foreign key.

That looks perfect. Move back and run it!

php bin/console doctrine:migrations:migrate

How are Tokens Created?

So, the question of how these ApiTokens will be created is not something we're going to answer. As we talked about, it's either super easy... or super complicated, depending on your needs.

So, for our app, we're just going to create some ApiTokens via the fixtures.

Making the ApiToken Class Awesome

But before we do that, open the new ApiToken entity class. Yep, all the usual stuff: some properties, annotations and a getter & setter for each method. I want to change things a bit. The make:entity command always generates getter and setter methods. But, in some cases, there is a better way to design things.

Add a public function __construct() method with a User argument:

... lines 1 - 9
class ApiToken
{
... lines 12 - 34
public function __construct(User $user)
{
... lines 37 - 39
}
... lines 41 - 60
}

Because every ApiToken needs a User, why not make it required when the object is instantiated? Oh, and we can also generate the random token string here. Use $this->token = bin2hex(random_bytes(60)). Then $this->user = $user:

... lines 1 - 9
class ApiToken
{
... lines 12 - 34
public function __construct(User $user)
{
$this->token = bin2hex(random_bytes(60));
$this->user = $user;
... line 39
}
... lines 41 - 60
}

Oh, and we can also set the expires time here - $this->expiresAt = new \DateTime() with +1 hour:

... lines 1 - 9
class ApiToken
{
... lines 12 - 34
public function __construct(User $user)
{
$this->token = bin2hex(random_bytes(60));
$this->user = $user;
$this->expiresAt = new \DateTime('+1 hour');
}
... lines 41 - 60
}

You can set the expiration time for however long you want.

Now that we are initializing everything in the constructor, we can clean up the class: remove all the setter methods:

... lines 1 - 9
class ApiToken
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $token;
/**
* @ORM\Column(type="datetime")
*/
private $expiresAt;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="apiTokens")
* @ORM\JoinColumn(nullable=false)
*/
private $user;
public function __construct(User $user)
{
$this->token = bin2hex(random_bytes(60));
$this->user = $user;
$this->expiresAt = new \DateTime('+1 hour');
}
public function getId(): ?int
{
return $this->id;
}
public function getToken(): ?string
{
return $this->token;
}
public function getExpiresAt(): ?\DateTimeInterface
{
return $this->expiresAt;
}
public function getUser(): ?User
{
return $this->user;
}
}

Yep, our token class is now immutable, which wins us major hipster points. Immutable just means that, once it's instantiated, this object's data can never be changed. Some developers think that making immutable objects like this is super important. I don't fully agree with that. But, it definitely makes sense to be thoughtful about your entity classes. Sometimes having setter methods makes sense. But sometimes, it makes more sense to setup some things in the constructor and remove the setter methods if you don't need them.

Oh, and if, in the future, you want to update the data in this entity - maybe you need to change the expiresAt, it's totally OK to add a new public function to allow that. But, when you do, again, be thoughtful. You could add a public function setExpiresAt(). Or, if all you ever do is re-set the expiresAt to one hour from now, you could instead create a public function renewExpiresAt() that handles that logic for you:

public function renewExpiresAt()
{
    $this->expiresAt = new \DateTime('+1 hour');
}

That method name is more meaningful, and centralizes more control inside the class.

Ok, I'm done with my rant!

Adding ApiTokens to the Fixtures

Let's create some ApiTokens in the fixtures already! We could create a new ApiTokenFixture class, but, to keep things simple, I'm going to put the logic right inside UserFixture.

Use $apiToken1 = new ApiToken() and pass our User. Copy that and create $apiToken2:

... lines 1 - 4
use App\Entity\ApiToken;
... lines 6 - 9
class UserFixture extends BaseFixture
{
... lines 12 - 18
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) use ($manager) {
... lines 22 - 34
$apiToken1 = new ApiToken($user);
$apiToken2 = new ApiToken($user);
... lines 37 - 39
return $user;
});
... lines 42 - 57
}
}

With our fancy createMany() method, you do not need to call persist() or flush() on the object that you return. That's because our base class calls persist() on the object for us:

... lines 1 - 9
abstract class BaseFixture extends Fixture
{
... lines 12 - 45
protected function createMany(int $count, string $groupName, callable $factory)
{
for ($i = 0; $i < $count; $i++) {
... lines 49 - 54
$this->manager->persist($entity);
... lines 56 - 58
}
}
... lines 61 - 90
}

But, if you create some objects manually - like this - you do need to call persist(). No big deal: add use ($manager) to make the variable available in the callback. Then,$manager->persist($apiToken1) and $manager->persist($apiToken2):

... lines 1 - 4
use App\Entity\ApiToken;
... lines 6 - 9
class UserFixture extends BaseFixture
{
... lines 12 - 18
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) use ($manager) {
... lines 22 - 34
$apiToken1 = new ApiToken($user);
$apiToken2 = new ApiToken($user);
$manager->persist($apiToken1);
$manager->persist($apiToken2);
... lines 39 - 40
});
... lines 42 - 57
}
}

That should be it! Let's reload some fixtures!

php bin/console doctrine:fixtures:load

When it's done, run:

php bin/console doctrine:query:sql 'SELECT * FROM api_token'

Beautiful, long, random strings. And each is related to a User.

Next, let's create an authenticator that's capable of reading, processing & authenticating these API tokens.

Leave a comment!

10
Login or Register to join the conversation
Roozbeh S. Avatar
Roozbeh S. Avatar Roozbeh S. | posted 2 years ago

Hi Ryan,
Thank you so much for your fantastic Tutorials!

I have two questions here about "Api Token".
1- Is there any specific reason why the relation between User and ApiToken Entities are "oneToMany" or Could it be also "oneToOne"?

2- By Following your other Tutorials, I have made a Database and Api Platform and also I have a React Native Mobile App with a login screen and an Api back-end! but, I don't know the flow of Authenticate the User from the app to the Api and in combination with retrieving a Token for Authorization! Can you please explain to me in details which steps to take?

Reply

Hey Roozbeh S.!

Thank you so much for your fantastic Tutorials!

I'm so glad you've found them useful!!!

1- Is there any specific reason why the relation between User and ApiToken Entities are "oneToMany" or Could it be also "oneToOne"?

Yep, could totally be OneToOne. It's just a matter of whether you want to allow a single account to have just one API token or the ability to create many.

2- By Following your other Tutorials, I have made a Database and Api Platform and also I have a React Native Mobile App with a login screen and an Api back-end! but, I don't know the flow of Authenticate the User from the app to the Api and in combination with retrieving a Token for Authorization!

First, I have to say that making a mobile app is something that I have not done. I do my best to read up on the strategies, but I'm not fully qualified to answer this question. That being said, for React Native, they have a nice example of authentication - https://reactnavigation.org/docs/auth-flow/ - the key method that you're looking for you can find if you search for signIn: async data => {<br />. There, they describe it: it can be as simple as sending a POST request with the email & password to an endpoint that validates that and creates (and returns) a token. We create an endpoint like that in this tutorial - https://symfonycasts.com/screencast/symfony-rest4/create-json-web-token. In that tutorial, we are using JWT (instead of tokens stored in the database), but that is NOT important. The important thing is that you have an endpoint that creates a token and returns it. GitHub's API also has this type of functionality where you can send HTTP basic authentication to an endpoint and get back a token - https://lornajane.net/posts/2012/github-api-access-tokens-via-curl

Let me know if that helps!

Cheers!

Reply
Roozbeh S. Avatar

Thank you so much Ryan,
you have explained it very well.

I am going to follow the links you have sent and hopefully I can solve my issues!

I’m very grateful for your respond.

Thank you

Reply
Dung L. Avatar
Dung L. Avatar Dung L. | posted 3 years ago

Very well explained and it is interesting, thanks!

Reply

A question about the "make" command.
Can you generate entity that has configuration in YAML, instead of annotations?
If not, is there an easy way to convert your annotations to YAML configs?

Reply

Hey Serge,

No, MakerBundle generates entities in annotation format only, and actually that's best practice to use annotations for your entities. But you can generate an annotated entity with MakerBundle and use "bin/console doctrine:mapping:convert" command to convert it into yaml/xml format.

Cheers!

1 Reply

Some questions about ApiToken entity:
1) Since all fields get their values at the constructor, shouldn't we remove the question nullable mark from the getters return type?
2) What are your thoughts about making $token the primary key? Or at least a unique key?

Reply

Hey IvanPuntiy!

These are great questions!

1) Yes! Definitely - I wasn't even thinking about this / being lazy. We can now remove those - which is awesome!

2) Primary key... maybe... I just personally always like having a specific, meaningless primary key (whether it's an auto-incrementing integer or a uuid). I don't see a lot of benefit into making it primary. But making it unique, heck yea! You won't run into collisions (the odds of that happening are astronomical) but it *will* make querying on this field a bit faster... so why not. As a warning, if you get an error about the unique key length being too long, it's an Innodb + utf8 problem - just make the length of your token field a bit smaller (even 180 is small enough).

Cheers!

Reply

I am trying to work through this because I have a project that I need this for and I am just wondering if I am thinking about this correctly?

When I create a new user, I want to automatically create a token. So, I am using teh following Doctrine EventSubscriber and am wondering if there is a better way?


namespace App\Doctrine;

use App\Entity\ApiToken;
use App\Entity\User;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\Common\EventSubscriber;

class UserTokenSubscriber implements EventSubscriber
{
    /**
     * Returns an array of events this subscriber wants to listen to.
     *
     * @return string[]
     */
    public function getSubscribedEvents()
    {
        return ['prePersist', 'preUpdate'];
    }

    /**
     * @param LifecycleEventArgs $args
     *
     * @throws \Doctrine\ORM\ORMException
     */
    public function prePersist(LifecycleEventArgs $args) {
        $entity = $args->getEntity();

        if(!$entity instanceof User) {
            return;
        }

        if($entity->getApiTokens() && count($entity->getApiTokens()) == 0) {
            $token = new ApiToken($entity);

            $em = $args->getEntityManager();
            $em->persist($token);
        }
    }

    /**
     * @param LifecycleEventArgs $args
     * @throws \Doctrine\ORM\ORMException
     */
    public function preUpdate(LifecycleEventArgs $args) {
        $entity = $args->getEntity();

        if(!$entity instanceof User) {
            return;
        }

        if($entity->getApiTokens() && count($entity->getApiTokens()) == 0) {
            $token = new ApiToken($entity);

            $em = $args->getEntityManager();
            $em->persist($token);
        }
    }

}

And, for completeness, I added this to services.yaml:


    app.doctrine.hash_password_subscriber:
        class: App\Doctrine\HashPasswordSubscriber
        tags:
        - { name: doctrine.event_subscriber }

    app.doctrine.user_token_subscriber:
            class: App\Doctrine\UserTokenSubscriber
            tags:
            - { name: doctrine.event_subscriber }
Reply

Hey Scott

I don't think there is something wrong with your approach (that's how we encrypt the password for users), although I'm not such a fan of Doctrine listeners, I would probably set that ApiToken right where users are created

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}
userVoice