Laravel 5.6 – Passport JWT httponly cookie SPA authentication for self consuming API?

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:

  1. Authorization code grant (recommended, provided the client secret is stored on the server side)
  2. Resource owner password credential grant

The reasons I don’t mention Implicit grant type as an option are:

  1. The client authentication step by providing client secret and authorization code is missing. So less security
  2. 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
  3. 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:

  1. the user clicks on the login button on the SPA landing page
  2. the user is redirected to the authorization server login page. The client id is provided in the URL query parameter
  3. 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
  4. 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
  5. 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
  6. The user will then be redirected to a resource server endpoint with a authorization code in the URL query parameter
  7. 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)
  8. 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)
  9. 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:

  1. 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.

  2. 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:

  1. 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

  2. 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?

  1. When storing the tokens in cookies, we can set the cookie as 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 mandatory
  2. 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.

  1. 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

  2. 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

  3. 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?

  1. 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.

  2. 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?

  1. 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

  2. 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

  3. TRACE HTTP method should be blocked in the server as this can be used to read the httpOnly cookie

  4. 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

  5. Additionally, the SameSite setting as mentioned above should be used

  6. 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

  1. RFC 6749 – OAuth2.0
  2. OWASP JWT Cheat Sheet
  3. SameSite Cookie IETF Draft
  4. Cookie Prefixes
  5. RFC 6265 – Cookie

Leave a Comment