My name is Philipp C. Heckel and I write about nerdy things.

How-To: PHP based JSON-RPC API, with authentication, validation and logging


  • Jan 05 / 2016
  • 7
Uncategorized

How-To: PHP based JSON-RPC API, with authentication, validation and logging


At my work, we use JSON-RPC based APIs very heavily, in particular with our PHP JSON-RPC library php-json-rpc. While JSON-RPC is not as wide spread as REST, it fits our needs quite nicely. In particular, it is protocol independent and can be used over HTTP, SSH or as local CLI. With our library and its numerous extensions (HTTP, SSH, authentication, validation, request-to-class mapping and logging), development is super fast and incredibly easy.

In this post, I’d like to demonstrate how to set up a PHP based JSON-RPC API, with authentication, validation and logging.


Content


0. Code available on GitHub

All the code for this post is available in a JSON-RPC Demo Project on GitHub. Feel free to poke around there and/or steal bits and pieces from it.

To try the examples, check out the code and run this:

1. Demo API: Managing devices (e.g. phones, computers)

For this blog post, we’ll create a simple Device Management API to list and add devices (e.g. phones, computers, and such) using two endpoints called devices/add and devices/listAll. The endpoints will be simple stub implementations, but we’ll still see how easy it is to create and manage endpoints.

Here’s an example JSON-RPC API request and its response to list all devices:

2. JSON-RPC core library (php-json-rpc)

The core JSON-RPC library php-json-rpc is written by Spencer Mortensen. It’s a great piece of software that implements the core specification of JSON-RPC 2.0.

2.1. Sample code

At the center of it, the JsonRpc\Server class provides a way to encode and decode messages according to JSON-RPC standards. All you have to do is provide your own JsonRpc\Evaluator implementation to use it:

An Evaluator basically translates JSON-RPC methods and parameters to PHP methods/arguments and executes the method. Your implementation must only provide a method evaluate($method, $arguments). It might (but should not) look as simple as this:

2.2. Usage

Once we’ve implemented the endpoints/methods, we’ll be able to add and list devices via HTTP(S). Here’s an example via curl:

Of course you can also use your own JSON-RPC client, or you can use JavaScript/jQuery (see below). I’m using curl here because it’s super easy.

2.3. More examples

Find a working demo of the php-json-rpc library in the examples folder of the library or in example 0 of the JSON-RPC demo project.

3. Method and parameter mapping (php-json-rpc-simple)

While we can certainly implement all use cases with the core library, having to provide the mapping yourself (usually with a giant switch-case) can be tedious. The php-json-rpc-simple library provides a way to do that mapping for you. It offers a simple implementation of the JsonRpc\Evaluator interface for you and maps JSON-RPC method and params to a corresponding PHP class, method and arguments.

3.1. Sample code

To use the mapping magic of the php-json-rpc-simple library, alter the API server logic to automatically map JSON-RPC methods to a PHP class/method inside the Demo\Api\Endpoint namespace (see example1/api.php):

Since the mapping is done for you, the endpoint methods only contain the actual logic and/or library calls (see src/Api/Endpoint/Devices.php):

3.2. Usage

The usage is not very different from the example above. This time, however, the mapping is performed by the library:

That also means, of course, that invalid or missing parameters will be detected by the library and a JSON-RPC error is returned:

3.3. More examples

Find a working demo of the php-json-rpc-simple library in example 1 of the JSON-RPC demo project.

4. Validation of parameters (php-json-rpc-validator)

Another tedious thing in API development is parameter validation. User input can be invalid or even malicious, an API (or any web page really) must validate the input parameters. The php-json-rpc-validator library offers annotation-based validation of parameters.

4.1. Sample code

Normally, a publicly available endpoint method must check the arguments that are passed to it. Code like this is not uncommon:

Instead of this annoying and polluting validation code, we can now define the constraints of each method argument using the @Validate annotation. This annotation is based on Symfony’s @Collection annotation and supports a variety of constraints. Examples include @Assert\NotBlank, @Assert\NotNull, @Assert\EqualTo, @Assert\Regex, @Assert\GreaterThan, and may more.

To add validation support to your API server file (in our examples: api.php), all you need to do is wrap the existing JsonRpc\Evaluator in a Validator\Evaluator. After that, the API server logic looks like this (see example2/api.php):

