Securing your data

Collection and document endpoints require an access control list (ACL) statement. Specify a statement using the x-datastore-acl tag within the blueprint for each endpoint and HTTP method specified.

For collection requests, the ACL for both the collection and the document is evaluated and only returns those documents for which the ACL statements are satisfied.

Available comparison operators

ACL statements support the following comparison operators:

Name Example Result

Identical

a === b

TRUE if both a and b are the same value and data type.

Not identical.

a !== b

TRUE if either a is not the same as b or a does not have the same data type as b.

Less than.

a < b

TRUE if a is less than b and both are the same data type.

Greater than.

a > b

TRUE if a is greater than b and both are the same data type.

Less than or equal to.

a ⇐ b

TRUE if a is less than or equal to b and both are the same data type.

Greater than or equal to.

a >= b

TRUE if a is greater than or equal to b and both are the same data type.

Available logical operators

Logical operators join multiple access control rules within a single ACL.

Name Example Result

AND

a && b

TRUE if both rule a and rule b are TRUE.

OR

a || b

TRUE if either rule a or rule b are TRUE.

Bracketing ACL statements

You can group multiple access control rules inside brackets:

Example Result

(a || b) && c

TRUE if either rule a or rule b are TRUE and rule c is TRUE.

(a && b) || c

TRUE if both rule a and rule b are TRUE or rule c is TRUE.

Short-circuiting of ACL statements

Data Services performs the least number of comparisons when determining if an ACL statement is satisfied.

Therefore, if the user were granted access early in a statement that would render the whole statement to evaluate TRUE, the ACL comparison would be short-circuited.

This can then hide errors in subsequent rules in the statement, such as a comparison using a JWT property that does not exist.

Example Result

a || b

If rule a evaluates to TRUE, then rule b is never evaluated.

a || b || c

If rule a evaluates to FALSE, then rule b would be checked, until the statement evaluates to TRUE or FALSE.

(a && b) && c

If rule a evaluates to FALSE then neither rule b or c are evaluated.

Short-circuiting does not occur within collection requests where the ACL statement uses the values from the current collection’s documents.

Special values in ACL statements

Name Examples Note

public

x-datastore-acl: public

This special value grants access to all requests for that endpoint.

The syntax public is an expression alias of true === true. Since this is an expression and not a value, it does not need to be compared to another value. It is expected to only be used by itself rather than as part of the more complicated expression.

jwt.{property}

x-datastore-acl: jwt.is_admin === TRUE

The jwt. syntax allows access to trusted data stored within JWT that is passed with the request. Data types in a JSON web token payload set are respected when evaluating expressions. The ACL rule denies access when a JWT property is used as part of an expression but does not exist in the JWT payload.

document.{property}

x-datastore-acl: document.is_deleted === FALSE
x-datastore-acl: document.userid === jwt.userid
x-datastore-acl: document.$id === jwt.userid

The document. syntax allows access to document properties stored within the data service. This can be used within ACL statements at document endpoints only.

The data type of the document value is determined by what type the document property has been documented as in the JSON schema when access rules are evaluated.

If the specified property has no value stored, the document property value is compared to NULL. However, the ACL rule always denies access if an invalid property is used.

This particular value also allows for referencing the source properties of the document.

Document property values can also be compared to values stored within the JWT.

The ACL expression can still use the document. syntax when creating a new document using a PUT request. Instead, the ACL rule is evaluated using the property values POSTed in the request. These are the document values after the request is complete.

parent.{property}

x-datastore-acl: parent.is_deleted === FALSE
x-datastore-acl: parent.userid === jwt.userid
x-datastore-acl: parent.$id === jwt.userid

The parent. syntax allows access to a parent document’s properties stored within the data service. This can be used within ACL statements on any non-root level collection or document endpoint where a parent document exists.

The data type of the document value is determined by what type the document property has been documented as in the JSON schema when access rules are evaluated.

If the specified property has no value stored, the document property value is compared to NULL. However, the ACL rule always denies access if an invalid property is used.

