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:
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:
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 the CreateFreshApiToken
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.
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.
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.
This is the first thing to be decided. When it comes to SPA, the two possible options are:
The reasons I don't mention Implicit grant type as an option are:
(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:
Cache-Control: no-cache, no-store
, Pragma: no-cache
, Expires: 0
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.
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"
secure
and httpOnly
. 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 mandatorydomain=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 onceTokens 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
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.
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 when referer
is a trusted domain. If referer
header is absent or a non-whitelisted domain, simply reject the request. When using SSL/TLS referrer
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 missing referer
header
TRACE
HTTP method should be blocked in the server as this can be used to read the httpOnly
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.
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');
}
Important parts are:
Laravel\Passport\HasApiTokens
trait to your User
modeldriver
option of the api
authentication guard to passport
in your config/auth.php
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
middleware to your web
middleware group in app/Http/Kernel.php
Note you probably can skip migrations and creating clients.
/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.
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.
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!! :)
More information you can see here
http://esbenp.github.io/2017/03/19/modern-rest-api-laravel-part-4/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With