read

Our goal will be to scaffold out a basic app that uses doctrine on the backend... and then get passport installed to handle authentication. This is a cool setup because then you have all you need to create an app that also has an api.

Doctrine

http://www.laraveldoctrine.org/. Laraveldoctrine is a full integration of Doctrine 2 for Laravel, that has great documentation and some really cool extra features and packages that make it something anybody who loves Laravel should take a look at. I wanted to make a guide featuring this package because I believe it is a great alternative to Eloquent with some nice advantages for large projects. It also suites my coding style a bit better. I will not be writing about the differences between Doctrine and Eloquent in this particular guide, but I will say that both are great database abstraction layers.

Passport

Passport is a great Laravel package that allows you to setup an oauth 2 server. https://github.com/laravel/passport

PART 1, Laravel + Doctrine + authentication

start a new Laravel project

laravel new doctrine-passport-demo

Setup your .env file to point to a new database you create etc... For my purposes I created a database called 'demo' (using mysql).

Doctrine setup prerequisites

Follow the steps here: http://www.laraveldoctrine.org/docs/1.3/orm/installation

And here: http://www.laraveldoctrine.org/docs/1.3/migrations/installation

And lastly we'll need some of the extensions to make life easier: http://www.laraveldoctrine.org/docs/1.3/extensions/installation

install the laravel-doctrine/extensions package and the gedmo one. We don't need the Beberlei extension.

Setup!

Our next goal is to get basic login/logout and registration functionality.

For this demo, I'm going to use Doctrine annotations, and I am going to create a User entity to use for logins, and I'm going to stick it into an Entity folder.

Setup a basic user entity that has 4 traits: Authenticatable, CanResetPassword, Notifiable and Timestamps. Make sure to use the Doctrine specific traits and interface etc. I'll include my basic user here as an example

<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use LaravelDoctrine\ORM\Auth\Authenticatable;
use LaravelDoctrine\Extensions\Timestamps\Timestamps;
use LaravelDoctrine\ORM\Notifications\Notifiable;

/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User implements AuthenticatableContract, CanResetPasswordContract
{

    use Authenticatable, CanResetPassword, Timestamps, Notifiable;

    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $email;

    /**
     * @var string
     * @ORM\Column(type="string",nullable=false)
     */
    protected $name;

}

Configuration

