/ laravel

I Modified The Token Authentication in Laravel / Spark!

Okay, that was yet another case of “programming by next error thrown”. It was frustrating. But also hilarious. Hilarious in the sense that I can’t believe how much code I had to shovel to implement something seemingly simple.

Background: I set up a Laravel / Spark project. Out of the box it gives me the normal user authentication method of username and password, AND I can use the REST API with tokens! Tokens are awesome, it basically replaces the username / password, so I can make authenticated requests to the API and know who the user is!

The token itself is... one random string of 60 characters. For my needs that wasn’t good enough. I’m building the logging service, so I want to know which site the logs come from, whether that site has a token, and whether it was THAT specific site that sent the request. For that I needed two more things in the tokens:

  • a secret
  • a site

The way I document this is a recreation of my thought process and actions as I got to the solution. Occasionally my steps will not make sense as I didn’t change enough things, but I will bump into those problems. I am purposefully not doing EVERYTHING at once, because I want you to understand the reasons and what led me to change them and how I investigated and figured things out.

I was given a bunch of articles about implementing your own custom guard and custom API authentication in Laravel, but all of them are very much like this:

how to draw an owl

Fig1: Draw two circles

Fig2: Draw the rest of the damn Owl

I’m always missing the steps in between, so I strive for writing an article that will tell you those. It’s going to be long.

Onwards!

By the way, all the logical steps are in the commit messages in the repository that accompanies this article: https://github.com/javorszky/articlesaas/commits/master.

Step 0: What do I want end up with?

For this post to have any sort of meaning, an end goal is needed. Those are

  • a way to store site and secret on a token
  • a UI to support that
  • UI to display the list of tokens with site and secret and token information so we can refer back to them

Everything in this post will go towards supporting these three things.

Step 1: Database migration

The post assumes that you got Spark up and running, meaning you configured the database access details and made at least one successful initial migration.

Modifying the api_tokens table was straightforward. Just open up the migration file for the tokens in database/migrations/, add the two new column types, slightly modify the index, and refresh the migration. DONE.

// /database/migrations/2017_03_17_135004_create_api_tokens_table.php

Schema::create('api_tokens', function (Blueprint $table) {
    $table->string('id')->primary();
    $table->integer('user_id');
    $table->string('name');
    $table->string('site', 191)->nullable(); // added
    $table->string('token', 100)->unique();
    // $table->string('secret', 100)->unique(); // added
    $table->string('secret', 100)->nullable(); // added
    $table->text('metadata');
    $table->tinyInteger('transient')->default(0);
    $table->timestamp('last_used_at')->nullable();
    $table->timestamp('expires_at')->nullable();
    $table->timestamps();

    $table->index(['user_id', 'expires_at', 'site']);
});

Then rerun the migration with php artisan migrate:refresh. Reason I need the nullable on the site is because initially I will not be passing anything to it, and I want the database to not complain about it. Same goes for secret. Once we get to the point where we can actually store the data, we’ll modify the migrations again.

Step 2: UI

This wasn’t too hard either. Needed to modify the .blade file for the create api token action, and some .js files for Vue magick.

2.1 .blade

That one lives in resources/views/vendor/spark/settings/api/create-token.blade.php. Added a new input. Basically copied the bit about the token name, and renamed “name” into “site”:

Before:

// /resources/views/vendor/spark/settings/api/create-token.blade.php
...
<!-- Token Name -->
<div class="form-group" :class="{'has-error': form.errors.has('name')}">
    <label class="col-md-4 control-label">Token Name</label>

    <div class="col-md-6">
        <input type="text" class="form-control" name="name" v-model="form.name">

        <span class="help-block" v-show="form.errors.has('name')">
            @{{ form.errors.get('name') }}
        </span>
    </div>
</div>

<!-- Mass Ability Assignment / Removal -->
...

After:

// /resources/views/vendor/spark/settings/api/create-token.blade.php
...
<!-- Token Name -->
<div class="form-group" :class="{'has-error': form.errors.has('name')}">
    <label class="col-md-4 control-label">Token Name</label>

    <div class="col-md-6">
        <input type="text" class="form-control" name="name" v-model="form.name">

        <span class="help-block" v-show="form.errors.has('name')">
            @{{ form.errors.get('name') }}
        </span>
    </div>
</div>

<!-- Token Site -->
<div class="form-group" :class="{'has-error': form.errors.has('site')}">
    <label class="col-md-4 control-label">Token Site</label>

    <div class="col-md-6">
        <input type="text" class="form-control" name="site" v-model="form.site">

        <span class="help-block" v-show="form.errors.has('site')">
            @{{ form.errors.get('site') }}
        </span>
    </div>
</div>

<!-- Mass Ability Assignment / Removal -->
...

2.2 .js Vue

I also had to mess with the vue part of this. There were two files:

resources/assets/js/spark-components/settings/api/create-token.js
resources/assets/js/spark-components/settings/api/tokens.js

2.2.1 create-token.js

This was the original content of the file:

// /resources/assets/js/spark-components/settings/api/create-token.js

var base = require('settings/api/create-token');

