Secure external API calls to internal systems – a declarative approach based on JSON Schema
First: This post is focusing on how to achieve fine grained access control (a.k.a field level access control) based on JSON schema as a concept. To implement a fully working access control service – other functionality like authentication, roles and rate-limiting must be in place as well.
Internal systems cannot be exposed as-is
There may be times when we want to provide customers or other external parties access to parts of our internal systems. Not everything in our internal system, because it contains data that must not be exposed, but some specific parts. Parts that we choose. This could be data belonging to our customer or access to initiate some business process from an external system.
How can we make sure some fields or parameters of our internal systems are accessible externally while others must never be exposed?

If our system is easy to extend to allow external exposure on Internet we are in a good position to do this. Then we are able to limit access to just the parts of the system we choose.
If, instead, our system is designed to be internal-only or if our system is designed for use cases different from the ones we want to support, this may be challenging.
More applications or one access control component
If not carefully thought through – we may end up with a lot of custom code in new applications to compensate for the lacking access control in our internal systems.

On the other hand – if we create one component to handle access control really well. We may be able to use that one for any internal system we want to expose.

API calls as JSON data model
By targeting the problem of external API exposure as a data access concern it may be possible to solve by adding a layer on top of our internal system, most of our problems of exposing APIs (if not all !) can be solved with the same mechanism.
Data access concern? An API may be viewed as just data sent (the HTTP request) and data returned (the HTTP response). If viewed through such data lens, not only the payload of the API request and the response but the whole request and response is data.
An HTTP request (for an imaginary Create Order API) can be modelled as data in JSON as:
{
"method": "post",
"url":"https://example.com/orders",
"headers": {
"accept": ["*/*"],
"host": ["example.com"],
"authorization": ["Bearer SOME_BEARER_TOKEN"],
"content-length": ["231"],
"content-type": ["application/json"]
},
"body": {
"productId": "17",
"quantity": 200
}
}
…and an HTTP response can be modelled as:
{
"status": 201,
"headers": {
"location": "https://example.com/orders/7654",
"content-length": ["97"],
"content-type": ["application/json"
},
"body":{
"id": "7654",
"createdAt": "2022-12-13T12:03:02",
"productId": "17",
"quantity": 200,
"unitProductionCost": "1.5"
}
}
Specify access control on field level with JSON Schema
HTTP requests
By modelling the HTTP request and response as JSON it is possible to use a schema to describe what’s allowed and what’s not. JSON Schema is a standard format for describing JSON documents and can be used to set constraints for the request JSON object.
JSON Schema example for an imaginary Create Order API request:
{
"type": "object",
"required": [ // method, url, headers and body is required
"method",
"url",
"headers",
"body"
],
"properties": {
"method": {
"const": "post" // method must be POST
},
"url": {
"const": "https://example.com/orders" // Only this URL is allowed
},
"headers": {
"properties": {
"content-type": {
"type": "array",
"item": {
"type": "string",
"pattern":
"^application[/]([a-z0-9]+[+])?json([;]|$)" // Mime type "application/json" required
}
}
},
"additionalProperties": true // Other header are allowed and can have any format
},
"body": {
"type": "object",
"properties": {
"productId": {
"pattern": "^[a-z0-9]{5}$" // Must be
},
"quantity": {
"type": "number",
"minimum": 1,
"maximum": 1000
}
},
"additionalProperties": false // No other fields in body are allowed
}
}
}
By applying this schema for every API call the following HTTP request will be allowed:
{
"method": "post",
"url":"https://example.com/orders",
"headers": {
"accept": ["*/*"],
"host": ["example.com"],
"authorization": ["Bearer SOME_BEARER_TOKEN"],
"content-length": ["231"],
"content-type": ["application/json"]
},
"body": {
"productId": "17001",
"quantity": 200
}
}
…while the following HTTP request will be denied:
{
"method": "post",
"url":"https://example.com/orders",
"headers": {
"accept": ["*/*"],
"host": ["example.com"],
"authorization": ["Bearer SOME_BEARER_TOKEN"],
"content-length": ["231"],
"content-type": ["application/json"]
},
"body": {
"productId": "17001",
"quantity": 200,
"price": 0 // This is not allowed by schema
}
}
…and the result could inform the API caller of why the request was denied:
{
"status": 403,
"errors": [
{
"rejectedValue": 0, // This value was rejected by the schema
"field": "/body/price", // This field was rejected by the schema
"violations": [
{
"value": false,
"keyword": "/properties/body/additionalProperties".
},
{
"value": "Property 'price' has not been defined and schema does not allow additional properties",
"keyword": "description"
}
]
}
]
}
This means any request not being valid according to the schema will be rejected and an error message with relevant information for the client developer/external system can be sent.
HTTP responses
For responses, we cannot stop execution of the API call (since the request has already been performed by the internal system when the response is sent) but we can make sure only the data we want will be sent to the external system. And we can use JSON Schema as the language to describe this as well.
Let’s look at the model of an HTTP response from our imaginary internal Create Order API:
{
"status": 201,
"headers": {
"location": "https://example.com/orders/7654",
"content-length": ["97"],
"content-type": ["application/json"
},
"body":{
"id": "7654",
"createdAt": "2022-12-13T12:03:02",
"productId": "17",
"quantity": 200,
"unitProductionCost": "1.5"
}
}
If unitProductionCost is something we consider internal it should not be exposed to external systems. This protection can be achieved by defining a schema for the HTTP response and use that schema to filter the HTTP response.
Example:
{
"properties": {
"body": {
"type": "object",
"properties": {
"id": true,
"createdAt": true,
"productId": true,
"quantity": true
},
"additionalProperties": false
}
}
}
This schema will allow any URL, headers or method but it will not allow any other properties than the ones listed in properties object for body (JSON Schema is constraints based and if we do not apply any constraints – everything is allowed. See https://json-schema.org)
The result after filtering the HTTP response object is:
{
"status": 201,
"headers": {
"location": "https://example.com/orders/7654",
"content-length": ["97"],
"content-type": ["application/json"
},
"body":{
"id": "7654",
"createdAt": "2022-12-13T12:03:02",
"productId": "17",
"quantity": 200
}
}
…and unitProductionCost is removed from the response body.
Summary

By adopting JSON Schema as the language to declare what data should be allowed in each HTTP request and what data will be filtered in the HTTP response we can build an reusable component/service to handle fine grained access control for any system integrating via JSON over HTTP.
…and finally
I have built access control in different flavours for customers within banking and finance over the years and I am currently working on a managed access control service for anyone to use. If you are interested in knowing more about how to build your own access control service based on JSON Schema or if a managed service to handle this sounds interesting to you, contact me, I’ll be happy to help.
// Niklas
E-mail: niklas.eldberger@zuunr.com
Twitter: @niklaseldberger
LinkedIn: niklas-eldberger-9b45b2a

Leave a comment