4.2. Usage

If the input is valid, the request and response look no different than above. However, if the parameter input is invalid, the API now responds with a JSON-RPC error:

4.3. More examples

Find a working demo of the php-json-rpc-validator library in example 2 of the JSON-RPC demo project.

5. Authentication (php-json-rpc-auth)

What would an API be without authentication? Probably reckless, in most cases.

The php-json-rpc-auth library offers a simple framework to implement any kind of authentication and authorization for your API. Instead of implementing all the different auth mechanisms (HTTP Basic Auth, Digest, OAuth, SAML, Cookies, …), it merely provides a simplistic Auth\Authenticator class to consult a user-provided set of Auth\Handlers. The actual implementation of these handler class(es) must be provided by the developer.

5.1. Sample code

Assuming that we want to implement HTTP Basic Auth for our API, we only need to implement an Auth\Handler (see src/Api/Auth/BasicAuthHandler.php):

The canHandle() method tells the authenticator whether the given request can be authenticated with this handler. The authenticate() method is only called if canHandle() returned true and then authenticates the actual request.

As for the API server code, we can simply wrap an Auth\Evaluator around our existing evaluator (see example3/api.php).

This may look a bit complicated, but it merely nests different Auth\Evaluator implementations. In the example above, an incoming request is first passed to the Auth\Evaluator, then (if authorized) to the Validator\Evaluator, and finally (if validated) to the Simple\Evaluator. This is a very classic implementation of the decorator pattern.

5.2. Usage

Trying to access API without authentication will lead to a “Missing auth” error message. In this example, please note that curl‘s -u parameter (basic auth user/pass) is not being passed:

In this example, the -u parameter is passed, i.e. the Authorization HTTP header is sent, but with invalid credentials. We’re basically trying to access API with invalid Basic Auth credentials:

Only if we provide the correct credentials, we’re allowed to access the API via HTTP (authenticated via HTTP Basic Auth):

5.3. More examples

Find a working demo of the php-json-rpc-auth library in example 3 of the JSON-RPC demo project.

6. Logging with “php-json-rpc-log”

Logging requests and responses of an API can be a very important tool for troubleshooting or statistics. The php-json-rpc-log library offers a very simplistic way to log JSON-RPC equests and responses.

6.1. Sample code

We’re mostly using Monolog, but the php-json-rpc-log library supports any kind of PSR-3 logger. Simply use the Logged\Server class instead of the original JsonRpc\Server class, and pass a Psr\Log\LoggerInterface to it:

6.2. Usage

In the case above, we passed a Monolog\Logger with a SyslogHandler to it, meaning that requests that we send will be logged to /var/log/syslog. The usage is the same as in the other examples, but we’ll see output like this when we tail syslog:

6.3. More examples

Find a working demo of the php-json-rpc-log library in example 4 of the JSON-RPC demo project.

7. Advanced usage

There are a million ways to combine these components, and I can surely not list them all here. However, here’s a short list of very useful applications.

7.1. Using the API from the shell or via SSH (with ‘root’-only access)

Unlike REST-based APIs, we can easily expose a JSON-RPC based API via other protocols, such as SSH or raw TCP sockets. We can even just use it locally by piping the JSON-RPC request to a PHP script.

In example 5 of the JSON-RPC demo project, we extend the api.php file to support requests from STDIN (if the file is called from the CLI). The API can still be used via HTTP, but the script now checks via php_sapi_name() if we’re in CLI mode.

In addition to that, the demo implements another auth handler (see src/Api/Auth/CliAuthHandler.php) to check if the user is root, and disallows access if she isn’t:

With this insanely simple addition, we can now use the API locally by piping the request to the api.php script:

Now, since we can simply pipe, that means using the API across machines via SSH is trivial:

I’ve been using curl in all the examples, but of course the primary use case of an API is often the use inside a web application. Since JSON-RPC requests are merely POST requests with JSON payload, you can use jQuery’s $.ajax() method — for instance like this:

In example 6 of the JSON-RPC demo project, we implement a small web page to display (and auto-refresh) the list of devices using the API, but only if we’re logged in via a cookie:

Without being logged in (no cookie):
When logged in (with cookie):

