API Token Based Access Laravel 5.1 (Yet another article on this)

Posted: 2016-05-31 12:21:31

API Token

Laravel 5.2 introduces the auth token guard setup which is way simpler than Oauth. Also it does not assume a certain level of complexity in needs like Scopes and expirations of tokens that we do not need for our internal app to app communications.

We are using 5.1 so this will implement it.

See some docs here https://gistlog.co/JacobBennett/090369fbab0b31130b51

For creating a user the AppServiceProvider boots the user record and if no api_token is present it will set one. As well as the UUID (we use https://packagist.org/packages/ramsey/uuid for this)

app/Providers/AppServiceProvider.php:18

        User::creating(function($user) {
            if (!$user->id)
            {
                $user->id = Uuid::uuid4()->toString();
            }

            if (!$user->api_token)
            {
                $user->api_token = Uuid::uuid4()->toString();
            }
        });

The factory includes this as well database/factories/ModelFactory.php

$factory->define(App\User::class, function ($faker) {
    return [
        'id' => $faker->uuid,
        'name' => $faker->name,
        'email' => $faker->email,
        'api_token' => $faker->uuid,
        'password' => str_random(10),
        'remember_token' => str_random(10),
    ];
});

Note in the \App\User model has this field hidden like the password field app/User.php

///
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
    use Authenticatable, CanResetPassword;

    protected $hidden = ['password', 'remember_token', 'api_token'];

///

And of course we need the migration for this

<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddApiTokenToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function(Blueprint $table)
        {
            $table->string('api_token', 60)->unique();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function(Blueprint $table)
        {
            if(Schema::hasColumn('users', 'api_token'))
                $table->dropColumn('api_token');
        });
    }
}

This is all happening using the api_token middleware seen here app/Http/Middleware/ApiToken.php

<?php
namespace App\Http\Middleware;
use App\User;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Support\Facades\App;
class ApiToken
{
    protected $auth;

    public function __construct(Guard $auth)
    {
        $this->auth = $auth;
    }

    public function handle($request, Closure $next)
    {
        if($request->input('api_token') && $this->hasMatchingToken($request->input('api_token'))) {
            return $next($request);
        }

        /**
         * This assumes it is behind auth at all times
         * so if the above fails we then let auth manage it
         */
        if ($this->auth->guest()) {
            if ($request->ajax()) {
                return response('Unauthorized.', 401);
            } else {
                return redirect()->guest('auth/login');
            }
        }

        return $next($request);
    }

    /**
     * Laravel 5.2 uses vendor/laravel/framework/src/Illuminate/Auth/TokenGuard.php:66 and then
     * vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider.php:87 to get
     * a user based on the password
     *
     * @NOTE
     * We can load the user if we want to manage scopes/roles etc but right now it is
     * just pass fail
     */
    public function hasMatchingToken($token)
    {
        if($user = User::where('api_token', $token)->first())
            return true;
    }
}

And plug that into the Kernel app/Http/Kernel.php

<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
    ];
    /**
     * The application's route middleware.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.token' => \App\Http\Middleware\ApiToken::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    ];
}

Tests

You can see the tests tests/UserTokenTest.php

<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class UserTokenTest extends TestCase
{
    use DatabaseMigrations;
    /**
     * @test
     */
    public function should_pass_auth_with_no_token_since_they_auth()
    {
        $user = factory(\App\User::class)->create();
        $this->be($user);
        $results = $this->call('GET', '/example/auth_token');
        $this->assertEquals(200, $results->status());
        $this->assertEquals("foo", $results->getContent());
    }
    /**
     * @test
     */
    public function should_auth_user_since_they_have_token()
    {
        $user = factory(\App\User::class)->create();
        $results = $this->call('GET', sprintf("/example/auth_token?api_token=%s", $user->api_token));
        $this->assertEquals(200, $results->status());
        $this->assertEquals("foo", $results->getContent());
    }
    /**
     * @test
     */
    public function should_fail_user_no_token_no_auth()
    {
        $results = $this->call('GET', '/example/auth_token');
        $this->assertEquals(302, $results->status());
    }
}

Console Commands to Make Tokens

You can see the commands app/Console/Commands/UserTokenCrud.php

<?php
namespace App\Console\Commands;
use App\User;
use Illuminate\Console\Command;
use Ramsey\Uuid\Uuid;
class UserTokenCrud extends Command
{
    /**
     * @TODO
     * Show, Update, Delete
     */
    protected $signature = 'cat:create-token {user_email} {--show=false : Just show the token} ';
    protected $description = 'Create the token for the user';
    public function __construct()
    {
        parent::__construct();
    }
    public function handle()
    {
        try
        {
            $user = User::where('email', $this->argument('user_email'))->first();
            if(!$user)
                throw new \Exception(sprintf("User not found for %s", $this->argument("user_email")));
            if($this->option('show'))
            {
                $this->info(sprintf("User token is %s", $user->api_token));
                return false;
            }
            $token = Uuid::uuid4()->toString();
            $user->api_token = $token;
            $user->save();
            $this->info(sprintf("User with email %s now has token %s", $user->email, $token));
        }
        catch(\Exception $e)
        {
            $this->error(sprintf("Error finding user %s", $e->getMessage()));
        }
    }
}

Plug that into the Console Kernel Console/Kernel.php

<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //\App\Console\Commands\Inspire::class,
        \App\Console\Commands\UserTokenCrud::class
    ];
    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        //$schedule->command('inspire')
                 ->hourly();
    }
}

Allowing to update, delete and see tokens

Example Routes

Route::group(['middleware' => 'auth.token'], function () {
    Route::get('example/auth_token', function () {
        return "foo";
    });
});

I added some testing / example routes. Once you have your api_token give them a try

  • /example/auth_token?api_token=foo to show the API Token working
  • /s3?api_token=foo
  • /rds?api_token=foo
  • /dynamodb?api_token=foo

Note the token might change if you seed the database. You can ssh into the server and run

php artisan cat:create-token foo@gmail.com --show

To get the token.