Photo: The National Archives (UK)

Authenticating Node.js using PHP sessions

Michał Męciński
codeburst
5 min readJun 16, 2017

--

A few weeks ago I wrote about hybrid PHP and Node.js web applications. In a hybrid approach, some pages are generated by a PHP application. In addition, a client-side script talks to a Node.js service, using AJAX or socket.io, to dynamically update the content. Today I’m going to dive into some more technical details.

One of the challenges that come with a hybrid PHP and Node.js approach is user authentication. I’m assuming that you know how to use cookie-based authentication in PHP. When you add a Node.js service to the application, it also needs to know the identity of the logged in user and verify that the requests come from a trusted source.

There are a few possible ways to secure the communication between the client-side script and the Node.js service. One example is to use an encrypted token which could be created by the PHP application and decoded by the Node.js service. A common standard for such authentication tokens is called JWT (JSON Web Token). It’s also possible to use the same cookie-based authentication mechanis for both PHP application and the Node.js service.

Node.js and cookie-based authentication

We can take advantage of the fact that both the PHP application and the Node.js service are installed on the same host, but with different ports. An interesting feature of cookies is that although they are specific to a given host name and path, they are shared between various ports. This behavior is due to historical reasons, but in our case it’s quite helpful.

Suppose that the PHP application works on http://example.com (using default port 80) and the Node.js service works on http://example.com:8000. When a session cookie called PHPSESSID is created for the example.com domain, it is automatically passed by the browser to all requests to the Node.js service, both AJAX and socket.io, even the cookie has a httponly flag and its value is not available directly to the client-side script. The same is also true for secure requests using the https protocol.

However, when you send an AJAX POST request to the Node.js service, you will notice that the browser will block it. The reason is the cross-origin policy, which treats different ports on the same server as separate origins.

Cross-origin policy

The Node.js service must handle the OPTIONS requests which are used to validate the cross-origin policy. Here is a very basic implementation of the OPTIONS request handler:

http.createServer( ( request, response ) => {
if ( request.method == 'OPTIONS' ) {
response.writeHead( 200, {
'Access-Control-Allow-Origin':
request.headers[ 'origin' ],
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Credentials': 'true',
'Content-Length': 0
} );
response.end();
} else {
// handle POST and GET requests
// ...
}
} );

As you can see, the response to an OPTIONS request has no body. Instead, it contains a few HTTP headers:

  • Access-Control-Allow-Origin specifies which sites are allowed to send requests to this service. Normally you could use a* wildcard to indicate that all sites are allowed. However, when the Access-Control-Allow-Credentials header is set to true, this wildcard cannot be used and an explicit origin must be returned instead. Here we simply return back the value from the Origin request header, but for more security this value should be verified.
  • Access-Control-Allow-Methods specifies the HTTP methods that can be used with this service. Here we just list the methods that are supported by the service.
  • Access-Control-Allow-Headers specifies additional HTTP headers that can be included in the requests. Since POST requests typically contain a Content-Type header, we include it here; you can add more headers if necessary. Note that the Cookie header is not included here because it’s handled by the following access control rule.
  • Access-Control-Allow-Credentials specifies whether the browser can send headers that pass user credentials, in our case the Cookie header.

We also have to enable sending the Cookie header on the client-side, because this option is disabled by default. For example, if you use jQuery to perform AJAX requests, you could use the following code:

$.ajax( {
method: 'POST',
url: 'http://example.com:8000/service',
data: ...,
xhrFields: {
withCredentials: true
}
} );

Setting xhrFields.withCredentials to true ensures that the cookies are included in the AJAX request, if the server’s OPTIONS response allows it.

CSRF prevention

There is one important thing that we have to remember about. Any malicious website could include a script which sends a POST request to our Node.js service. In this case, the browser will automatically add session cookie to the request, so it will appear to be authenticated and valid. This is commonly known as cross-site request forgery, or an CSRF attack.

One solution is to verify the the Origin and Referer request headers in the Node.js service, instead of simply allowing all origins. It’s also recommended to use a technique called the “synchronizer token” to ensure that the requests really come from your application. You can read more about it here.

In short, the token is randomly generated when the user logs in and stored in the session data. It is also included as a hidden field in all HTML forms. The value sent in the POST request can be compared with the value stored in the session data to ensure that the request really comes from our application.

The same mechanism can be used to protect the Node.js service from CSRF attacks. The token can be included in the body of the AJAX requests, for example:

$.ajax( {
method: 'POST',
url: 'http://example.com:8000/service',
data: JSON.serialize( {
csrfToken: $( '#csrtToken' ).val(),
...
} ),
xhrFields: {
withCredentials: true
}
} );

The Node.js service can compare the value of the token passed in the request body with the value stored in the session data. A very simplified code might look as follows:

const Cookies = require( 'cookies' );
const PHPUnserialize = require( 'php-unserialize' );
http.createServer( ( request, response ) => {
if ( request.method == 'OPTIONS' ) {
// handle OPTIONS requests as shown above
// ...
} else if ( request.method == 'POST' ) {
// read the PHPSESSID cookie from the request
const cookies = new Cookies( request, null );
const sessionId = cookies.get( 'PHPSESSID' );
// read the session data from the database, file or cache
const data = readSessionData( sessionId );
// deserialize session data
const session = PHPUnserialize.unserializeSession( data );
// parse the request body
const body = JSON.parse( request.body );
// compare the request token with session data
if ( body.csfrToken == session.csrfToken ) {
// perform the request
// ...
}
}
} );

You will have to write the appropriate function which reads session data from the database, file system or cache server, depending on the storage mechanism used by your application.

You can use the php-unserialize module to transform serialized PHP session data into a JavaScript object. Unfortunately, the php-unserialize module from npm is quite old and contains a few bugs, for example it doesn’t support objects stored in session data. These bugs are fixed in the latest version on GitHub, so you can use it instead of the npm module.

That’s all for today, thanks for reading. I’m going to continue writing technical articles about developing web applications using PHP, JavaScript and Node.js. Let me know if there is anything specific that you would like me to write about.

--

--