Vue.component('spark-create-token', {
    mixins: [base]
}

Turns out that require('settings/api/create-token'); refers to a source file in spark; specifically this one: spark/resources/assets/js/settings/api/create-token.js. That has an actual Vue object defined with methods and everything. The great thing about Vue (though I haven’t used it much yet), is that I can just override some of those things, so my create-token.js file under the resources directory ended up being this:

// /resources/assets/js/spark-components/settings/api/create-token.js

var base = require('settings/api/create-token');

Vue.component('spark-create-token', {
    mixins: [base],
    methods: {
    	/**
         * Reset the token form back to its default state.
         */
        resetForm() {
            this.form.name = '';
            this.form.site = '';

            this.assignDefaultAbilities();

            this.allAbilitiesAssigned = false;
        }
    }
});

I’ve added the this.form.site = ''; to the resetForm method.

Similarly I updated the tokens.js file to be:

// /resources/assets/js/spark-components/settings/api/tokens.js

var base = require('settings/api/tokens');

Vue.component('spark-tokens', {
    mixins: [base],
    /**
     * The component's data.
     */
    data() {
        return {
            updatingToken: null,
            deletingToken: null,

            updateTokenForm: new SparkForm({
                name: '',
                site: '',
                abilities: []
            }),

            deleteTokenForm: new SparkForm({})
        }
    }
});

The new field now shows up, and resets, but it doesn’t actually do anything yet. As in, I can type in the box and hit Create, but it won’t actually save the site name. After the changes above, this is what I see:

create API with site field

Note: You need to run npm run dev though so the Javascript files also recompile for the javascript / autoclear changes to take effect.

Step 3: Saving the site on the token

3.1 Investigation

3.1.1 Routes

First I needed to see what happens when I click the button. It sends a POST to /settings/api/token. I find that route in Spark’s own routing file, found at spark/src/Http/routes.php. That one points to a bunch of other routes that deal with tokens. All in all these:

// /spark/src/Http/routes.php
...
$router->put('/spark/token', 'TokenController@refresh');
$router->get('/settings/api/tokens', 'Settings\API\TokenController@all');
$router->post('/settings/api/token', 'Settings\API\TokenController@store');
$router->put('/settings/api/token/{token_id}', 'Settings\API\TokenController@update');
$router->get('/settings/api/token/abilities', 'Settings\API\TokenAbilitiesController@all');
$router->delete('/settings/api/token/{token_id}', 'Settings\API\TokenController@destroy');
...

I don’t deal with the 2FA routes right now (others in the route file that deal with tokens). I need to find what file TokenController@store is. CMD+T brings up fuzzy search, and typing TokenController gives me two files with the same name. I want the one that has Settings/API folder in the path, because that’s what the routing wants. Routing is weird as it has elements of folder structure / namespace / classname.

3.1.2 Settings/API/TokenController

The store method looks like this:

// /spark/src/Http/Controllers/Settings/API/TokenController.php

public function store(CreateTokenRequest $request)
{
    $data = count(Spark::tokensCan()) > 0 ? ['abilities' => $request->abilities] : [];

    return response()->json(['token' => $this->tokens->createToken(
        $request->user(), $request->name, $data
    )->token]);
}

Okay, it calls createToken on whatever tokens is there. That I know is an instance of TokenRepository, because the __construct is this:

// /spark/src/Http/Controllers/Settings/API/TokenController.php

public function __construct(TokenRepository $tokens)
{
    $this->tokens = $tokens;

    $this->middleware('auth');
}

CMD+T searching for TokenRepository again brings up two files. We need the one that does not have Contract in its path as one of them (the non-contract one) implements the other one (the contract one).

3.1.3 Repositories/TokenRepository

We now have the file and the actual createToken method.

// /spark/src/Repositories/TokenRepository.php
...
public function createToken($user, $name, array $data = [])
{
    $this->deleteExpiredTokens($user);

    return $user->tokens()->create([
        'id' => Uuid::uuid4(),
        'user_id' => $user->id,
        'name' => $name,
        'token' => str_random(60),
        'metadata' => $data,
        'transient' => false,
        'expires_at' => null,
    ]);
}
...

Seems like I can pass in the site name from the front end with $site as the third argument, which I can grab with $request->site in TokenController.

3.2 Implementing our own TokenController

3.2.1 Routes

First step is to copy Spark’s token routes to our app’s routes. Spark’s routes live in spark/src/Http/routes.php. The file to copy to is this one:

// /routes/web.php
...
Route::get('/', 'WelcomeController@show');

Route::get('/home', 'HomeController@show');
...

Became

// /routes/web.php
...
$router->get('/', 'WelcomeController@show');

$router->get('/home', 'HomeController@show');

$router->put('/spark/token', 'TokenSecretController@refresh');
$router->get('/settings/api/tokens', 'Settings\API\TokenSecretController@all');
$router->post('/settings/api/token', 'Settings\API\TokenSecretController@store');
$router->put('/settings/api/token/{token_id}', 'Settings\API\TokenSecretController@update');
$router->get('/settings/api/token/abilities', 'Settings\API\TokenSecretAbilitiesController@all');
$router->delete('/settings/api/token/{token_id}', 'Settings\API\TokenSecretController@destroy');

...

I changed the static Route::get methods to $route->get, and used the dynamic versions going forward. Reason I can do that is because $router is defined / passed on to the router file in app/Providers/RouteServiceProvider.php.

3.2.2 Controllers

I’ve also changed the name of the controller from TokenController to TokenSecretController, TokenAbilitiesController to TokenSecretAbilitiesController, and there was another TokenController, which was NOT under Settings/API. The name of that one also got changed to TokenSecretController.

And because RouteServiceController declares the default namespace to be App\Http\Controllers, I needed to copy Spark’s files, rename them, and modify the namespaces a bit.

So /spark/src/Http/Controllers/Settings/API/TokenController.php became /app/Http/Controllers/Settings/API/TokenSecretController.php, and their namespaces changed from Laravel\Spark\Http\Controllers\Settings\API to App\Http\Controllers\Settings\API.

We will need to move all these files across, but let’s go step by step. It WILL throw a LOT of errors at this point.

3.2.2.1 TokenSecretControllers (both of them)

As mentioned above, I pretty much just copied the /spark/src/Http/Controllers/Settings/API/TokenController.php to /app/Http/Controllers/Settings/API/TokenSecretController.php and changed the header of the file from:

// /app/Http/Controllers/Settings/API/TokenSecretController.php

namespace Laravel\Spark\Http\Controllers\Settings\API;

use Laravel\Spark\Token;
use Laravel\Spark\Spark;
use Illuminate\Http\Request;
use Laravel\Spark\Http\Controllers\Controller;
use Laravel\Spark\Contracts\Repositories\TokenRepository;
use Laravel\Spark\Http\Requests\Settings\API\CreateTokenRequest;
use Laravel\Spark\Http\Requests\Settings\API\UpdateTokenRequest;

class TokenController extends Controller
...

To:

// /app/Http/Controllers/Settings/API/TokenSecretController.php

namespace App\Http\Controllers\Settings\API; // change here

use Laravel\Spark\Token;
use Laravel\Spark\Spark;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller; // change here
use Laravel\Spark\Contracts\Repositories\TokenRepository;
use Laravel\Spark\Http\Requests\Settings\API\CreateTokenRequest;
use Laravel\Spark\Http\Requests\Settings\API\UpdateTokenRequest;

class TokenSecretController extends Controller // change here

For the one that's just in /Controllers, I changed

// /app/Http/Controllers/TokenSecretController.php

namespace Laravel\Spark\Http\Controllers;

use Illuminate\Http\Request;
use Laravel\Spark\Contracts\Repositories\TokenRepository;

class TokenController extends Controller
{
...

to

// /app/Http/Controllers/TokenSecretController.php

namespace App\Http\Controllers; // changed here

use Illuminate\Http\Request;
use Laravel\Spark\Contracts\Repositories\TokenRepository;

class TokenSecretController extends Controller // changed here
{
...
3.2.2.2 TokenSecretAbilitiesController

Even though we’re not going to use abilities, much of the code still depends on it, so I had to move it as well.

So /spark/src/Http/Controllers/Settings/API/TokenAbilitiesController.php became /app/Http/Controllers/Settings/API/TokenSecretAbilitiesController.php, and its header changed from

// /app/Http/Controllers/Settings/API/TokenSecretAbilitiesController.php

namespace Laravel\Spark\Http\Controllers\Settings\API;

use Laravel\Spark\Spark;
use Laravel\Spark\Http\Controllers\Controller;

class TokenAbilitiesController extends Controller
{
...

to

// /app/Http/Controllers/Settings/API/TokenSecretAbilitiesController.php

namespace App\Http\Controllers\Settings\API; // changed here

use Laravel\Spark\Spark;
use App\Http\Controllers\Controller; // changed here

class TokenSecretAbilitiesController extends Controller // changed here
{
...

With that in place, I can click the button! And voilá!

pushing the button now uses our TokenSecretController

It’s using our controllers. An easy way to test that is to put a

print_r( 'this' );
die();

inside TokenSecretController@store and trying to save a new token. The API Token modal will not appear and the network tab will tell you the response to /token was this.

3.2.3 Repositories

Let’s move the Repository files across. Opening up Settings/API/TokenSecretController.php tells us it’s using TokenRepository in its __construct. That one, according to the header, is apparently Laravel\Spark\Contracts\Repositories\TokenRepository. I’ve copied the file from spark/src/Contracts/Repositories/TokenRepository.php to app/Contracts/Repositories/TokenSecretRepository.php, and changed the header of that file from

// /app/Contracts/Repositories/TokenSecretRepository.php

namespace Laravel\Spark\Contracts\Repositories;

use Laravel\Spark\Token;
use Illuminate\Database\Eloquent\Collection;

interface TokenRepository
...

to

// /app/Contracts/Repositories/TokenSecretRepository.php

namespace App\Contracts\Repositories; // changed this

use Laravel\Spark\Token;
use Illuminate\Database\Eloquent\Collection;

interface TokenSecretRepository // changed this
...

In the controller file I also had to implement a few changes:

// /app/Http/Controllers/Settings/API/TokenSecretController.php

namespace App\Http\Controllers\Settings\API;

use Laravel\Spark\Token;
use Laravel\Spark\Spark;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Contracts\Repositories\TokenSecretRepository; // changed here
use Laravel\Spark\Http\Requests\Settings\API\CreateTokenRequest;
use Laravel\Spark\Http\Requests\Settings\API\UpdateTokenRequest;

class TokenSecretController extends Controller
{
...
public function __construct(TokenSecretRepository $tokens) // changed here
    {
...

And that gets us an error! You have to look into the response to the API, however the error is going to be this:

Target [App\Contracts\Repositories\TokenSecretRepository] is not instantiable while building [App\Http\Controllers\Settings\API\TokenSecretController].

That didn’t make sense as that’s exactly what Spark is doing behind the scenes. A bit of Googling, a Stack Overflow answer led me to look in Spark’s providers. And sure enough, in SparkServiceProvider:

// /spark/src/Providers/SparkServiceProvider.php
...
    private function registerApiTokenRepository()
    {
        $concrete = class_exists('Laravel\Passport\Passport')
                        ? 'Laravel\Spark\Repositories\PassportTokenRepository'
                        : 'Laravel\Spark\Repositories\TokenRepository';

        $this->app->singleton('Laravel\Spark\Contracts\Repositories\TokenRepository', $concrete);
    }
...

Uh, so basically it means any time there’s a reference to the Contract (interface), then the class there, the actual repository should be instantiated instead.

I copied the actual repository across with my usual changes, and this is what ended up being the header of the file:

// /app/Repositories/TokenSecretRepository.php

namespace App\Repositories;

use Carbon\Carbon;
use Ramsey\Uuid\Uuid;
use Laravel\Spark\JWT;
use Laravel\Spark\Token;
use App\Contracts\Repositories\TokenSecretRepository as Contract;

class TokenSecretRepository implements Contract
{
...

I’ve also added the following to my AppServiceProvider:

// /app/Providers/AppServiceProvider.php
...
    public function register()
    {
        $this->app->singleton('App\Contracts\Repositories\TokenRepository', 'App\Repositories\TokenRepository');
        //
    }

And now everything works! So far.

3.2.3 Modifying code

Now that everything is in place, we can start doing some actual work.

3.2.3.1 TokenSecretController

Time to modify the store method. This is on the Settings/API/TokenSecretController version.

// /app/Http/Controllers/Settings/API/TokenSecretController.php
...
    public function store(CreateTokenRequest $request)
    {
        $data = count(Spark::tokensCan()) > 0 ? ['abilities' => $request->abilities] : [];

        return response()->json(['token' => $this->tokens->createToken(
            $request->user(), $request->name, $request->site, $data // changed this
        )->token]);
    }

I’ve added $request->site after $request->name. That would call createToken on TokenSecretRepository, so let’s change that method too to accept the site. And while we’re at it, let’s store the secret too:

// /app/Repositories/TokenSecretRepository.php
...
    public function createToken($user, $name, $site, array $data = []) // changed this
    {
        $this->deleteExpiredTokens($user);

        return $user->tokens()->create([
            'id' => Uuid::uuid4(),
            'user_id' => $user->id,
            'name' => $name,
            'site' => $site, // added this
            'token' => 'tk_' . str_random(60), // changed this
            'secret' => 'sc_' . str_random(60), // added this
            'metadata' => $data,
            'transient' => false,
            'expires_at' => null,
        ]);
    }
...

An error!

Declaration of App\Repositories\TokenSecretRepository::createToken($user, $name, $site, array $data = Array) must be compatible with App\Contracts\Repositories\TokenSecretRepository::createToken($user, $name, array $data = Array)

Because interfaces and inheritance and whatnot... Changing the contract as well then:

// /app/Contracts/Repositories/TokenSecretRepository.php
...
    public function createToken($user, $name, $site, array $data = []); // changed here
...

Reload the page, and try again, and:

token with secret saves

And let’s see whether it’s stored in the database:

it is stored in the database

Step 4: Tidy up so far

4.1 Lock in migrations

We figured out how to store site and secret, so now we can remove the nullable types from the migration. Our new migration is going to be:

// /database/migrations/2017_03_17_135004_create_api_tokens_table.php

Schema::create('api_tokens', function (Blueprint $table) {
    ...
    $table->string('site', 191);
    $table->string('token', 100)->unique();
    $table->string('secret', 100)->unique();
    ...
});

Refresh the migrations (yes, you will lose all data contained within), register again, and try creating a token. It works! Unless you don’t fill out the site, at which point everything comes crashing down:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'site' cannot be null (SQL: insert into `api_tokens` (`id`, `user_id`, `name`, `site`, `token`, `secret`, `metadata`, `transient`, `expires_at`, `updated_at`, `created_at`) values ([...]))

4.2 Add validation to site field

To avoid running into this problem, we can add front end validation to the create token form, which would tell us that the site field is required, AND it needs to be a valid URL.

According to the Laravel documentation on validation, that should happen on Request, before the thing is being stored. Our thing (token) is being stored in TokenSecretController@store within the Settings/API namespace, but there’s no sign of validation there.

The docs also mention custom form request validation, and it says all we need to do is typehint the request in the store method, and Laravel takes care of the rest. As luck would have it, the store method’s signature looks like this:

// /app/Http/Controllers/Settings/API/TokenSecretController.php
...
    public function store(CreateTokenRequest $request)
    {
...

It’s already typehinted. Finding the CreateTokenRequest class is a CMD+T away (files are named after classes) which yields to a file with an empty class in spark/src/Http/Requests/Settings/API/CreateTokenRequest.php. That class extends the more generic TokenRequest one, and that one lives in spark/src/Http/Requests/Settings/API/TokenRequest.php. There’s a method in that file called validator that looks like it does what we need. Let’s copy this file, and all dependent files to our app away from Spark. Technically we could just extend THIS class with our own and only overwrite where needed, but because we have other classes also extending this one, I find it better to replace them altogether.

4.2.1 TokenRequest

Therefore spark/src/Http/Requests/Settings/API/TokenRequest.php became spark/src/Http/Requests/Settings/API/TokenSecretRequest.php, and the header changes from:

// /spark/src/Http/Requests/Settings/API/TokenRequest.php

namespace Laravel\Spark\Http\Requests\Settings\API;

use Laravel\Spark\Spark;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Http\FormRequest;

class TokenRequest extends FormRequest
{
...

to

// /app/Http/Requests/Settings/API/TokenSecretRequest.php

namespace App\Http\Requests\Settings\API;

use Laravel\Spark\Spark;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Http\FormRequest;

class TokenSecretRequest extends FormRequest
{

With that done, we can add the rule to the validator method:

// /app/Http/Requests/Settings/API/TokenSecretRequest.php
...
    public function validator()
    {
        return $this->validateAbilities(Validator::make($this->all(), [
            'name' => 'required|max:255',
            'site' => 'bail|required|url|max:191' // added this
        ], $this->messages()));
    }
...

4.2.2 CreateTokenRequest

Similarly spark/src/Http/Requests/Settings/API/CreateTokenRequest.php became /app/Http/Requests/Settings/API/CreateTokenSecretRequest.php and its entire content changed from

namespace Laravel\Spark\Http\Requests\Settings\API;

class CreateTokenRequest extends TokenRequest
{
    //
}

to

namespace App\Http\Requests\Settings\API;

class CreateTokenSecretRequest extends TokenSecretRequest
{
    //
}

4.2.3 UpdateTokenRequest

And spark/src/Http/Requests/Settings/API/UpdateTokenRequest.php became /app/Http/Requests/Settings/API/UpdateTokenSecretRequest.php and its entire content changed from

namespace Laravel\Spark\Http\Requests\Settings\API;

class UpdateTokenRequest extends TokenRequest
{
    //
}

to

namespace App\Http\Requests\Settings\API;

class UpdateTokenSecretRequest extends TokenSecretRequest
{
    //
}

4.2.4 Changes to TokenSecretController

We need to tell Laravel to use our new classes. Therefore

// /app/Http/Controllers/Settings/API/TokenSecretController.php
...
    public function store(CreateTokenRequest $request)
...
    public function update(UpdateTokenRequest $request, $tokenId)
...

needs to change to

// /app/Http/Controllers/Settings/API/TokenSecretController.php
...
    public function store(CreateTokenSecretRequest $request)
...
    public function update(UpdateTokenSecretRequest $request, $tokenId)
...

Trying that will fail because Laravel will complain that it can’t find these classes. The header of the file needs to be adjusted too from:

// /app/Http/Controllers/Settings/API/TokenSecretController.php

namespace App\Http\Controllers\Settings\API;

use Laravel\Spark\Token;
use Laravel\Spark\Spark;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Contracts\Repositories\TokenSecretRepository;
use Laravel\Spark\Http\Requests\Settings\API\CreateTokenRequest;
use Laravel\Spark\Http\Requests\Settings\API\UpdateTokenRequest;

class TokenSecretController extends Controller
{
...

to

// /app/Http/Controllers/Settings/API/TokenSecretController.php

namespace App\Http\Controllers\Settings\API;

use Laravel\Spark\Token;
use Laravel\Spark\Spark;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Contracts\Repositories\TokenSecretRepository;
use App\Http\Requests\Settings\API\CreateTokenSecretRequest; // changed this
use App\Http\Requests\Settings\API\UpdateTokenSecretRequest; // changed this

class TokenSecretController extends Controller
{
...

Not only did we have to change the Laravel\Spark bit to App, but also the CreateTokenRequest to CreateTokenSecretRequest bit too. Also with the update class. I may or may not have run into this.

Yay!

required throws an error

not a URL throws an error too

Step 5: Showing the tokens / secrets on the API page

Right now the token, and only the token, is displayed once, and the secret and site are definitely hidden. Well, not shown.

Is it possible to show the full token information that we want? Those being:

  • name
  • last used
  • token
  • secret

Currently only name and last used are available.

5.1 Tokens blade template

With a few modifications I can change the way the tokens are displayed. Because tokens and secrets are rather long strings, it doesn’t make sense to display them in table with column, so I’ll need one column with lots of rows and each token will occupy one row with all its data. It’s not going to be pretty, but that’s not the point at the moment.

The .blade file I need to poke is resources/views/vendor/spark/settings/api/tokens.blade.php. The current display for every record in the tokens is this code:

<thead>
    <th>Name</th>
    <th>Last Used</th>
    <th></th>
    <th></th>
</thead>

<tbody>
    <tr v-for="token in tokens">
        <!-- Name -->
        <td>
            <div class="btn-table-align">
                @{{ token.name }}
            </div>
        </td>

        <!-- Last Used At -->
        <td>
            <div class="btn-table-align">
                <span v-if="token.last_used_at">
                    @{{ token.last_used_at | datetime }}
                </span>

                <span v-else>
                    Never
                </span>
            </div>
        </td>

        <!-- Edit Button -->
        <td>
            <button class="btn btn-primary" @click="editToken(token)">
                <i class="fa fa-pencil"></i>
            </button>
        </td>

        <!-- Delete Button -->
        <td>
            <button class="btn btn-danger-outline" @click="approveTokenDelete(token)">
                <i class="fa fa-times"></i>
            </button>
        </td>
    </tr>
</tbody>

Removing the many columns and shoving all information in one row yields:

<tbody>
    <tr v-for="token in tokens">
        <!-- Name -->
        <td>
            <div class="btn-table-align">
                <strong>Name: </strong>@{{ token.name }},

                <strong>Last used: </strong>
                <span v-if="token.last_used_at">
                    @{{ token.last_used_at | datetime }}
                </span>

                <span v-else>
                    Never
                </span>
            </div>

            <div class="btn-table-align">
                <strong>Site: </strong> @{{ token.site }}
            </div>

            <div class="btn-table-align">
                <strong>Token: </strong> @{{ token.token }}
            </div>

            <div class="btn-table-align">
                <strong>Secret: </strong>@{{ token.secret }}
            </div>
            <button class="btn btn-primary" @click="editToken(token)">
                <i class="fa fa-pencil"></i>
            </button>
            <button class="btn btn-danger-outline" @click="approveTokenDelete(token)">
                <i class="fa fa-times"></i>
            </button>
        </td>
    </tr>
</tbody>

I removed the <thead>, as it’s not needed right now. It looks like this:

reworked tokens list
Not bad, but... token is missing. If you look at the request that the site sends to /settings/api/tokens, you’ll see that the token property is simply missing from the returned data:

token property is missing from the tokens in the collection

5.2 Tokens data

The request calls TokenSecretController@all (the on under Settings/API), which calls $this->tokens->all, so essentially TokenSecretRepository->all, which calls $user->tokens()->....

I assume that $user in this case is the User model, of which I have one in the app directory. That one extends SparkUser which is just Laravel\Spark\User found in the file spark/src/User.php. That one has a trait on it: HasApiTokens.

Traits are an easy way to sideload additional methods onto classes. They are kind of weird to redeclare, but I digress. The HasApiTokens trait is in the file spark/src/HasApiTokens.php. That one declares a tokens method. Which means that when TokenSecretRepository calls $user->tokens()->, the method on the trait gets executed. Phew.

That one returns an Eloquent model relationship of the class Token. The trait is in the namespace Laravel\Spark, so the Token will also be there. The file, after some rummaging, is spark/src/Token.php.

And in there we can find the reason we can’t find the token on the returned data:

// /spark/src/Token.php
...
    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'token',
    ];
...

A bit of digging through the Laravel documentation produced a page on Eloquent serialisation, which describes what that property is for and how to use it.

We want to make sure to use an instance of Token that does not have that on it. Following the logic that we used so far, we need to do two things:

  1. Use a User model that has a trait that calls a custom implementation of the Token, and
  2. Have a custom implementation of Token that does not hide the token itself

5.2.1 Custom User by extenstion

App\User extends Laravel\Spark\User which extends Authenticatable, which is just another name for Illuminate\Foundation\Auth\User.

So I thought I would just write a custom trait HasApiTokenSecrets, only declare the methods that have the Token class in it, change it to my TokenSecret class which extends the Token class and redeclares the $hidden property to be empty, and we should be set.

In order to do that, in app/User.php I added a line:

// /app/User.php

namespace App;

use Laravel\Spark\User as SparkUser;

class User extends SparkUser
{
    use HasApiTokenSecrets;
...

I also created the HasApiTokenSecrets trait, with the entire file being:

// /app/HasApiTokenSecrets.php

namespace App;

trait HasApiTokenSecrets
{
    /**
     * Get all of the API tokens for the user.
     */
    public function tokens()
    {
        return $this->hasMany(TokenSecret::class);
    }

    /**
     * Set the current API token for the user.
     *
     * @param  \Laravel\Spark\Token  $token
     * @return $this
     */
    public function setToken(TokenSecret $token)
    {
        $this->currentToken = $token;

        return $this;
    }
}

And the entire TokenSecret class:

// /app/TokenSecret.php

namespace App;

use Laravel\Spark\Token;

class TokenSecret extends Token
{
    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [];
}

When I try to view the admin area, this is what I get:

no bueno, new trait does not overwrite old trait

Which means that a trait can’t overwrite something that another trait has already defined in a parent class. Conflict resolution also doesn’t work, because PHP either complains that the trait I'm disfavouring with insteadof isn’t added to the class, or I simply get a 502 Bad Gateway when using as.

Well, it was fun. Time to bypass SparkUser in its entirety.

5.2.2 Custom User by bypass

Remember, the reason we’re doing this is because we want to change what some methods do on the HasApiTokens trait. Therefore we need to change things where we control what we load onto the class. In order to achieve that I decided I’m not going to extend Laravel\Spark\User, but extend and implement all the things that extends and implements on App\User with my modifications.

I therefore merged the two User classes into App\User, and the resulting class is thus:

// /app/User.php

namespace App;

use Laravel\Spark\Billable;
use Illuminate\Support\Str;
use Illuminate\Notifications\RoutesNotifications;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Billable, HasApiTokenSecrets, RoutesNotifications;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
        'authy_id',
        'country_code',
        'phone',
        'card_brand',
        'card_last_four',
        'card_country',
        'billing_address',
        'billing_address_line_2',
        'billing_city',
        'billing_zip',
        'billing_country',
        'extra_billing_information',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'trial_ends_at' => 'datetime',
        'uses_two_factor_auth' => 'boolean',
    ];

    /**
     * Get the profile photo URL attribute.
     *
     * @param  string|null  $value
     * @return string|null
     */
    public function getPhotoUrlAttribute($value)
    {
        return empty($value) ? 'https://www.gravatar.com/avatar/'.md5(Str::lower($this->email)).'.jpg?s=200&d=mm' : url($value);
    }

    /**
     * Make the team user visible for the current user.
     *
     * @return $this
     */
    public function shouldHaveSelfVisibility()
    {
        return $this->makeVisible([
            'uses_two_factor_auth',
            'country_code',
            'phone',
            'card_brand',
            'card_last_four',
            'card_country',
            'billing_address',
            'billing_address_line_2',
            'billing_city',
            'billing_state',
            'billing_zip',
            'billing_country',
            'extra_billing_information'
        ]);
    }

    /**
     * Convert the model instance to an array.
     *
     * @return array
     */
    public function toArray()
    {
        $array = parent::toArray();

        if (! in_array('tax_rate', $this->hidden)) {
            $array['tax_rate'] = $this->taxPercentage();
        }

        return $array;
    }
}

5.2.3 Custom HasApiTokenSecrets trait

And HasApiTokenSecrets is almost an exact copy of HasApiTokens. The only difference when HasApiTokens references the Token class, this one references TokenSecret class:

// /app/HasApiTokenSecrets.php

namespace App;

trait HasApiTokenSecrets
{
    /**
     * The current authentication token in use.
     *
     * @var \App\TokenSecret
     */
    protected $currentToken;

    /**
     * Get all of the API tokens for the user.
     */
    public function tokens()
    {
        return $this->hasMany(TokenSecret::class);
    }

    /**
     * Determine if the current API token is granted a given ability.
     *
     * @param  string  $ability
     * @return bool
     */
    public function tokenCan($ability)
    {
        return $this->currentToken ? $this->currentToken->can($ability) : false;
    }

    /**
     * Get the currently used API token for the user.
     *
     * @return \App\TokenSecret
     */
    public function token()
    {
        return $this->currentToken;
    }

    /**
     * Set the current API token for the user.
     *
     * @param  \App\TokenSecret  $token
     * @return $this
     */
    public function setToken(TokenSecret $token)
    {
        $this->currentToken = $token;

        return $this;
    }
}

Save those files, and let’s look at our tokens list:

we now have tokens as well

Step 6: Make things pretty

I want to achieve two more things:

  1. Reword the modal so it doesn’t tell me it’s only showing me the token once (no longer true), and show the secret as well, and
  2. Make token / secret monospace and maybe click-to-copy

6.1 API confirmation modal work

For the confirmation modal to contain the secret as well as the token, we need to make sure that it has access to it, and that the template can deal with it.

6.1.1 modal .blade template

Because earlier we worked with the create-token.js when implementing the site name field, we know where the files are that govern the functionality. Looking into the spark/resources/assets/js/settings/api/create-token.js file, we see that create calls showToken, which puts data in a variable and shows an element: #modal-show-token. Doing a global search for that, it turns out that’s in resources/views/vendor/spark/settings/api/create-token.blade.php.

Let’s get hacking!

The important bit is the modal body:

// /resources/views/vendor/spark/settings/api/create-token.blade.php
...
<div class="modal-body">
    <div class="alert alert-warning">
        Here is your new API token. <strong>This is the only time the token will ever
        be displayed, so be sure not to lose it!</strong> You may revoke the token
        at any time from your API settings.
    </div>

    <textarea id="api-token" class="form-control"
              @click="selectToken"
              rows="5">@{{ showingToken }}</textarea>
</div>

<!-- Modal Actions -->
<div class="modal-footer">
    <button type="button" class="btn btn-primary" @click="selectToken">
    <span v-if="copyCommandSupported">Copy To Clipboard</span>
    <span v-else>Select All</span>
    </button>
    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
...

I’ve added another textarea, changed the id of that one, and shrunk them a bit. I’ve also turned showingToken from a value to an object here with the dot notation.

And I’ve slightly reworded the text in the yellow box at the top of the box:

// /resources/views/vendor/spark/settings/api/create-token.blade.php
...
<div class="modal-body">
    <div class="alert alert-warning">
        <p>Here is your new API token. You may revoke the token at any time from your API settings.</p>
        <p>
            <span v-if="copyCommandSupported">Clicking on the token or the secret will copy them on the clipboard.</span>
            <span v-else>You have to manually select and copy the token and the secret.</span>
        </p>
        <p>Don’t worry, they will remain accessible to you in the future.</p>
    </div>
    <textarea id="api-token" class="form-control"
              @click="selectToken"
              rows="3">@{{ showingToken.token }}</textarea>

    <textarea id="api-secret" class="form-control"
              @click="selectSecret"
              rows="3">@{{ showingToken.secret }}</textarea>
</div>
<!-- Modal Actions -->
<div class="modal-footer">
    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>

I also had to add a selectSecret method to select everything in the api-secret modal. Basically I copied the selectToken method but changed the id on it.

And in order to pass through two variables to the modal I had to change the create method too. The create-token.js ended up being this:

// /resources/assets/js/spark-components/settings/api/create-token.js
...
        /**
         * Create a new API token.
         */
        create() {
            Spark.post('/settings/api/token', this.form)
                .then(response => {
                    this.showToken({'token':response.token, 'secret':response.secret});

                    this.resetForm();

                    this.$parent.$emit('updateTokens');
                });
        },
        /**
         * Select the secret and copy to Clipboard.
         */
        selectSecret() {
            $('#api-secret').select();

            if (this.copyCommandSupported) {
                document.execCommand("copy");
            }
        }
...

That seems to be all of it, time to recompile, so feel free to run npm run dev. You should not have a problem with this.

Reload the page, and create a new token.

we have a token but not the secret
No secret... That means... we don’t have the property?

I stuck a console.log(response); into the create method of create-token.js (ours) just to see what the data. Turns out only the token is being sent across. The source of that is probably TokenSecretController@store:

// /app/Http/Controllers/Settings/API/TokenSecretController.php
...
    public function store(CreateTokenSecretRequest $request)
    {
        $data = count(Spark::tokensCan()) > 0 ? ['abilities' => $request->abilities] : [];

        return response()->json(['token' => $this->tokens->createToken(
            $request->user(), $request->name, $request->site, $data
        )->token]);
    }
...

So uhm... haven’t looked into this properly before, but it seems like this bit returns the entire created token data:

$this->tokens->createToken(
    $request->user(), $request->name, $request->site, $data
)

And ->token only selects the token bit of it, and the ['token' => ... ] returns that as an array.

So let’s work this around so I can send through secret as well:

// /app/Http/Controllers/Settings/API/TokenSecretController.php
...
    public function store(CreateTokenSecretRequest $request)
    {
        $data = count(Spark::tokensCan()) > 0 ? ['abilities' => $request->abilities] : [];

        $token = $this->tokens->createToken(
            $request->user(), $request->name, $request->site, $data
        );

        return response()->json([
            'token' => $token->token,
            'secret' => $token->secret
        ]);
    }
...

Now if we create a new token:

modal has token and secret too

Tiny polish on it to add labels:

// /resources/assets/js/spark-components/settings/api/create-token.js
...
<label for="api-token">Token</label>
<textarea id="api-token" class="form-control"
          @click="selectToken"
          rows="2">@{{ showingToken.token }}</textarea>

<label for="api-secret">Secret</label>
<textarea id="api-secret" class="form-control"
          @click="selectSecret"
          rows="2">@{{ showingToken.secret }}</textarea>
...

labels for token and secret present

6.1.2 Token list blade template

I ended up with this:

styled token list
I’ve turned the fields that display the token and the secret into readonly <textarea> elements with the class form-control to give it a nice styling. Added a token-display class as well which defines the font-family: monospace; so it looks like code. Could I have done it with the <pre> tag? Absolutely. I liked the look of this better though.

I also moved around some parts, like the name and last used. Spark’s front end is built using a pretty default Bootstrap styling with some sprinkled on it, but not much.

I’ve also added two methods to the textareas which will copy the token / secret when you click on it.

All in all, the resulting markup in the .blade file:

// /resources/views/vendor/spark/settings/api/tokens.blade.php
...
<tr v-for="token in tokens">
    <!-- Name -->
    <td>
        <div class="btn-table-align">
            <p class="h3">@{{ token.name }} <small>Last used:
                <span v-if="token.last_used_at">
                    @{{ token.last_used_at | datetime }}
                </span>

                <span v-else>
                    Never
                </span>
            </small></p>
        </div>

        <div class="btn-table-align">
            <strong>Site: </strong> @{{ token.site }}
        </div>

        <div class="btn-table-align">
            <strong>Token: </strong>
            <textarea readonly class="form-control token-display" rows="1" @click="selectContent($event)">@{{ token.token }}</textarea>
        </div>

        <div class="btn-table-align">
            <strong>Secret: </strong>
            <textarea readonly class="form-control token-display" rows="1" @click="selectContent($event)">@{{ token.secret }}</textarea>
        </div>

        <button class="btn btn-primary" @click="editToken(token)">
            <i class="fa fa-pencil"></i>
        </button>
        <button class="btn btn-danger-outline" @click="approveTokenDelete(token)">
            <i class="fa fa-times"></i>
        </button>
    </td>
</tr>
...

And the .less file:

// /resources/assets/less/app.less

@import "./../../../node_modules/bootstrap/less/bootstrap";

// @import "./spark/spark";
@import "./../../../vendor/laravel/spark/resources/assets/less/spark";

.token-display {
	font-family: monospace;
}

And the tokens.js file for the functionality:

// /resources/assets/js/spark-components/settings/api/tokens.js
...
    computed: {
        copyCommandSupported() {
            return document.queryCommandSupported('copy');
        }
    },
    methods: {
        /**
         * Select the secret and copy to Clipboard.
         */
        selectContent(event) {
            $(event.target).select();
            if (this.copyCommandSupported) {
                document.execCommand("copy");
            }
        }
    }
...

And that’s it.

Step +1: Let’s only allow folks to add only one site

That’s technically a unique composite index on the database. There are two sides to it:

  1. the database layer where we keep the data to be inserted into the database, though this crashes the front-end because of the 500 error, and
  2. a softer front-end validation to check for a unique user_id / site pair

The database is easy. Change this:

// /database/migrations/2017_03_17_135004_create_api_tokens_table.php
...
$table->index(['user_id', 'expires_at', 'site']);
...

to this:

// /database/migrations/2017_03_17_135004_create_api_tokens_table.php
...
$table->unique(['user_id', 'site']);
$table->index('expires_at');
...

The front end validation happens in the TokenSecretRequest. Change this:

//
...
return $this->validateAbilities(Validator::make($this->all(), [
    'name' => 'required|max:255',
    'site' => 'bail|required|url|max:191' // added this
], $this->messages()));
...

to this:

// /app/Http/Requests/Settings/API/TokenSecretRequest.php
...
return $this->validateAbilities(Validator::make($this->all(), [
    'name' => 'required|max:255',
    'site' => [
        'bail',
        'required',
        'url',
        'max:191',
        Rule::unique('api_tokens')->where(function ($query) {
            $query->where('user_id', $this->user()->id);
        })
    ] // added this
], $this->messages()));
...

This is kind of a complex rule that you can read about in the Validator documentation about uniqueness.

$this->user() refers to the current user. And now we get this when we’re trying to add the same site:

the site has already been added

And now that’s it. For reals. Check out the commits on the GitHub repository accompanying the article: https://github.com/javorszky/articlesaas/commits/master.

The End

Let me know if I could have done something better / differently. Laravel is still new to me, so I’m learning the ropes. The next project is going to be authenticating based on these. Get in touch via twitter at @javorszky.

Gabor Javorszky

I'm a freelance WordPress / WooCommerce developer focusing on Subscriptions and advanced functionality. Get in touch: @javorszky / gabor (at) javorszky (dot) co (dot) uk. Read more on the About page.

Read More