My name is Philipp C. Heckel and I write about nerdy things.
This site moved here recently from blog.philippheckel.com!

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


Programming

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


At Datto, 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 Datto\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 at Datto, but the php-json-rpc-log library supports any kind of PSR-3 logger. Simply use the Logger\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!

2 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’)) ?



Leave a comment

I'd very much like to hear what you think of this post. Feel free to leave a comment. I usually respond within a day or two, sometimes even faster. I will not share or publish your e-mail address anywhere.