This particular value also allows for referencing source properties of the parent document.

Parent document property values can also be compared to values stored within the JWT.

The ACL expression can still use the document. syntax when creating a new document using a PUT request. Instead, the ACL rule is evaluated using the property values POSTed in the request. These are the document values after the request has been completed.

Error codes

Code Description JSON

401 Unauthorized

Returned when an ACL protects a request, but no valid JWT was provided with the request. This can be caused by no JWT being sent or the supplied JWT needing to be signed with the correct secret key and cannot be decoded. Data Services returns 401 Unauthorized instead of 403 Forbidden to indicate no user data to compare against. So, whether the current user can make the request is unknown.

{
"title": "Unauthorized",
"status": "401"
}

403 Forbidden

Returned when an ACL rule failed for a request. No additional information about what rule failed is supplied in the error object to avoid information leakage.

{
"title": "Forbidden",
"status": "403"
}

Blueprint Example

paths:
  /my-collection:
    get:
      x-datastore-acl: public
…
  /my-collection/{documentid}:
    parameters:
      - in: path
        name: documentid
        required: true
    get:
      x-datastore-acl: document.userid === jwt.userid
…

Using JWTs for trusted user data

This documentation discusses JSON Web Tokens (JWTs) and how they pass trusted data between Matrix and the Data Services through a JS application.

JWTs were selected as they are backed by a popular open standard that is widely used within web APIs for this purpose, and so fits with the Data Services goal of using open standards where possible, as it does for API definitions and data validation.

A high-level overview of the data flow is shown in the image. The JS application requests Matrix for a JWT, which includes a JSON object containing user data. The JS application then includes this JWT with all API requests to the Data Services. Both Matrix and the Data Services know a shared secret key that the Data Services uses to ensure that Matrix has generated the JWT.

data flow overview

JWT payload

A JS application that requests the Data Services can only pass trusted data using a JWT. This ensures that the data has been signed using the secret key for that application’s specific instance of the Data Services.

A secret key cannot be used within client-side code as it would be visible to end users, allowing them to send any data they want to the Data Services and have the Data Services think that data can be trusted. Instead, trusted data must be hashed using the server’s secret key, which can be performed using the JWT asset within Matrix.

Trusted data is primarily used within the Data Services for access control. While trusted data is also often stored within a document, this is typically done to enable future access control functions, such as allowing a user to edit their comments only.

The Data Services does not require any trusted data by default, so it is up to the user to decide what data they require based on the access control rules they have defined.

A JWT should only contain data that the Data Services require and only data that must be trusted to enforce access controls. JWTs have a 7kb length limit (roughly the size of an HTTP header) and are not appropriate for sending large amounts of user data.

All trusted data is gathered into a JSON object. Within the context of a JWT, each of the properties is called a claim. A list of standard claims has been defined in the IANA registry, providing standard property names that can be used within the JSON object.

This is particularly useful if a service is expecting something like a user’s name, allowing it to look for a key called *name*specifically.

The Data Services does not require specific properties within the JSON object. While using standard claims for user data is not a requirement for interacting with the Data Services, it should still be considered a best practice.

An example JSON array of data that the Matrix custom asset could produce at this time would be:

{
    "sub": "jsmith",
    "given_name": "John",
    "family_name": "Smith",
    "admin": true
}

Within the context of a JWT, this JSON object is known as the payload.

JWT expiry

If a JWT is intercepted, a third party can use it to send additional API requests using the same trusted data. If the JWT payload contains user data, a 3rd party can use the intercepted JWT to perform actions as that user. This type of attack is known as a replay attack, as an action is replayed with the same trusted data.

JWTs should have an expiry time to limit replay attacks, allowing them to remain functional for a specific period only. While the JWT can be used in a replay attack within this time window, it is still far safer than allowing a JWT to remain effective indefinitely. In addition, the expiry time also ensures that user data must be refreshed at a regular interval, allowing for modified user data or permissions to be reflected relatively quickly within a JS application.