in your doctrine.php file... (if you don't have this you need publish the config files for doctrine)

fix the namespace and paths to point at where you put your Entities...

    'namespaces' => [
            'App\Entities'
    ],
    'paths' => [
            base_path('app/Entities')
    ],

Uncomment the Timestampable extension as well as another other you might want to use.

'extensions' => [
        //LaravelDoctrine\ORM\Extensions\TablePrefix\TablePrefixExtension::class,
        LaravelDoctrine\Extensions\Timestamps\TimestampableExtension::class,
        LaravelDoctrine\Extensions\SoftDeletes\SoftDeleteableExtension::class,
        //LaravelDoctrine\Extensions\Sluggable\SluggableExtension::class,
        //LaravelDoctrine\Extensions\Sortable\SortableExtension::class,
        //LaravelDoctrine\Extensions\Tree\TreeExtension::class,
        //LaravelDoctrine\Extensions\Loggable\LoggableExtension::class,
        //LaravelDoctrine\Extensions\Blameable\BlameableExtension::class,
        //LaravelDoctrine\Extensions\IpTraceable\IpTraceableExtension::class,
        //LaravelDoctrine\Extensions\Translatable\TranslatableExtension::class
],

Now go to your auth.php file

change the providers section to use your entity and the doctrine driver.

    'providers' => [
        'users' => [
            'driver' => 'doctrine',
            'model' => App\Entities\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

OK, so at this point in time your composer.json file should have these entries:

    "require": {
        "php": ">=5.6.4",
        "gedmo/doctrine-extensions": "^2.4", // we installed this
        "laravel-doctrine/extensions": "^1.0", // we installed this
        "laravel-doctrine/migrations": "^1.1", // we installed this
        "laravel-doctrine/orm": "^1.3", // we installed this
        "laravel/framework": "5.4.*",
        "laravel/tinker": "~1.0"
    },

And your app.php should have this stuff if you followed the setup guides etc listed above:

        LaravelDoctrine\ORM\DoctrineServiceProvider::class,
        LaravelDoctrine\Extensions\GedmoExtensionsServiceProvider::class,
        LaravelDoctrine\Migrations\MigrationsServiceProvider::class,

Finally, go ahead and replace the PasswordResetServiceProvider provided by Laravel, with the one provided by laravel-doctrine in the app.php file as well.

//        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        \LaravelDoctrine\ORM\Auth\Passwords\PasswordResetServiceProvider::class,

Using Laravel's quick authentication setup

Now we are ready to scaffold out the authentication stuff. Run the make auth command:

php artisan make:auth

Go ahead and delete the 2 migrations that get created with this command in the migrations/ folder. After you delete them, then we'll generate our own migration for our user entity.

php artisan doctrine:migrations:diff

then migrate it

php artisan doctrine:migrations:migrate

Note: you'll notice we don't create a migration or entity for the password_resets table. We don't actually need one, Doctrine will magically create one when it tries to use it for the first time.

Now you just have to make a few more modificaitons to the generated controllers to gether everything to work. Go to the RegisterController and modify the validator function. You must use the unique rule slightly different with laravel doctrine.. you can find documentation for it here: http://www.laraveldoctrine.org/docs/1.3/orm/validation

It should look like this (change the reference to the users table to your User entity)

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:App\Entities\User',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }

Change the create() function so it uses the new entity to create the user (you'll have to create some getters/setters for your properties on the entity)

   protected function create(array $data)
    {

        $user = new User();
        $user->setEmail($data['email']);
        $user->setName($data['name']);
        $user->setPassword(bcrypt($data['password']));

        \EntityManager::persist($user);
        \EntityManager::flush();

        return $user;
    }

Then in the ResetPasswordController override the resetPassword function by adding this function to the controller:

    protected function resetPassword($user, $password)
    {
        $user->setPassword(bcrypt($password));
        $user->setRememberToken(Str::random(60));

        EntityManager::persist($user);
        EntityManager::flush();

        $this->guard()->login($user);
    }

Lastly... fix the app.blade.php file so it uses the getter for your name.

   <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
           {{ Auth::user()->getName() }} <span class="caret"></span>
   </a>

Go ahead and test it, that should be it!

Part 2 Setting up Passport

Initial steps

install the composer package...

composer require laravel/passport

Add the service provider to app.php

Laravel\Passport\PassportServiceProvider::class,

open AppServiceProvder.php and add a line to ignore the migrations to the register function.

    public function register()
    {
        Passport::ignoreMigrations();
    }

Entities

Now we need to be able to generate all the proper tables for passport. I created Entities with annotations to represent the tables. Entities aren't really needed since we won't use them to actually manipulate the data, so you can create the tables/mappings some other way if you prefer. I will include the entities I created here:

OauthAccessToken

<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 * Class OauthAccessTokens
 * @package App\Entities
 * @ORM\Entity
 * @ORM\Table(name="oauth_access_tokens", indexes={@ORM\Index(name="user_id_token_index", columns={"user_id"})})
 */
class OauthAccessToken
{

    /**
     * @var string
     * @ORM\Id
     * @ORM\Column(type="string", length=100)
     */
    protected $id;

    /**
     * @var int
     * @ORM\Column(name="user_id", type="integer", nullable=true)
     */
    protected $userId;

    /**
     * @var int
     * @ORM\Column(name="client_id", type="integer")
     */
    protected $clientId;

    /**
     * @var string
     * @ORM\Column(type="string", nullable=true)
     */
    protected $name;

    /**
     * @var string
     * @ORM\Column(type="text", nullable=true)
     */
    protected $scopes;

    /**
     * @var boolean
     * @ORM\Column(type="boolean")
     */
    protected $revoked;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $createdAt;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $updatedAt;

    /**
     * @ORM\Column(name="expires_at", type="datetime", nullable=true)
     */
    protected $expiresAt;

}

OauthAuthCode

<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 *
 *
 * Class OauthAuthCode
 * @package App\Entities
 * @ORM\Entity
 * @ORM\Table(name="oauth_auth_codes")
 */
class OauthAuthCode
{

    /**
     * @var string
     * @ORM\Id
     * @ORM\Column(type="string", length=100)
     */
    protected $id;

    /**
     * @var int
     * @ORM\Column(name="user_id", type="integer")
     */
    protected $userId;

    /**
     * @var int
     * @ORM\Column(name="client_id", type="integer")
     */
    protected $clientId;

    /**
     * @var string
     * @ORM\Column(type="text", nullable=true)
     */
    protected $scopes;

    /**
     * @var boolean
     * @ORM\Column(type="boolean")
     */
    protected $revoked;

    /**
     * @ORM\Column(name="expires_at", type="datetime", nullable=true)
     */
    protected $expiresAt;

}

OauthClients

<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\Timestampable;

/**
 * OauthClients
 * @package App\Entities
 * @ORM\Entity
 * @ORM\Table(name="oauth_clients", indexes={@ORM\Index(name="user_id_client_index", columns={"user_id"})})
 */
class OauthClients
{

    /**
     * @var int
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @var int
     * @ORM\Column(name="user_id", type="integer", nullable=true)
     */
    protected $userId;

    /**
     * @var string
     * @ORM\Column(type="string")
     */
    protected $name;

    /**
     * @var string
     * @ORM\Column(name="secret", type="string", length=100)
     */
    protected $secret;

    /**
     * @var string
     * @ORM\Column(type="text")
     */
    protected $redirect;

    /**
     * @var boolean
     * @ORM\Column(name="personal_access_client", type="boolean")
     */
    protected $personalAccessClient;

    /**
     * @var boolean
     * @ORM\Column(name="password_client", type="boolean")
     */
    protected $passwordClient;

    /**
     * @var boolean
     * @ORM\Column(type="boolean")
     */
    protected $revoked;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $createdAt;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $updatedAt;
}

OauthPersonalAccessClient

<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 * Class OauthPersonalAccessClient
 * @package App\Entities
 * @ORM\Entity
 * @ORM\Table(name="oauth_personal_access_clients", indexes={@ORM\Index(name="client_id_index", columns={"client_id"})})
 */
class OauthPersonalAccessClient
{

    /**
     * @var int
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @var int
     * @ORM\Column(name="client_id", type="integer")
     */
    protected $clientId;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $createdAt;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $updatedAt;
}

OauthRefreshToken

<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 * OauthRefreshToken
 * @package App\Entities
 * @ORM\Entity
 * @ORM\Table(name="oauth_refresh_tokens", indexes={@ORM\Index(name="access_token_index", columns={"access_token_id"})})
 */
class OauthRefreshToken
{

    /**
     * @var string
     * @ORM\Id
     * @ORM\Column(type="string", length=100)
     */
    protected $id;

    /**
     * @var int
     * @ORM\Column(name="access_token_id", type="string", length=100)
     */
    protected $accessTokenId;

    /**
     * @var boolean
     * @ORM\Column(type="boolean")
     */
    protected $revoked;

    /**
     * @ORM\Column(name="expires_at", type="datetime", nullable=true)
     */
    protected $expiresAt;

}

You can then run the diff command to generate a migration. It should create something like this:

    /**
     * @param Schema $schema
     */
    public function up(Schema $schema)
    {
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');

        $this->addSql('CREATE TABLE oauth_access_tokens (id VARCHAR(100) NOT NULL, user_id INT DEFAULT NULL, client_id INT NOT NULL, name VARCHAR(255) DEFAULT NULL, scopes LONGTEXT DEFAULT NULL, revoked TINYINT(1) NOT NULL, created_at DATETIME DEFAULT NULL, updated_at DATETIME DEFAULT NULL, expires_at DATETIME DEFAULT NULL, INDEX user_id_token_index (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE oauth_auth_codes (id VARCHAR(100) NOT NULL, user_id INT NOT NULL, client_id INT NOT NULL, scopes LONGTEXT DEFAULT NULL, revoked TINYINT(1) NOT NULL, expires_at DATETIME DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE oauth_clients (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, secret VARCHAR(100) NOT NULL, redirect LONGTEXT NOT NULL, personal_access_client TINYINT(1) NOT NULL, password_client TINYINT(1) NOT NULL, revoked TINYINT(1) NOT NULL, created_at DATETIME DEFAULT NULL, updated_at DATETIME DEFAULT NULL, INDEX user_id_client_index (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE oauth_personal_access_clients (id INT AUTO_INCREMENT NOT NULL, client_id INT NOT NULL, created_at DATETIME DEFAULT NULL, updated_at DATETIME DEFAULT NULL, INDEX client_id_index (client_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
        $this->addSql('CREATE TABLE oauth_refresh_tokens (id VARCHAR(100) NOT NULL, access_token_id VARCHAR(100) NOT NULL, revoked TINYINT(1) NOT NULL, expires_at DATETIME DEFAULT NULL, INDEX access_token_index (access_token_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
    }

    /**
     * @param Schema $schema
     */
    public function down(Schema $schema)
    {
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');

        $this->addSql('DROP TABLE oauth_access_tokens');
        $this->addSql('DROP TABLE oauth_auth_codes');
        $this->addSql('DROP TABLE oauth_clients');
        $this->addSql('DROP TABLE oauth_personal_access_clients');
        $this->addSql('DROP TABLE oauth_refresh_tokens');
    }

Run the migration...

Run the passport install command

php artisan passport:install

Add the HasApiTokens trait to your user entity. It should now look something like this:

class User implements AuthenticatableContract, CanResetPasswordContract
{

    use Authenticatable, CanResetPassword, Timestamps, Notifiable, HasApiTokens;

Call the passport routes from your AuthServiceProvider in the boot method

    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }

Configuration

Next go to the auth.php file and setup the configuration to use passport.

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

Frontend

publish the built in frontend pages

php artisan vendor:publish --tag=passport-components

The published components will be placed in your resources/assets/js/components directory. Once the components have been published, you should register them in your resources/assets/js/app.js file (I replace the example vue component):

Vue.component(
    'passport-clients',
    require('./components/passport/Clients.vue')
);

Vue.component(
    'passport-authorized-clients',
    require('./components/passport/AuthorizedClients.vue')
);

Vue.component(
    'passport-personal-access-tokens',
    require('./components/passport/PersonalAccessTokens.vue')
);

Run npm install in your project directory

npm install

Then recompile your assets by running

npm run dev

Now edit your home.blade.php file and replace "You are now logged in!" with the vue components

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Dashboard</div>

                <div class="panel-body">
                    <passport-clients></passport-clients>
                    <passport-authorized-clients></passport-authorized-clients>
                    <passport-personal-access-tokens></passport-personal-access-tokens>                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Making it work!

If you were to try it now, things will still break because passport won't work straight out of the box with Doctrine... so there are still a few things to do.

add a getKey method on your user entity

   public function getKey() {
        return $this->getId();
    }

Last but not least... fix the API method they have for you to test in api.php so it returns something we can actually get...

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user()->getName();
});

BOOM! it should all work now.

I used Postman to test my oauth2 setup

successful get request to API endpoint

Note: If you want to get password grants to work there is a bit of additional setup. Add a trait that looks like this to your user entity and you should be good to go:

trait UsesPasswordGrant
{

    /**
     * @param string $userIdentifier
     * @return User
     */
    public function findForPassport($userIdentifier)
    {
        $userRepository = EntityManager::getRepository(get_class($this));
        return $userRepository->findOneByEmail($userIdentifier);
    }

}

So that is essentially it... I tried to be as detailed as possible. If you have any questions let me know below and I might be able to help. You can download this demo project at https://github.com/isaackearl/doctrine-passport-demo if you want to skip the setup!

Blog Logo

Isaac Earl

Web dev & whatever else


Published

Blog Comments powered by Disqus.
Image

Isaac's Blog

Ramblings of a software dev

Back to Overview