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.
{
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:
{
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:
{
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:
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:
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:
{
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:
{
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
{
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
{
test: {
request: { type: 'string' },
response: { type: 'string' }
}
}
Required Fields
{
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
{
test: {
request: {
type: 'object',
fields: {
users: {type: 'array', of: {
type: 'object',
fields: {
email: {type: 'string', required: true},
name: {type: 'string'},
}
}},
}
},
response: 'object',
}
}
Default Values
{
test: {
request: {
fields: {
color: {type: 'string', default: 'red'}
}
}
// No response signature allows all fields
}
}
Nested Objects
{
test: {
request: {
fields: {
name: {type: 'string'},
address: {type: 'object', fields: {
street: {type: 'string'},
zip: {type: 'string'},
}},
}
},
response: 'object'
}
}