To achieve this, we implement yet another auth handler (CookieAuthHandler, see src/Api/Auth/CookieAuthHandler.php), and toggle a cookie when the “Login” button is pressed.

All that’s left is to call the API in regular intervals and update the UI. We implemented a small helper class called Datto.API.Client (see web/example6/js/client.js) to interact with the API from our web app:

That’s it. I hope this was helpful. Please let me know what you think in the comments!

7 Comments

  1. Craig Manley

    Looks good.
    I assume the transport encoding is UTF-8 (or UTF-16 or UTF-32 if configurable).
    Now if 2 applications are to communicate using this library but they both use cp1251 encoding internally instead of UTF-8, where can that be set in this library or will transcoding occur automatically (e.g. using mb_internal_encoding() or ini_get(‘default_charset’)) ?



  2. Dav

    Howdy Philipp,

    “At Datto, we use JSON-RPC based APIs very heavily…”

    I know it’s been a while since you wrote this article
    but it interests me why you choose the JSON-RPC instead of
    (as some claim better, easier, etc) REST/CRUD API?

    What is the objective/technical reason for such a choice ?

    Thanks,
    Dav.


  3. Philipp C. Heckel

    Hey Dav, sorry for the delayed response. Had a baby 2 weeks ago, hence I’m a little busy.

    We chose JSON-RPC and still heavily use it over REST because it is not tied to a protocol (i.e. HTTP). The message itself contains everything needed for the request, as opposed to REST where HTTP verb, path and body are relevant. With JSON-RPC, you can do things like echo '{"jsonrpc":"2.0","id":1,...' | ssh user@myserver apicli, or you can queue requests and batch them properly.

    It is much more versatile than REST, although I must say that lots of people are used to REST. Another drawback is that large binary content (images, files) is not easily transported over JSON-RPC, because it has to be encoded with base64 or something like that, which blows up its size.

    I think it was a good choice, we still mostly use JSON-RPC.


  4. Dav

    Howdy Philipp,

    Wow, congratulations then, and it is totally understandable.

    Thank you for your answer and I hope it won’t be much of a hassle if I dwell on this issue a bit more.
    “…encoded with base64 or something like that, which blows up its size..”
    I think any protocol that uses JSON as its framing (or any nonbinary e.g. XML) format is essentially affected by this issue (the REST included).

    It seems to me (and please correct me if I’m wrong) that the main benefit of the JSON-RPC is its independence from the transport protocol. Which combined with the completeness of the data frame/ message is what makes this protocol truly portable and universal.

    But how to convince the people, so accustomed to the HTTP (and its subset REST) or in the broader sense the prevalent (and annoying to some) webification of the IT, that the JSON-RPC is actually the better protocol to implement the m2m communication especially if a machine/node can be managed without the HTTP overhead?


  5. Philipp C. Heckel

    > But how to convince the people,

    Well, that is the million dollar question, isn’t it? I think if you find a good use case in your org that isn’t easily solvable with REST then you can easily convince people.

    I will say this: To this day we still have these discussions in our org. People are very very very used to REST.

    > without the HTTP overhead

    You’ll still have a transport overhead of some sort. For the most part, we still do JSON-RPC over HTTP, so the this argument is a little flawed, depending how you use it of course. We mainly use it over HTTP, SSH and just over local unix sockets.

    I’ve recently started looking at binary protocols such as gRPC, but of course then you are bound to a transport again. If you’re looking for universally portable and binary-compatible, but transport independent, you could also do straight up protobuf to encode the message and then pick your own transport. That is certainly more cumbersome for devs though.


  6. Dav

    Howdy Philipp.

    >You’ll still have a transport overhead of some sort….

    Yes, true, but it would span in between non-existent (UDS), insignificant (TCP), up to slight/expected (SSH/TLS).

    Me personally, I’m impressed, how you decided to use just regular SSH to transport the RPC calls. It seems so unorthodox and yet very much suited for this role considering e.g. that one could configure the server side to start some specialized cli as a shell alternative for such connections.

    And as we are talking here about the transport layer,
    I’d like to take this opportunity and ask you about the ZeroMQ?
    Have you ever used it?
    What properties/features would you look for in it, for you to consider it as a replacement for your current transport technology?

    >I’ve recently started looking at binary protocols…

    Have you ever considered bjson (http://bjson.org/) ?