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

Datastore 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, continuing 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 take place 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 it will only ever be used by itself and so never 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 that have been 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, if an invalid property is used, the ACL rule always denies access.

This special 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 may still use the document. syntax when creating a new document using a PUT request. The ACL rule is evaluated using the property values that have been POSTed in the request instead. These are the document values once 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 that have been 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, if an invalid property is used, the ACL rule always denies access.

This special 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 may still use the document. syntax when creating a new document using a PUT request. The ACL rule is evaluated using the property values that have been POSTed in the request instead. These are the document values once 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 was not signed with the correct secret key and cannot be decoded. Datastore returns 401 Unauthorized instead of 403 Forbidden to indicate that there was no user data to compare against. So it is unknown if the current user is allowed to make the request or not.

{
"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 documention discusses JSON Web Tokens (JWTs) and how they are used to pass trusted data between Matrix and the DDS via 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 DDS 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 below. The JS application makes a request to 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 DDS. Both Matrix and the DDS know a shared secret key that the DDS uses to ensure that the JWT has been generated by Matrix.

image

JWT payload

A JS application that makes requests to the DDS 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 DDS.

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 DDS and have the DDS think that data can be trusted. Instead, trusted data must be hashed using the secret key on the server, which can be performed using the JWT asset within Matrix.

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

The DDS 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 is required by the DDS, and only data that must be trusted to enforce access controls. JWTs have a 7kb length limit (roughly the size of a HTTP header) and so 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 specifically look for a key called name.

The DDS does not require any specific properties within the JSON object. While using standard claims for user data is not a requirement for interacting with the DDS, 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, it can be used by a third party to send additional API requests using the same trusted data. If the JWT payload contains user data, it is possible for a 3rd party to use the intercepted JWT to perform actions as that user. This type of attack is known as a replay attack, as an action is being replayed with the same trusted data.

To limit replay attacks, JWTs should have an expiry time, allowing them to remain useful for a specific period of time 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 DDS 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 consider to never expire and the DDS will not check its age.

The length of expiry can be set by the Matrix JWT asset.

If you are using the JS SDK to communicate with the DDS, the JWT is automatically refreshed when it gets within 2 minutes of expiring. To ensure the JWT is not being refreshed for every DDS 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 would look like this:

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

Generating a JWT

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

Once 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 2 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 JWT header and payload have been generated by the Matrix JWT asset, they must be hashed using a secret key. The secret key is provided by the DDS 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 may 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 are using the DDS 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.

Each time an API call is made to the DDS, 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 back 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 DDS, the JS application can not verify that 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 this data to the DDS outside of the JWT.

+ If trusted data is to be stored inside the DDS, 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, you pass it using 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