/airlock

Primary LanguagePHPMIT LicenseMIT

Build Status Total Downloads Latest Stable Version License

Introduction

Laravel Airlock provides a featherweight authentication system for SPAs and simple APIs.

Installation

You may install Laravel Airlock via Composer:

composer require laravel/airlock

Next, you should publish the Airlock configuration and migration files using the vendor:publish Artisan command. The airlock configuration file will be placed in your config directory:

php artisan vendor:publish --provider="Laravel\Airlock\AirlockServiceProvider"

Finally, you should run your database migrations:

php artisan migrate

Configuration

Configuring Your Domains

If you are using Laravel Airlock to authenticate your single page application (SPA), you should configure which domains your SPA will be making requests from. You may configure these domains using the stateful configuration option in your config/airlock.php configuration file. This configuration setting determines which domains will maintain "stateful" authentication in order to make requests to your API.

Adding The Airlock Middleware

Next, you should add Airlock's middleware to your api middleware group within your app/Http/Kernel.php file:

use Laravel\Airlock\Http\Middleware\EnsureFrontendRequestsAreStateful;

'api' => [
    EnsureFrontendRequestsAreStateful::class,
    'throttle:60,1',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

Attaching The Authentication Guard

Next, you should attach the airlock authentication guard to your API routes within your routes/api.php file. This guard will ensure that incoming requests are authenticated as either a stateful authenticated requests from your SPA or contain a valid API token header if the request is from a third party:

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

If you are using Passport to authenticate other portions of your application using OAuth2, you are welcome to also use Airlock. The auth middleware allows you to specify multiple guards that will be used in sequence when attempting to authenticate incoming requests:

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

SPA Authentication

To authenticate your SPA, your SPA's login page should first make a request to the /airlock/csrf-cookie route to initialize CSRF protection for the application:

axios.defaults.withCredentials = true;

axios.get('/airlock/csrf-cookie').then(response => {
    // Login...
});

Once CSRF protection has been initialized, you should make a POST request to the typical Laravel /login route. If the request is successful, you will be authenticated and subsequent requests to your API routes will automatically be authenticated.

CORS & Cookies

If you are having trouble authenticating with your application from an SPA that executes on a separate subdomain, you have likely misconfigured your CORS or session cookie settings. You should ensure that your application's CORS configuration is returning the Access-Control-Allow-Credentials header with a value of True.

In addition, you should ensure your application's session cookie domain configuration supports any subdomain of your root domain. You may do this by prefixing the domain with a leading .:

'domain' => '.domain.com',

Authorization Private / Presence Broadcast Channels

If your SPA needs to authenticate with private / presence broadcast channels, you should place the Broadcast::routes method call within your routes/api.php file like so:

Broadcast::routes(['middleware' => ['auth:airlock']]);

Next, in order for Pusher's authorization requests to succeed, you will need to provide a custom Pusher authorizer when initializing Laravel Echo. This allows your application to configure Pusher to use the axios instance that is properly configured for cross-domain requests:

window.Echo = new Echo({
    broadcaster: "pusher",
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    encrypted: true,
    key: process.env.MIX_PUSHER_APP_KEY,
    authorizer: (channel, options) => {
        return {
            authorize: (socketId, callback) => {
                axios.post('/api/broadcasting/auth', {
                    socket_id: socketId,
                    channel_name: channel.name
                })
                .then(response => {
                    callback(false, response.data);
                })
                .catch(error => {
                    callback(true, error);
                });
            }
        };
    },
})

Issuing API Tokens

Airlock also allows you to issue API tokens / personal access tokens that may be used to authenticate API requests. The token should be included in the Authorization header as a Bearer token.

To begin issuing tokens for users, your User model should use the HasApiTokens trait:

use Laravel\Airlock\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

To issue a token, you may use the createToken method. The createToken method returns a Laravel\Airlock\NewAccessToken instance. API tokens are hashed using SHA-256 hashing before being stored in your database, but you may access the plain-text value of the token using the plainTextToken property of the NewAccessToken instance. You should display this value to the user once:

$token = $user->createToken('token-name');

return $token->plainTextToken;

You may access all of the user's tokens using the tokens Eloquent relationship provided by the HasApiTokens trait:

foreach ($user->tokens as $token) {
    //
}

Token Abilities

Airlock allows you to assign "abilities" to tokens, similar to OAuth "scopes". You may pass an array of string abilities as the second argument to the createToken method:

return $user->createToken('token-name', ['server:update'])->plainTextToken;

When handling an incoming request authenticated by Airlock, you may determine if the token has a given ability using the tokenCan method:

if ($user->tokenCan('server:update')) {
    //
}

The tokenCan method will always return true if the incoming authenticated request was from your first-party SPA.

Authenticating Mobile Applications

You may use Airlock tokens to authenticate your mobile application's requests to your API. To get started, create a route that accepts the user's email / username, password, and device name, then exchanges them for a new Airlock token. You may then store the token on your device and use it to make additional API requests:

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

Route::post('/airlock/token', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
        'device_name' => 'required'
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    return $user->createToken($request->device_name)->plainTextToken;
});

Revoking Tokens

You may "revoke" tokens by deleting them from your database using the typical Eloquent methods you are used to:

$user->tokens->each->delete();

Within your web application's UI, you may wish to list each of the user's tokens and allow the user to revoke the tokens individually as needed.

Customization

You may customize the personal access token model used by Airlock via the usePersonalAccessTokenModel methods. Typically, you should call this method from the boot method of your AppServiceProvider:

use App\Airlock\CustomPersonalAccessToken;
use App\CustomUser;
use Laravel\Airlock\Airlock;

public function boot()
{
    Airlock::usePersonalAccessTokenModel(
        CustomPersonalAccessToken::class
    );
}

Testing

While testing, the Airlock::actingAs method may be used to authenticate a user and specify which abilities are granted to their token:

use App\User;
use Laravel\Airlock\Airlock;

public function test_task_list_can_be_retrieved()
{
    Airlock::actingAs(
        factory(User::class)->create(),
        ['view-tasks']
    );

    $response = $this->get('/api/task');

    $response->assertOk();
}

If you would like to grant all abilities to the token, you should include * in your ability list:

Airlock::actingAs(
    factory(User::class)->create(),
    ['*']
);

Contributing

Thank you for considering contributing to Airlock! The contribution guide can be found in the Laravel documentation.

Code of Conduct

In order to ensure that the Laravel community is welcoming to all, please review and abide by the Code of Conduct.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

License

Laravel Airlock is open-sourced software licensed under the MIT license.