Authentication and Laravel Airlock
Tags: laravel, vue
Published: 2020-01-21 09:26:59

HTTP requests are stateless. To authenticate a user, you need to explicitly mention who the user is on every request. This can be done by sending a token  that has information about the user, or sending a session ID that the server can use to find the user.

Tokens are a flexible way to authenticate, but you need to worry about where on the client side you want to securely store that token. Specially if the client is a JS application. On the other hand, sessions are stored on the server side so they are more safe. However, you need to worry about the storage size and the fact that it's only available to applications running on the same root domain.

##Laravel Airlock

Airlock is a lightweight authentication system for Laravel. You can use it to ensure requests to your API have a valid token or authentication session.

Consider a JavaScript frontend hosted on the same domain of the API, or a subdomain. With Airlock, you can authenticate requests to your API routes using the regular stateful web guard. Your frontend will need to make a POST request to the `/login` route and if the credentials are correct, Laravel will store a session containing the user ID that'll be used for authenticating all future requests.

What Airlock does is make sure your API routes are stateful if the requests are coming from a trusted source. Inside the `EnsureFrontendRequestsAreStateful` middleware, Airlock checks if the request is coming from a domain that you've previously configured in a `airlock.stateful` configuration value. In that case it'll send the request through these middleware:

- EncryptCookies
- AddQueuedCookiesToResponse
- StartSession
- VerifyCsrfToken

This will allow the regular web guard shipped with laravel to work, since it needs access to your session. If the requests are not "stateful", sessions won't be accessible.

All you need to do now is change the authentication guard in your api.php routes file from `auth:api` to `auth:airlock`. This guard will check if there's an authentication session available and allow the request to pass. No tokens are stored in your frontend, no tokens are sent with the request, just regular highly secure session-based authentication.

## Session Configuration

Airlock also ensures your sessions are stored securely by setting two configuration values:

- session.http_only: true
- session.same_site: lax

The first setting ensures that browsers cannot access the session ID stored in your cookies, only your backend can. The second ensures that the cookie will be sent only if the user is on your site; not viewing it via iframe or making an ajax request from a different host, etc...

## The Session ID

The client making the request must be able to send the session ID, for that you need to do a couple of things:

1. Set a proper value for the session.domain configuration of the application running your API. If you set it to `.domain.com`, all requests coming from that domain or any subdomain will have the session ID and will be able to make the request.
2. Set the `withCredentials` property of your HTTP client to true. This will instruct the client to include the cookies in the request. Otherwise it won't be included if the SPA is on a different subdomain.

That's why you can't have the API hosted in `domain.com` while the SPA is on `another-domain.com`. They both need to be on the same domain so they get the same session ID.

## CSRF Protection

By default, all POST/PATCH/PUT/DELETE requests to your api routes are allowed. However, since Airlock authenticates your users using a session, we need to ensure that these requests are coming from your SPA, not any other 3rd party claiming to be the SPA. Airlock adds the `VerifyCsrfToken` middleware to accomplish that.

Before authenticating the user, you need to make a GET request to `/airlock/csrf-cookie`. The response will include the `XSRF-TOKEN` cookie which will be stored in your browser and used by your HTTP client (e.g. axios) in future requests.

Laravel will read the token attached to the request headers and compare it with the token stored in your session. 

##CORS

Modern web browsers have security policies in place to protect users from hijacking. If you're visiting `domain.com` and that site is trying to make a request to `another-domain.com`, browsers make sure that `another-domain.com` doesn't mind such request.

If you have your API on api.domain.com and the SPA on spa.domain.com, you need to explicitly allow requests from your SPA to your API since they aren't on the same domain.

You can install [fruitcake/laravel-cors](https://github.com/fruitcake/laravel-cors) to help you with that.

Here's how you may configure it:

```php
return [
 'paths' => [
  'api/*', 
  'login', 
  'airlock/csrf-cookie'
 ],

 'allowed_origins' => [
  'https://spa.domain.com',
  'https://third.party.com'
 ],

 'supports_credentials' => true,
];
```

The first attribute enables CORS for the specified paths. All CORS rules we set will only be applied to these paths.

Next, we'll allow access to only a set of origins that we trust.

Finally we instruct Laravel to send the `Access-Control-Allow-Credentials` header in every response, this will make browsers share the cookies sent with the JavaScript app running.

### Issuing Tokens

You can only authenticate users using sessions if they are using a javascript application running on the same domain/subdomain as your API. For this reason, Airlock allows you to issue personal access tokens for apps and devices that won't have access to the session.

```php
$user->createToken(
 'laravel-forge', 
 ['server:create', 'server:delete']
);
```

Using this piece of code, you're creating a token named laravel-forge that has the ability to create and delete servers.

In your API, you can check a token ability using:

```php
$user->tokenCan('server:create');
```

You can also revoke the token using:

```php
$user->tokens()->whereName('laravel-forge')->delete();
```

Or revoke the currently used token (log the user out):

```php
auth()->user()->currentAccessToken()->delete();
```

Tokens are hashed using SHA-256 hashing and stored in a database table. Airlock will check the token sent in an Authorization header and make sure it exists in the database and is still valid. You can configure the token expiration by setting airlock.expiration.

## JSON Web Tokens

Tokens generated by Airlock are not JWT. The value you path to the Authorization header is a random string that represents the token key in the database. All details about the token is in the database row, not on on the token itself. This makes it easier to update the token name & abilities by updating the database record.

##Passport

You can use Airlock instead of passport if your application doesn't need the Client Credential grant to allow machine-to-machine communication or the Authorization Code grant. These types of communication require more advanced authentication techniques that Airlock is not built to handle.

In all other scenarios, Airlock is a really good option for authenticating your users without having to setup an entire OAuth2 server implementation.