Skip to content

API Signatures

The Ioto web server supports API access signatures. This feature allows you to verify the authenticity of requests to your API and the integrity of responses to the client.

This greatly improves the security of device agent by ensuring that only valid requests are processed and that important data is not exposed to unauthorized users.

How it works

The web server can configure an API signature file that specifies the allowed API signatures. This specifies the request, query and response payloads data types and fields. The web server will then verify the signature of the request and response against the allowed signatures.

If a request does not match the allowed signatures, the web server will return a 400 Bad Request error.

How to use it

The API signature file is a JSON file that specifies the allowed API signatures. The file is typically named signatures.json5 and is specified via the signatures property in the web.json5 configuration file.

json5
{
    signatures: {
        enable: true,
        path: 'state/config/signatures.json5'
        strict: true,
    }
}

The path property specifies the path to the API signature file. The strict property specifies whether the web server should strictly enforce the API signatures. If a request has unknown fields or missing required fields, the request will be rejected. If strict is false, the web server will warn about any requests that do not match the allowed signatures but will still process the request.

Strict mode also request that APIs must have request and response definitions.

Example Signature File

Here is an example of a signature file:

json5
{
    user: {
        login: {
            role: 'public',
            request: {
                fields: {
                    username: {type: 'string', required: true},
                    password: {type: 'string', required: true},
                }
            },
            response: {
                fields: {
                    token: {type: 'string', required: true},
                }
            },
        },
        logout: {
            role: 'user',
            request: {
                fields: {
                    token: {type: 'string', required: true},
                }
            },
            response: null,
        }
    }
}

Matching Signature Entries

The signature file is indexed by the URL path after stripping the route matching prefix.

For example, consider the following routes in the web.json5 configuration file:

json5
{
    routes: [
        { match: '/auth/', handler: 'action', validate: true },
    ]
}

This creates a URL route for requests that start with /auth/. The validate property instructs the web server to validate the request against the signature file.

The signature entry is indexed by the URL path after stripping the route matching prefix. If a request /auth/user/login was received, the /auth/ would be stripped and the user/login would be used to index the appropriate entry in the signatures file. In this case, the controller name of user and the method of login would be used.

If you have a URL format that does not match the controller/method format, you can use the webValidateRequest function. You can invoke this manually from your webRequestHook on the WEB_HOOK_ACTION event. The function takes a search path into the signature file as a parameter.

For example:

c
if (!webValidateRequestBody(web, "user.login")) {
    // Failed to validate request body
}

Note that the path is in a dot notation format that maps to the signature file.

Signature File Format

The signature file is in JSON5 format which is a superset of JSON and permits comments, trailing commas, unquoted keys and single quoted, and multiline strings.

The top-level of a signature file is a set of controllers. Each controller is a JSON5 object that contains the method signatures for the controller.

There is a special method named _meta that contains metadata about the controller itself.

The methods blocks contain request, response and request.query blocks.

  • The request block contains the request payload data types and fields.
  • The response block contains the response payload data types and fields.
  • The request.query block contains the request query parameters data types and fields.

A block can be set to a data type or a collection of properties. For example:

json5
request: null,
request: 'string',
request: {
    type: 'array',
    ...
}

Supported types are: object, array, string, date, number, boolean or null. If the type is object, the block may contain a fields property that specifies the object fields. If the type is array, the block contains an of property that specifies the type of the array items. The of property is an object that contains sub-fields (or arrays). These can be nested to arbitrary depth.

The fields property contains zero or more field definitions where the field key is the name of the field. A special key of * can be used to specify that all fields not otherwise specified are allowed.

Field Definitions

Field definitions can define the following properties:

  • default: A default value to use for the field if not specified.
  • drop: If true, the field is not included and is dropped.
  • fields: Definition for a nested object.
  • of: Definition for a nested array.
  • notes: A markdown description of the field.
  • required: If true, the field is required.
  • role: The role required to access the field.
  • type: The type of the field.
  • validate: A regular expression to validate the field value.

Drop Properties

It is useful to drop fields that should not be disclosed in the response. This can be done by setting the drop property to true. You can also set the drop property to a string that specifies the role required to retain the field. For example:

json5
{
    fields: {
        password: {type: 'string', drop: true},
        balance: {type: 'string', drop: 'user'},
    }
}

So that drop fields can be specified in database schema, the following format is also supported:

json5
{
    fields: {
        password: {type: 'string', drop: {
            request: 'admin',
            response: true,
        }},
    }
}

This will drop the password field for requests with roles less than admin and for all responses.

Examples

Wildcard Field

json5
{
    response: {
        type: 'object',
        fields: {
            name: {type: 'string', required: true},
            password: {type: 'string', drop: true},
            // Wildcard field that allows all other fields
            '*': {},
        },
    }
}

Primitive String Request/Response

json5
{
    test: {
        request: { type: 'string' },
        response: { type: 'string' }
    }
}

Required Fields

json5
{
    test: {
        //  Test request, response fields and required and drop
        request: {
            type: 'object',
            fields: {
                email: {type: 'string', required: true},
                name: {type: 'string'},
            }
        },
        response: 'object'
    }
}

Array of Objects

json5
{
    test: {
        request: {
            type: 'object',
            fields: {
                users: {type: 'array', of: {
                    type: 'object',
                    fields: {
                        email: {type: 'string', required: true},
                        name: {type: 'string'},
                    }
                }},
            }
        },
        response: 'object',
    }
}

Default Values

json5
{
    test: {
        request: {
            fields: {
                color: {type: 'string', default: 'red'}
            }
        }
        // No response signature allows all fields
    }
}

Nested Objects

json5
{
    test: {
        request: {
            fields: {
                name: {type: 'string'},
                address: {type: 'object', fields: {
                    street: {type: 'string'},
                    zip: {type: 'string'},
                }},
            }
        },
        response: 'object'
    }
}