Buy
Buy

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 ever 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!

  • 2018-10-22 Victor Bocharsky

    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!

  • 2018-10-19 Serge Boyko

    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?

  • 2018-10-18 weaverryan

    Hey Ivan Puntiy!

    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!

  • 2018-10-17 Ivan Puntiy

    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?

  • 2018-10-02 Diego Aguiar

    Hey Scott Collier

    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!

  • 2018-10-02 Scott Collier

    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 }