Introduction

Recently I had to figure out how to validate a persons details in the DMZ using some standard reference; the main concern was that the machine in the DMZ could not “send” any sensitive (useful) information. This is “typically” what is done, that is to say “hide” information. There was a dilemma of how to communicate this information from external (DMZ) to internal (secure data). RESTful web services seemed like the best way to go.

One of the most downloaded Symfony bundles is FOSRestBundle, and this is understandable, since REST is becoming more prevalent nowadays. So I decided to use FOSRestBundle for my application. This blog describes how to install the bundle and get it working.

Installation

If you haven’t installed Symfony before, you’ll need to install that and all its requirements first. My post Apache & Symfony on RHEL describes details on installing Apache (httpd), PHP 7.0, MariaDB, Composer, and the Symfony binary. So I would suggest you look at that blog first and then proceed with these steps.

I’ll presume you’ve created a Symfony project called “restTest”, and then you’ll need to issue the following commands to install FOSRestBundle:

composer require friendsofsymfony/rest-bundle

If for some reason on your machine you can’t write to the html folder (example /var/www/html), and you need to use sudo to create the permissions, then you’ll need to run composer differently. Use the command “whereis composer” to find out the path to composer, then issue the following command:

sudo /usr/local/bin/composer require friendsofsymfony/rest-bundle

Then, enable the bundle by editing “restTest/app/AppKernel.php”:

// app/AppKernel.php

