API plugin


1Presentation

Temma can easily be used to create web APIs (also known as webservices) of the REST type.

Basically, web APIs are managed by controllers, which receive data sent as GET or POST parameters, and return data in JSON format.

This plugin is used to:

  • manage authentication using public/private key pairs (see below);
  • define the JSON view (so you don't have to define it for every action);
  • manage different versions of the API, which can be used simultaneously.

2Authentication

Some APIs may be freely accessible, with no verification of access rights. But most of the time, the user who connects to the API must authenticate himself, to verify that he/she has the right to access the functionalities.

The API pluggin uses HTTP Basic authentication, based on two keys, a "public" and a "private" key, which are sent as login and password respectively each time the API is requested.

It is important that the site is secured by SSL, so that the keys are not visible to anyone eavesdropping on network exchanges. SSL certificates are now easy to set up (and free of charge), thanks to the Let's Encrypt solution.

The choice was made not to rely on authentication tokens (such as JWT) for two reasons:

  • HTTP Basic authentication is the simplest to implement (on both client and server sides). If it has a bad reputation from the point of view of exchange security, this dates back to the days when most web communications were not secured by SSL. With HTTPS sites, there are no security concerns.
    Tokens, on the other hand, require at least one additional connection to generate the token, during which the identifiers (public/private key or login/password) are sent to the server. This kinematics is no more secure, as the identifiers still circulate on the network: an eavesdropping hacker would see them pass (directly or in a "digest" form that could still be exploited), even if they don't circulate with each request. What's more, this imposes complex management of token expiry times on the customer, with retry mechanisms.
  • Tokens are used to avoid the need to double-check the user's access rights on every request, thus limiting access to the database. While this initially seemed like a good idea, many applications require precise management of access rights; when a user's access is withdrawn, it's not acceptable for them to continue using the API until their token expires. In such situations, token lifetimes are often reduced to the point where authentication has to be re-checked almost every time a request is made. The result is the worst of both worlds: frequent access to the database, but also complex connection kinematics.

3URLs and controllers

This plugin is designed to handle URLs like /v[version]/[controller]/[action].
Examples:

  • /v1/: will call the default action of the default controller
  • /v1/articles: will call the default controller action "\v1\Articles"
  • /v2/user/list: calls the "list()" action of the "\v2\User" controller
  • /v3/user/remove/123: calls the "remove()" action of the "\v3\User" controller, supplying "123" as parameter

Controllers must therefore be placed in the namespace corresponding to the API version number.


4Database

This helper requires two database tables, one named User (containing user information), the other named ApiKey (containing public/private key pairs).

The User table must contain the following fields:

  • id (int): Primary key.
  • date_creation (datetime): User creation date.
  • date_last_login (datetime): Date of user's last authentication.
  • date_last_access (datetime): Date of user's last access.
  • email (string): User's email address.
  • name (string): User name (mandatory field, even if you don't use it).
  • roles (set): Roles assigned to the user.
  • services (set): Services to which the user has access..

The ApiKey table must contain the following fields:

  • public_key (string): Public key of the user.
  • private_key (string): Digest message (SHA-256 algorithm) of the user's private key.
  • name (string): Key pair name (user-defined if required).
  • user_id (int): Foreign key to the user.

Here's an example of a query to create these tables, for which you need to customize the roles and services fields of the User table:

CREATE TABLE User (
    id               INT UNSIGNED NOT NULL AUTO_INCREMENT,
    date_creation    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    date_last_login  DATETIME,
    date_last_access DATETIME,
    email            TINYTEXT CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
    name             TINYTEXT,
    roles            SET('writer', 'reviewer', 'validator'), -- must be personalized
    services         SET('articles', 'news', 'images'), -- must be personalized
    PRIMARY KEY (id),
    UNIQUE INDEX email (email(255))
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

CREATE TABLE ApiKey (
    public_key    CHAR(32) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
    private_key   CHAR(64) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
    name          TINYTEXT NOT NULL DEFAULT ('Default'),
    user_id       INT UNSIGNED NOT NULL,
    PRIMARY KEY (public_key),
    FOREIGN KEY (user_id) REFERENCES User (id) ON DELETE CASCADE
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

5Configuration

5.1Plugin configuration

This plugin must be one of the very first pre-plugins in the execution chain.

A directive must therefore be added to the etc/temma.php file (see configuration documentation):

[
    'plugins' => [
        // list of pre-plugins
        '_pre' => [
            '\Temma\Plugins\Api'
        ]
    ]
]

5.2Database configuration

By default, table and field names are those indicated above, and these tables must be placed in the database opened by the connection named db in the dataSources configuration directive. You have the option of specifying the database name, table names and field names, if different from the default:

[
    'x-security' => [
        'auth' => [
            'userData' => [
                'base'     => 'auth_app',
                'table'    => 'tUser',
                'id'       => 'user_id',
                'email'    => 'user_mail',
                'name'     => 'user_name',
                'roles'    => 'user_roles',
                'services' => 'user_services',
            },
            'apiKeyData' => [
                'base'        => 'auth_app',
                'table'       => 'tKeys',
                'public_key'  => 'public_string',
                'private_key' => 'secret_string',
                'user_id'     => 'identifier_user',
            ]
        ]
    ]
]
  • Lines 4 to 12: User table configuration.
    • Line 5: Name of the database containing the table.
    • Line 6: Table name.
    • Line 7: Name of field containing primary key.
    • Line 8: Name of the field containing the user's e-mail address.
    • Line 9: Name of the user.
    • Line 10: Name of the field containing user roles.
    • Line 11: Name of the field containing the services to which the user has access.
  • Lines 13 to 19: API keys table configuration.
    • Line 14: Name of the database containing the table.
    • Line 15: Table name
    • Line 16: Name of the field containing the public key.
    • Line 17: Name of the field containing the hash of the private key.
    • Line 18: Name of the field containing user identifier.

If the user table contains other fields you wish to retrieve, you can add them to the list. If the fields are not to be renamed, set the key and value to the same name:

[
    'x-security' => [
        'auth' => [
            'userData' => [
                'email'    => 'user_mail',
                'name      => 'user_name',
                'org'      => 'user_organization',
                'birthday' => 'birthday',
            ]
        ]
    ]
]
  • Line 5: Name of the field (user_mail) containing the user's e-mail address.
  • Line 6: Name of the field (user_name) containing the user's name.
  • Line 7: user_organization field added, renamed to org.
  • Line 8: birthday field added, not renamed.

5.3Database configuration using DAO

Rather than using the configuration file (etc/temma.php) to define database parameters, it is possible to use custom DAOs.

Configuration example:

[
    'x-security' => [
        'auth' => [
            'userDao'   => '\MyApp\UserDao',
            'apiKeyDao' => '\MyApp\ApiKeyDao',
        ]
    ]
]

DAO example:

namespace MyApp;

class UserDao extends \Temma\Dao\Dao {
    protected $_tableName = 'tUser';
    protected $_idField = 'use_i_id';
    protected $_fields = [
        'user_id'       => 'id',
        'user_mail'     => 'email',
        'user_roles'    => 'roles',
        'user_services' => 'services',
        'user_name'     => 'name',
        'organization',
        'birthday',
    ];
}

6Auth attribute

Temma provides the Auth attribute, which allows you to specify whether a controller or action can only be accessed by an authenticated user (or not). As this attribute is based on the presence of a currentUser template variable (as well as its roles and services keys), it is fully compatible with this helper.

For example, you can specify that a controller is only accessible to logged-in users:

namespace v1;

use \Temma\Attributes\Auth as TµAuth;

#[TµAuth]
class User extends \Temma\Web\Controller {
    // ...
}

It is possible to specify that certain controller actions are freely accessible, while others require authentication, and others are only accessible to non-authenticated users:

namespace v1;

use \Temma\Attributes\Auth as TµAuth;

class Media extends \Temma\Web\Controller {
    // action accessible to all
    public function list() { }

    // action accessible only to
    // authenticated users
    #[TµAuth]
    public function get() { }

    // action accessible only to
    // non-authenticated users
    #[TµAuth(authenticated: false)]
    public function remove() { }
}

It is also possible to restrict access to controllers or actions to users with a specific role or access to a specific service:

namespace v2;

use \Temma\Attributes\Auth as TµAuth;

class Article extends \Temma\Web\Controller {
    // access authorized only to users with
    // the "manager" role
    #[TµAuth('manager')]
    public function remove($articleId) { }

    // authorized only to users with access
    // to the "images" or "text" services
    #[TµAuth(service: ['images', 'text'])]
    public function list() { }
}

Refer to the attribute documentation to see all its capabilities.


7Key generation

You need to create public and private keys for your users so that they can connect to your API. Usually, this is done via a web interface through which users can request the generation of a new key pair, as well as revoke old keys.

The plugin offers the static generateKeys() method, which returns an associative array containing the public and private keys. The public key is a 32-character long string encoded in base 71 (see BaseConvert helper), while the private key is a 64-character long string (also encoded in base 71).

Both strings can be transmitted to the user. In the database, you must store the public key in clear text; for the private key, a hashed version (using the SHA-256 algorithm) of the key must be stored.
This means that the user must copy the private key when it is displayed to him/her, and that he/she won't be able to retrieve it afterwards (so he/she must be given the option of regenerating new keys if necessary).

Example code:

// key generation
$keys = \Temma\Plugins\Api::generateKeys();

// retrieve keys to be displayed to the user
$publicKey = $keys['public'];
$privateKey = $keys['private'];

// compute private key's hash
$hashedPrivateKey = hash('sha256', $privateKey);

// store keys in database using a previously
// created DAO
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
]);

Each key pair can be given a name, to enable users to manage them (e.g. generate keys for different uses, and have the option of deleting specific keys). If no name is supplied, the default name will be used.
To name the key pair, simply add the name when the pair is registered in the database:

// registration of the key pair, with an associated name
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
    'name'        => $keyName,
]);