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:
- 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. - 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 to help you with that.
Here's how you may configure it:
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.
$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:
$user->tokenCan('server:create');
You can also revoke the token using:
$user->tokens()->whereName('laravel-forge')->delete();
Or revoke the currently used token (log the user out):
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.
Top comments (4)
Doesn't samesite: lax already protect against csrf tokens on evergreen browsers?
If you are 100% sure all your users are on an evergreen browser then yes. But it's always a good practice to use all the extra security layers to protect your users.
Yes that's very true.
But what stops the attacker from retrieving a fresh csrf token using the /csrf-cookie endpoint? Are there security measures in place?
If they gain access to the cookies then yes. The whole point is just adding more layers of security and following all recommendations and best practices.