Skip to content
TekMonks edited this page Nov 6, 2021 · 20 revisions

The API Registry

The apiregistry.json file is used to expose APIs securely. The location for this file should be backend/apps/<the app>/conf/apiregistry.json. It acts as a router - mapping API URLs to the backend API code.

Structure of an API Registry Entry

The API registry is a JSON file - the key of the file is the URL for the API and the value in a URL query format, documenting various options for the API and how it should be exposed.

An example of a complex entry for a login API is below

"/apps/{{app}}/login" : "/apis/login.js?keys=xqwiwt98198hjief8923ydewjidw834284wlxj5jwr79205&needsToken=false&addsToken=sub:{{{role}}},flag:{{{result}}},id:{{{id}}}"

Use HTTPS

Please use HTTPS when exposing backend APIs. Various HTTP headers are heavily used to enforce restrictions and security for the backend API by the API registry. All these are easily accessible to man-in-the-middle (MITM) attacks if HTTPS is not used.

API Registry Entry Parameters

The various params are explained below

{{{app}}}

This automatically expands to the name of the application. It can be left as is. For example if <the app> is myNextApp then the full URL to the API would be http|s://<hostname>:<port>/apps/myNextApp/login. Here the scheme http|s and the hostname and port are as defined in the backend API server's configuration. This is defined in the file backend/server/conf/httpd.json.

keys

These are the API keys approved to call the API. It can be multiple keys, separated by a comma. If the key-value contains a comma, then two commas can be used to escape it. The incoming request must contain a header with the name either x-api-key or org_monkshu_apikey whose value is one of the keys expected by the API.

Since multiple keys can be specified, each external client can be provided an independent key to the API. Further, if we want to ban a certain client, that particular key can be removed from the approved set of keys for the API.

This allows for subscription control - that is who can call which APIs.

needsToken

This clause implies that calling the given API needs a valid JWT token. Further, this value can be used to enforce the sub clause of the claims section. For example, if we write needsToken=true then it means any valid JWT token is acceptable. This can be used to enforce, for example, that the user is logged in, but it can't discriminate the role of the user. To enforce roles, we can modify this to - needsToken=admin,user. This will now imply that the sub clause of the JWT token payload (claims) must be carrying the role of admin or user. So only those who have a valid JWT token, with the role of admin or user would be allowed to call the API in this case.

If needsToken is omitted or set to false then it implies the API can be called by anyone and doesn't need any JWT token security enforcement.

addsToken

This clause implies that the API being called adds a new JWT token in response if it is successful. This is typically the case for login APIs. They would generate JWT tokens if the login is successful. Monkshu server can automatically handle both generation and enforcement of such JWT tokens using the API Registry. There is no need for the APIs to worry about security or JWT tokens. addsToken can be used to add other claims to the token as well, which can be enforced later, automatically, using the checkClaims clause. An example of the addsToken clause is shown below

addsToken=sub:{{{role}}},flag:{{{result}}},id:{{{id}}}

This clause implies if the API is successful then add a new JWT token with three claims - the first one being standard subject clause, that is sub and this would be the role of the user. The value of the role is a variable since it is coded in mustache format. The value of this variable would be picked from the JSON response field role, or failing that, the JSON request field role. Similarly, a private claim of ID will be added.

To check if the API succeeded or failed, the flag field of the response will be used and its value must be a boolean. The value of this field would be picked only from the result field of the JSON response. Thus, if the result is true then the JWT token would be added with these claims, else it would not be. The flag itself is also added as a claim to the token, in addition to being a check on whether or not the API succeeded.

The flexibility of the addsToken clause allows us to declaratively secure users, generate tokens only when APIs succeed, and add a user's ID and roles as claims into the token itself for enforcement later. If used correctly, this frees up the APIs from having to ever enforce security, reducing security errors and issues later.

checkClaims

This clause allows the JWT manager of the server to enforce claims. For example, if a user is modifying his profile, we need to ensure that the ID of the profile he is modifying is actually the same ID he logged in with. With Monkshu this is quite easy using the combination of addsToken and checkClaims clauses. For example, consider the following two API declarations

"/apps/{{app}}/login" : "/apis/login.js?keys=xqwiwt98198hjief8923ydewjidw834284wlxj5jwr79205&needsToken=false&addsToken=sub:{{{role}}},flag:{{{result}}},id:{{{id}}}",
"/apps/{{app}}/updateUser" : "/apis/updateUser.js?keys=xqwiwt98198hjief8923ydewjidw834284wlxj5jwr79205&needsToken=user&checkClaims=id",

Here the first API login adds in a JWT claim of ID into the JWT token on successful login. The updateUser API declares a checkClaims of ID using the field id. This then instructs the JWT token manager to enforce, that the signed token's id claim must match the value of the id field of the JSON request. If this check fails, then reject this request as a security error.

This ensures that the updateUser API is only ever called when the ID of the user coming inside the request matches the value of the ID in the JWT token's claims. Since a JWT token is a signed token, its claims can't be fudged by the user, thus ensuring the APIs are automatically secured against any ID injection attacks, and that the API can trust the ID in the JSON request as being from the same user who logged in with the same ID.

This simplifies the development of APIs and offloads security enforcement to the server's JWT manager so that API authors can concentrate on the logic of the API and not have to worry about security and security enforcement.

encrypted

If this field is set to true will encrypt the JSON payload using the AES-256 algorithm. Both the request and response will be encrypted. The key will be a shared key, whose value is stored in the backend/server/conf/crypt.json file. The incoming request and response format will be as follows

