Laravel 5.6 - Passport JWT httponly cookie SPA authentication for self consuming API?
LaravelLaravel 5Csrf ProtectionLaravel PassportCookie HttponlyLaravel Problem Overview
NOTE: I had 4 bounties on this question, but non of the upvoted answers below are the answer needed for this question. Everything needed is in Update 3 below, just looking for Laravel code to implement.
UPDATE 3: This flow chart is exactly the flow I am trying to accomplish, everything below is the original question with some older updates. This flow chart sums up everything needed.
The green parts in the flow chart below are the parts that I know how to do. The red parts along with their side notes is what I am looking for help accomplishing using Laravel code.
I have done a lot of research but the information always ended up short and not complete when it comes to using Laravel with a JWT httponly cookie for a self consuming API (most tutorials online only show JWT being stored in local storage which is not very secure). It looks like httponly cookie containing a JWT by Passport should be used to identify the user on the Javascript side when sent with every request to the server to validate that the user is who they say they are.
There are also some additional things that are needed to have a complete picture of how to make this setup work which I haven't come across in a single tutorial which covers this:
- Laravel Passport (not tymon auth) to generate encrypted JWT and send it as httponly cookie as response after login from JS side. What middleware to use? If refresh tokens add more security, how to implement?
- JavaScript (axios for example) api pseudo code that makes call to auth endpoint, how is httponly cookie passed to backend, and how does backend verify token is valid.
- If single account is logged in from multiple devices, then a device is stolen, how to revoke access from all the authed user devices (assuming user does Change Password from a logged in device they have control over)?
- What would Login/Register, Logout, Change Password, Forgot Password controller methods typically look like to handle the creation/validating/revoking of tokens?
- CSRF token integration.
I hope an answer to this question serves as an easy to follow guide for future readers and those struggling at the moment to find an answer covering the above points on a self consuming API.
UPDATE 1:
- Please note I tried the
CreateFreshApiToken
before, but that didn't work when it comes to revoking tokens of the user (for points 3 and 4 above). This is based on this comment by a core laravel developer, when talking about theCreateFreshApiToken
middleware:
> JWT tokens created by this middleware aren't stored anywhere. They > can't be revoked or "not exist". They simply provide a way for your > api calls to be authed through the laravel_token cookie. It isn't > related to access tokens. > Also: you normally wouldn't use tokens issued by clients on the same app which issues them. You'd use them in a first or third party > app. Either use the middleware or the client issued tokens but not > both at the same time.
So it seems to be able to cater to points 3 and 4 to revoke tokens, it's not possible to do so if using the CreateFreshApiToken
middleware.
- On the client side, it seems
Authorization: Bearer <token>
is not the way to go when dealing with the secure httpOnly cookie. I think the request/response are supposed to include the secure httpOnly cookie as a request/response header, like this based on the laravel docs:
> When using this method of authentication, the default Laravel > JavaScript scaffolding instructs Axios to always send the X-CSRF-TOKEN > and X-Requested-With headers.
headerswindow.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': (csrf_token goes here)
};
This is also the reason I am looking for a solution which covers all the points above. Apologies, I am using Laravel 5.6 not 5.5.
UPDATE 2:
It seems the Password Grant/Refresh Token Grant combo is the way to go. Looking for an easy to follow implementation guide using Password Grant/Refresh Token Grant combo.
> Password Grant: > This grant is suitable when dealing with the client that we trust, > like a mobile app for our own website. In this case, the client sends > the user's login credentials to the authorization server and the > server directly issues the access token.
> Refresh Token Grant: > When the server issues an access token, it also sets an expiry for the > access token. Refresh token grant is used when we want to refresh the > access token once it is expired. In this case, authorization server > will send a refresh token while issuing the access token, which can be > used to request a new access token.
I am looking for an easy to implement, straight forward, holistic answer using the Password Grant/Refresh Token Grant combo that covers all the parts of the above original 5 points with httpOnly secure cookie, creating/revoking/refreshing tokens, login cookie creation, logout cookie revoking, controller methods, CSRF, etc.
Laravel Solutions
Solution 1 - Laravel
I'll try to answer this in a generic way so that the answer is applicable across frameworks, implementations and languages because the answers to all the questions can be derived from the general protocol or algorithm specifications.
Which OAuth 2.0 grant type should I use?
This is the first thing to be decided. When it comes to SPA, the two possible options are:
- Authorization code grant (recommended, provided the client secret is stored on the server side)
- Resource owner password credential grant
The reasons I don't mention Implicit grant type as an option are:
- The client authentication step by providing client secret and authorization code is missing. So less security
- The access token is sent back as a URL fragment (so that the token doesn't go to the server) which will continue to stay in browser history
- If XSS attack occurs, the malicious script can very well send the token to a remote server in control of an attacker
(Client Credentials grant type is kept out of scope of this discussion as it is used when the client is not acting on behalf of a user. For e.g. a batch job)
In case of Authorization Code grant type, the authorization server is usually a different server from the resource server. It is better to keep the authorization server separate and use it as a common authorization server for all SPA within the organization. This is always the recommended solution.
Here (in the authorization code grant type) the flow looks like below:
- the user clicks on the login button on the SPA landing page
- the user is redirected to the authorization server login page. The client id is provided in the URL query parameter
- The user enters his / her credentials and clicks on the login button. The username and password will be sent to the authorization server using HTTP POST. The credentials should be sent in the request body or header and NOT in the URL (as URLs are logged in browser history and application server). Also, the proper caching HTTP headers should be set, so that the credentials are not cached:
Cache-Control: no-cache, no-store
,Pragma: no-cache
,Expires: 0
- The authorization server authenticates the user against a user database (say, LDAP server) where the username and the hash of the user password (hashing algorithms like Argon2, PBKDF2, Bcrypt or Scrypt) is stored with a random salt
- On successful authentication, the authorization server would retrieve from its database the redirect URL against the provided client id in the URL query parameter. The redirect URL is the resource server URL
- The user will then be redirected to a resource server endpoint with a authorization code in the URL query parameter
- The resource server will then do an HTTP POST request to the authorization server for access token. The authorization code, client id, client secret should go in the request body. (Appropriate caching headers as above should be used)
- The authorization server would return the access token and the refresh token in response body or header (with the appropriate caching header as mentioned above)
- The resource server will now redirect the user (HTTP response code 302) to the SPA URL by setting appropriate cookies (to be explained in detail below)
On the other hand, for resource owner password credential grant type, the authorization server and the resource server are same. It is easier to implement and can also be used if it suits the requirement and implementation timelines.
Also refer to my answer on this here for further details on Resource Owner grant type.
It may be important to note here that in a SPA, all the protected routes should be enabled only after calling an appropriate service to ensure that valid tokens are present in the request. Similarly the protected APIs should also have appropriate filters to validate the access tokens.
Why shouldn't I store the tokens in browser localstorage or sessionstorage?
Many SPAs do store access and / or refresh token in the browser localstorage or sessionstorage. The reason I think we shouldn't store the tokens in these browser storages are:
-
If XSS occurs, the malicious script can easily read the tokens from there and send them to a remote server. There on-wards the remote server or attacker would have no problem in impersonating the victim user.
-
localstorage and sessionstorage are not shared across sub-domains. So, if we have two SPA running on different sub-domains, we won't get the SSO functionality because the token stored by one app won't be available to the other app within the organization
If, however, the tokens are still stored in any of these browser storages, proper fingerprint must be included. Fingerprint is a cryptographically strong random string of bytes. The Base64 string of the raw string will then be stored in a HttpOnly
, Secure
, SameSite
cookie with name prefix __Secure-
. Proper values for Domain
and Path
attributes. A SHA256 hash of the string will also be passed in a claim of JWT. Thus Even if an XSS attack sends the JWT access token to an attacker controlled remote server, it cannot send the original string in cookie and as a result the server can reject the request based on the absence of the cookie. Also, XSS and script injection can be further mitigated by using an appropriate content-security-policy
response header.
Note:
-
SameSite=strict
ensures that the given cookie will not accompany the requests originated from a different site (AJAX or through following hyperlink). Simply put - any request originating from a site with the same "registrable domain" as the target site will be allowed. E.g. If "http://www.example.com" is the name of the site, the registrable domain is "example.com". For further details refer to Reference no. 3 in the last section below. Thus, it provides some protection against CSRF. However, this also means that if the URL is given is a forum, an authenticated user cannot follow the link. If that is a serious restriction for an application,SameSite=lax
can be used which will allow cross-site requests as long as the HTTP methods are safe viz. GET, HEAD, OPTIONS and TRACE. Since CSRF is based on unsafe methods like POST, PUT, DELETE,lax
still provides protection against CSRF -
To allow a cookie to be passed in all requests to any sub-domain of "example.com", the domain attribute of the cookie should be set as "example.com"
Why should I store access token and / or refresh token in cookies?
- When storing the tokens in cookies, we can set the cookie as
secure
andhttpOnly
. Thus if XSS occurs, the malicious script cannot read and send them to remote server. XSS can still impersonate the user from the users' browser, but if the browser is closed, the script can't do further damage.secure
flag ensures that the tokens cannot be sent over unsecured connections - SSL/TLS is mandatory - Setting the root domain in the cookie as
domain=example.com
, for example, ensures that the cookie is accessible across all sub-domains. Thus, different apps and servers within the organization can use the same tokens. Login is required only once
How do I validate the token?
Tokens are usually JWT tokens. Usually the contents of the token are not secret. Hence they are usually not encrypted. If encryption is required (maybe because some sensitive information is also being passed within the token), there is a separate specification JWE. Even if encryption is not required, we need to ensure the integrity of the tokens. No one (user or the attacker) should be able to modify the tokens. If they do, the server should be able to detect that and deny all requests with the forged tokens. To ensure this integrity, the JWT tokens are digitally signed using an algorithm like HmacSHA256. In order to generate this signature, a secret key is required. The authorization server will own and protect the secret. Whenever the authorization server api is invoked to validate a token, the authorization server would recalculate the HMAC on the passed token. If it doesn't match with the input HMAC, it gives back a negative response. The JWT token are returned or stored in a Base64 encoded format.
However, for every API call on the resource server, the authorization server is not involved to validate the token. The resource server can cache the tokens issued by the authorization server. The resource server can use an in-memory data grid (viz. Redis) or, if everything cannot be stored in RAM, an LSM based DB (viz Riak with Level DB) to store the tokens.
For every API call, the resource server would check its cache.
-
If the access token is not present in the cache, APIs should return an appropriate response message and 401 response code such that the SPA can redirect the user to an appropriate page where the user would be requested to re-login
-
If the access token is valid but expired (Note, the JWT tokens usually contain the username and the expiry date among other things), APIs should return an appropriate response message and 401 response code such that the SPA can invoke an appropriate resource server API to renew the access token with the refresh token (with appropriate cache headers). The server would then invoke the authorization server with access token, refresh token and client secret and the authorization server can return the new access and refresh tokens which eventually flow down to the SPA (with appropriate cache headers). Then the client needs to retry the original request. All this will be handled by the system without user intervention. A separate cookie could be created for storing refresh token similar to access token but with appropriate value for
Path
attribute, so that the refresh token do not accompany every request, but available only in renewal requests -
If the refresh token is invalid or expired, APIs should return an appropriate response message and 401 response code such that the SPA can redirect the user to an appropriate page where the user would be requested to re-login
Why do we need two tokens - access token and refresh token?
-
Access token usually have a short validity period, say 30 minutes. Refresh token usually have a longer validity period, say 6 months. If the access token is somehow compromised, the attacker can impersonate the victim user only as long as the access token is valid. Since the attacker won't have the client secret, it cannot request the authorization server for a new access token. Attacker can however request the resource server for token renewal (as in the above setup, the renewal request is going through the resource server to avoid storing the client secret in browser), but given the other steps taken it is unlikely and moreover the server can take additional protection measures based on IP address.
-
If this short validity period of the access token helps the authorization server to revoke the issued tokens from the clients, if required. The authorization server can also maintain a cache of the issued tokens. The administrators of the system can then, if required, mark certain users' tokens as revoked. On access token expiry, when the resource server will go to the authorization server, the user will be forced to login again.
What about CSRF?
-
In order to protect the user from CSRF, we can follow the approach followed in frameworks like Angular (as explained in the Angular HttpClient documentation where the server has to send a non-HttpOnly cookie (in other words a readable cookie) containing a unique unpredictable value for that particular session. It should be a cryptographically strong random value. The client will then always read the cookie and send the value in a custom HTTP header (except GET & HEAD requests which are not supposed to have any state changing logic. Note CSRF cannot read anything from the target web app due to same origin policy) so that the server can verify the value from the header and the cookie. Since the cross domain forms cannot read the cookie or set a custom header, in case of CSRF requests, the custom header value will be missing and the server would be able to detect the attack
-
To protect the application from login CSRF, always check the
referer
header and accept requests only whenreferer
is a trusted domain. Ifreferer
header is absent or a non-whitelisted domain, simply reject the request. When using SSL/TLSreferrer
is usually present. Landing pages (that is mostly informational and not containing login form or any secured content may be little relaxed and allow requests with missingreferer
header -
TRACE
HTTP method should be blocked in the server as this can be used to read thehttpOnly
cookie -
Also, set the header
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
to allow only secured connections to prevent any man-in-the-middle overwrite the CSRF cookies from a sub-domain -
Additionally, the
SameSite
setting as mentioned above should be used -
State Variable (Auth0 uses it) - The client will generate and pass with every request a cryptographically strong random nonce which the server will echo back along with its response allowing the client to validate the nonce. It's explained in Auth0 doc
Finally, SSL/TLS is mandatory for all communications - as on today, TLS versions below 1.1 are not acceptable for PCI/DSS compliance. Proper cipher suites should be used to ensure forward secrecy and authenticated encryption. Also, the access and refresh tokens should be blacklisted as soon as the user explicitly clicks on "Logout" to prevent any possibility of token misuse.
References
Solution 2 - Laravel
Laravel Passport JWT
-
To use this feature you need to disable cookie serialization. Laravel 5.5 has an issue with serialization / unserialization of cookie values. You can read more about this here (https://laravel.com/docs/5.5/upgrade)
-
Make sure that
-
you have
<meta name="csrf-token" content="{{ csrf_token() }}">
in your blade template head -
axios is set to use csrf_token on each request.
You should have something like this in resources/assets/js/bootstrap.js
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
3. Setup auth routes explained here (https://laravel.com/docs/5.5/authentication) 4. Setup passport explained here (https://laravel.com/docs/5.5/passport).
Important parts are:
- add the
Laravel\Passport\HasApiTokens
trait to yourUser
model - set the
driver
option of theapi
authentication guard topassport
in yourconfig/auth.php
- add the
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
middleware to yourweb
middleware group inapp/Http/Kernel.php
Note you probably can skip migrations and creating clients.
- Make a POST request to
/login
passing your credentials. You can make an AJAX request or normal form submit.
If the login request is AJAX (using axios) the response data will be the HTML but what are you interested at is the status code.
axios.get(
'/login,
{
email: '[email protected]',
password: 'secret',
},
{
headers: {
'Accept': 'application/json', // set this header to get json validation errors.
},
},
).then(response => {
if (response.status === 200) {
// the cookie was set in browser
// the response.data will be HTML string but I don't think you are interested in that
}
// do something in this case
}).catch(error => {
if (error.response.status === 422) {
// error.response.data is an object containing validation errors
}
// do something in this case
});
On login, the server finds the user by credentials provided, generates a token based on user info (id, email ...) (this token is not saved anywhere) then the server returns a response with an encrypted cookie that contains the generated token.
- Make an API call to a protected route.
Assuming that you have a protected route
Route::get('protected', 'SomeController@protected')->middleware('auth:api');
You can make an ajax call using axios as normal. The cookies are automatically set.
axios.get('/api/protected')
.then(response => {
// do something with the response
}).catch(error => {
// do something with this case of error
});
When the server receives the call decrypts the request laravel_cookie
and get user information (ex: id, email ...)
Then with that user info does a database lookup to check if the user exists.
If the user is found then the user is authorized to access the requested resource.
Else a 401 is returned.
Invalidating the JWT token. As you mention the comment there's no need to worry about this since this token is not saved anywhere on the server.
Update
Regarding point 3 Laravel 5.6 Auth has a new method logoutOtherDevices
. You can learn more from here (https://laracasts.com/series/whats-new-in-laravel-5-6/episodes/7)
since the documentation is very light.
If you can't update your Laravel version you can check it out how is done in 5.6 and build your own implementation for 5.5
Point 4 from your question. Take a look at controllers found in app/Http/Controllers/Auth
.
Regarding access_tokens and refresh_tokens this is a totally different and more complex approach. You can find lots of tutorials online explaining how to do it.
I hope it helps.
PS. Have a Happy New Year!! :)
Solution 3 - Laravel
- Laravel Passport is an implementation of The PHP League's OAuth Server
- The password grant type can be used for username + password authentication
- Remember to hide your client credentials by making the auth request in a proxy
- Save the refresh token in a HttpOnly cookie to minimize the risk of XSS attacks
http://esbenp.github.io/2017/03/19/modern-rest-api-laravel-part-4/
Solution 4 - Laravel
I have also implemented Laravel passport in my project and I think I have covered most of the points which you have mentioned in your question.
- I have used the password grant for generating an access token and refresh token. You can follow these steps to set up the passport and implement the passport grant. In your login method, you have to validate the user credentials and generate the tokens and attach the cookie(Attaching cookie to the response) to the response. If you need I can get you some examples.
- I have added two middleware for CORS(Handling the incoming request headers) and to check if the incoming access token is valid or not if not valid generate the access token from stored refresh token (Refreshing token). I can show you the example.
- After login, all the request from the client side should contain the Authorization header(
Authorization: Bearer <token>
).
Let me know if you are clear with the above points.