class AppKernel extends Kernel
{
   public function registerBundles()
   {
      $bundles = [
         ...
         new FOS\RestBundle\FOSRestBundle(),
         ...
      ];

After that, then you’ll need to enable a JSON serializer and the configuration for the FOSRestBundle. To do this, edit the file “restTest/app/config/config.yml” as follows:

# app/config/config.yml
...
framework:
   ...
   serializer: { enable_annotations: true }
   ...
...
fos_rest:
   body_listener: true
   format_listener:
      rules:
         - { path: '^/', priorities: ['json'], fallback_format: json, prefer_extension: false }
   param_fetcher_listener: true
   view:
      view_response_listener: 'force'
      formats:
         json: true
...

That’s it for installation!

Create a Default Controller

Next let’s edit the default controller and create a simple controller that returns a very simple JSON response. In the controller example below, I’ll use @Rest annotations that simplify routing. Open the PHP file “restTest/src/AppBundle/Controller/DefaultController.php” and make the following changes:

<?php
namespace AppBundle\Controller;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends FOSRestController {
    /**
     * @Rest\Get("/api/{testID}")
     */
    public function apiAction(Request $request)
    {
       $data = [
          'test1' => 'someData1',
          'test2' => false,
       ];
 
       $view = $this->view($data, Response::HTTP_OK);
 
       return $view;
   }

That’s it! Now open a browser and enter the URL:

http://192.168.1.15/api/1234

In the above example, the value “192.168.1.15” is the IP address of the host running the Apache/Symfony server. You could also setup the “ServerName” directive and configure your hosts file (as described in my Apache httpd VirtualHost Config post), and then use a hostname in the URL. If everything is working and setup correctly, this will return the following JSON response:

{ "test1":"someData","test2":false }

This demonstrates a simple REST controller, you can modify the controller to return the desired data that you need.

Returning Useful Information

The above default controller works great, except for it only returns static data and nothing really useful. What if we wanted the passed in parameter “testID” to represent an ID of a user table in a database? Then we can pass in the parameter and then return via the controller information about the user.

Most Symfony users use the default (local)database which is MySQL. However, you can use other databases such as PostgreSQL, MSSQL, Oracle, etc…, and the database can also be external to the Symfony host. In my example I’ll use an external Oracle database connection using a Doctrine Database Abstraction Layer connection. Here is an example code getting user information:

/**
 * @Rest\Get("/api/{testID}")
 */
public function apiAction(Request $request)
{
   // Get an Oracle database connection.
   $orc = $this->get('doctrine.dbal.oracle_connection');
 
   // Query for user ID, and get first & last names.
   $query = "SELECT U.FIRST_NAME,U.LAST_NAME FROM USERS U
             WHERE U.ID = '".$request->get('testID')."'";
   $result = $orc->fetchAll( $query );
 
   if ($result == null){
      $data = [];
   }
   else{
      $data = [
         'fname' => $result[0]['FIRST_NAME'],
         'lname' => $result[0]['LAST_NAME'],
      ];
   }
 
   $view = $this->view($data, Response::HTTP_OK);
 
   return $view;
}

In the above code, we retrieve the “testID” from the Request and then run a query on the database for the passed in “testID” and return the First & Last names for the particular user in the JSON response.

Using the above controller, presuming the database contained the user with a “testID” of “3333” has a USERS tables values of “Tony Baloney” for First & Last names; then using the REST api URL of “http://192.168.1.15/api/333” would return:

{"fname":"Tony","lname":"Baloney"}

This is much more useful, since passing in the ID returns the users name as a response.

 Using WSSE Authentication

At the begining of this article, I mentioned the concern of sending information between servers (especially in the DMZ) and protecting that information. One way of protecting that information would be to authenticate. Using a REST system, this would mean to provide a username / password combination and only return a response to clients attempting to consume the web service that have authenticated by passing in the same username / password combination.

WSSE is an ideal protocol to use to achieve the security we need. WSSE operation can be summarized as setting custom http headers; specifically “X-WSSE” headers. A sample X-WSSE header might look like the following:

X-WSSE: UsernameToken Username="wsse", PasswordDigest="gBgj6GM5MsOQkn+j3VkBiSmSt7g=", Nonce="NTMxNTcyODI4ZGZkZmQ4Zg==", Created="2016-11-23T22:20:17Z"

There is one popular WSSE bundle for Symfony available:

djoos/EscapeWSSEAuthenticationBundle

You can open the above link and install using composer; then to enable it and add a sample user / password, we can edit the “app/config/security.yml” file like so:

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:

   # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
   providers:
      in_memory:
         memory:
            users:
               - { name: 'wsse_user', password: 'myPass' }
      wsse_users:
         memory:
            users:
               - { name: 'wsse_user', password: 'myPass' }

   firewalls:
      wsse_secured:
         provider: users
         wsse:
            provider: wsse_users

That’s it! WSSE authentication is enabled for every request. We should probably add a simpler Route to use to test out the authentication. Open the DefultController and ADD  (add below your other code) the following route:

/**
 * @Rest\Get("/testApi/{testNumber}")
 */
public function testApiAction(Request $request)
{
   $data = [ 'result' => $request->get('testNumber') ];

   return $this->view($data, Response::HTTP_OK);
}

Once you make this change, make sure you issue this command from your Symfony REST project directory to clear your production cache, which is a common mistake for developers new to Symfony:

php bin/console cache:clear --env=prod

If you issue the following command:

php bin/console debug:router

You should now see the above route “/testApi/{testNumber}” appear in the “Path” column. Now, to test this out, we can open a browser and enter in the following URL which should call the above route:

http://192.168.1.15/testApi/9933

You will get a blank page (no response), which is good, since it is working as expected.So, the question you might have is: how do we test this, and how do we know it’s working? In Chrome, we can add the REST Console extension, and we can use the REST Console together with this JavaScript WSSE Header Generator tool. In the JavaScript WSSE Header Generator tool, we simply enter in our username and password, check the “auto” boxes for nonce and created, and then click “Generate”. Below is a sample screenshot:

wsse_header_sample

The X-WSSE headers are shown below the Generate button. Now we can use the REST Console as a test client to consume the REST service. In Chrome open the REST Console – it appears under Apps. Enter the following:

Target Request URI: http://192.168.1.15/testApi/9933
Custom Headers -> Click "+"
   Request Parameters
      header: X-WSSE
      value: paste the value from JavaScript WSSE Header Generator above.
Click GET button

Note, the header value does not include “X-WSSE:”, but just the trailing part. If everything worked correctly and you autheticated, the Response section shows the JSON result. It should look like the sample below:

rest_console_response

Now you simply need to create a client that you will normally using in production to consume the REST web service.

CodeProject

Advertisements