The Data Services expects an expiry time to be provided inside an exp claim, with the value being a NumericDate, as defined within RFC 7519. If this claim is not provided, the JWT is considered to never expire and the Data Services will not check its age.

The Matrix JWT asset can set the length of expiry.

If you are using the JS SDK to communicate with the Data Services, the JWT is automatically refreshed when it gets within 2 minutes of expiring. To ensure the JWT is not being refreshed for every Data Services request, set the JWT expiry time to a value longer than 2 minutes.

If an expiry time claim has been included in the payload, the JWT payload will look like this:

{
    "sub": "jsmith",
    "given_name": "John",
    "family_name": "Smith",
    "admin": true,
    "exp": 1300819380
}

Generating a JWT

If you use the Matrix JWT asset, you do not need to generate your JWTs. The Matrix asset will do this for you.

When properly constructed, a JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The JWT is a dot-separated string consisting of three parts:

  • The first part is the base64URL encoded JSON header.

  • The second part is the base64URL encoded JSON payload.

  • The last part is the base64URL encoded hash of the previous two parts.

base64URL encoding is not the same as base64 encoding. Using base64 encoding does not guarantee that the data is URL safe as the + and / characters can appear in the string.

To verify that the Matrix JWT asset has generated the JWT header and payload, they must be hashed using a secret key. The secret key is provided by the Data Services when a project is deployed, and it must be entered into the Matrix JWT asset manually.

The HMAC-SHA256 hashing algorithm is used to hash the header and payload. This algorithm is the minimum requirement for hashing in the JWT standard and is also the most widely used and least complex hashing method supported by JWTs. The use of this algorithm is included in the JWT header data as follows:

{
    "alg": "HS256",
    "typ": "JWT"
}

To create the JWT, the header and payload must first be base64URL encoded. These values are then used to generate the HMAC-SHA256 hash, which must also be base64URL encoded. The three strings are then combined using dots to form the JWT. In PHP, the process might look like this:

$header = rtrim(
    strtr(base64_encode(json_encode($header)), '+/', '-_'),
    '='
);
$payload = rtrim(
    strtr(base64_encode(json_encode($payload)), '+/', '-_'),
    '='
);

$hash = hash_hmac('sha256', $header.'.'.$payload, $secretKey, true);
$hash = rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
$jwt  = $header.'.'.$payload.'.'.$hash;
A useful tool to create and test JWT tokens can be found here: https://jwt.io/

Using a JWT

If you use the Data Services JS SDK, you do not need to worry about including the JWT with API requests. The JS SDK automatically sources and attaches a JWT to all requests.

Whenever an API call is made to the Data Services, the JS application must include the JWT in the HTTP Authorization header if the request requires trusted data. The format of the header is:

Authorization: Bearer <token>

Where <token> is the JWT. For example:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

To get the JWT, the JS application makes a request to the Matrix JWT asset, which responds with a JWT in the body of the response.

As the JWT contains the base64URL encoded payload, the JS application can base64URL decode the second part of the JWT and get access to the data contained within.

Unlike the Data Services, the JS application can not verify the data has been hashed with the correct secret key, so it can not consider this data trusted. It can still use this data to display UI elements, but it should not send it to the Data Services outside of the JWT.

+ If trusted data is to be stored inside the Data Services, the JSON Schemas should source the data from the JWT directly instead of the JS application POSTing the data inside the request body.

Passing a JSON web token to your data service

You can use the JavaScript SDK to pass a JWT with any request to your data service.

You must pass the URL to your JWT service as a setting, and the JavaScript SDK retrieves and attaches the JWT to the requests.

Read the JavaScript SKD documentation for more information: https://docs.squiz.net/datastore/latest/features/javascript-sdk.html

To pass a JWT using CURL, use the Authorization header:

curl \
    -X GET \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer <token>" \
    --silent \
    -fS https://my-data-service.datastore.squiz.cloud/my-collection