{
    data: [encrypted JSON request or response]
}

The caller must send the request in this format, where the actual JSON payload is encrypted using the shared key and then the value of the data field is set to this encrypted payload. The response will follow the same scheme.

Usually the use of this field is not required, unless the APIs can't be exposed over an HTTPS channel.

get

This field can be set to true or false. If set to true then the API is a GET request API, else it is a POST request API. If this field is omitted, then the API would be a POST request API, by default. The APIs themselves always receive requests in Javascript object format (parsed JSON object), and must always respond in Javascript object format as well, regardless of whether they are GET or POST APIs.

Cookbooks

API Keys

Requirement

There is a need to control who can call the APIs, and organizations that can call the APIs should have their own set of authorizations. There is also a need to independently authorize them or deauthorize them from calling the APIs.

Solution

(1) For each authorized organization create an independent API key.

(2) Pass them the key and advise them to keep it secure.

(3) In the keys clause of the API registry add their keys to authorize them.

(4) From the keys clause of the API registry remove their keys to deauthorize them.

Example

"/apps/{{app}}/keyedAPI" : "/apis/login.js?keys=xqwiwt98198hjief,8923ydewjidw83428,4wlxj5jwr79205,LFAkZ8Gdekc8ddJ5KOvo"

This example shows 4 independent keys which can be passed to four different organizations to call the API. They 4 keys are

xqwiwt98198hjief
8923ydewjidw83428
4wlxj5jwr79205
LFAkZ8Gdekc8ddJ5KOvo

To deauthorize simply delete the key. For example,

"/apps/{{app}}/keyedAPI" : "/apis/login.js?keys=8923ydewjidw83428,4wlxj5jwr79205,LFAkZ8Gdekc8ddJ5KOvo"

Now the organization with the key xqwiwt98198hjief can no longer call the API.

Securing the APIs by role

Requirement

There is a requirement that only the admin can call admin APIs.

Solution

(1) Add in an addsToken clause to the login API which sets the subject (sub) field to the user who just logged in.

(2) Also add in a clause for flag which is matched to the boolean field in the response which indicates a successful login.

(3) Ensure that the login API returns a matching field in its response JSON with the role and a success flag field.

(4) Now for the API which needs to be secured, add in a needsToken clause matching the role or roles which should be able to call it.

Example

"/apps/{{app}}/login" : "/apis/login.js?keys=xqwiwt98198hjief8923ydewjidw834284wlxj5jwr79205&needsToken=false&addsToken=sub:{{{role}}},flag:{{{result}}},id:{{{id}}}",
"/apps/{{app}}/updateUserByAdmin" : "/apis/updateUser.js?keys=xqwiwt98198hjief8923ydewjidw834284wlxj5jwr79205&needsToken=admin",

Here the login API sets the sub to a role, from the response, if the flag field is true in the response. For example, if the response was as below, then the sub would be set to admin since the result field is true.

{
    result: true,
    id: "admin@tekmonks.com",
    role: "admin"
}

Now when the updateUserByAdmin API is called, the needsToken clause which is set to admin will ensure that only users who were previously authenticated as under the admin role will be allowed to call this API.

Ensure user-id in API call matches the ID of the authenticated user

Requirement

This is a common scenario where we need to ensure the users are calling backend APIs and not modifying other users' accounts. This is a claims enforcement scenario - that is, the user ID which is encoded in the request matches the user ID which was used to authenticate.

Solution

(1) Encode the login API to send back the user ID as part of the JWT claims token. This ensures that on successful login, the user ID gets added to the claims, and gets signed by the server. This ensures that the user can't modify the user ID encoded in the token's claims section.

(2) Add in a checkClaims clause with the field name matching the field name in the request which carries the user ID. The same field name must have been used in step 1 as well.

(3) When the server encounters this checkClaims clause, it will enforce that the value of the user ID field in the request is exactly the same as the value of the user ID encoded into the JWT token. Since the token can't be modified by the user (as it is signed by the server) and was set on successful login, this ensures the user ID in the request is correct.

Example

"/apps/{{app}}/login" : "/apis/login.js?keys=xqwiwt98198hjief8923ydewjidw834284wlxj5jwr79205&needsToken=false&addsToken=sub:{{{role}}},flag:{{{result}}},id:{{{id}}}",
"/apps/{{app}}/updateUser" : "/apis/updateUser.js?keys=xqwiwt98198hjief8923ydewjidw834284wlxj5jwr79205&needsToken=user&checkClaims=id",

Here the first API login adds in a JWT claim of id into the JWT token on successful login. The updateUser API declares a checkClaims of ID using the field id. This then instructs the JWT token manager to enforce, that the signed token's id claim must match the value of the id field of the JSON request. If this check fails, then reject this request as a security error.

Encrypted JSON Payloads

Requirement

In this scenario the requirement is that the JSON payload must be encrypted independently of the transport.

Solution

(1) Set the encrypted filed in the API declaration to be true.

(2) Share the cryptographic key as a shared secret between the server and the API clients.

Example

"/apps/{{app}}/mySecureAPI" : "/apis/updateUser.js?encrypted=true

Since the encrypted field is set to true this will encrypt the JSON payload using the AES-256 algorithm. Both the request and response will be encrypted. The key will be a shared key, whose value is stored in the backend/server/conf/crypt.json file. The incoming request and response format will be as follows

{
    data: [encrypted JSON request or response]
}

The caller must send the request in this format, where the actual JSON payload is encrypted using the shared key and then the value of the data field is set to this encrypted payload. The response will follow the same scheme.