Skip to main content

Understanding Access Control and Integrating with other Microservices

Introduction to scope based access control

Liquid uses a list of permission names (also known as scopes) to determine if a user is authorized to access a specific API / resource. There is a set of default permissions assigned to users at the time of account creation. By default, the scope assigned is delegated:all, but it can be changed by setting the option user.account-creation.default-scope to a comma separated list of scopes you would like to be assigned for a user at the time of account creation. This does not affect any of the existing accounts. delegated:all means the user has access to all user level APIs like follow, unfollow, block, unblock and profile edit, but no access to admin level APIs.

See list of all scopes available in Liquid

You can assign additional scopes to users than the ones assigned at the time of account creation by using the /user/admin-api/access endpoint. You either need to be a super_admin or you need to have access to the scope admin:profile:access:write to use this API. Here's a sample request to the endpoint which assigns a user access to some admin APIs:

Request:

POST https://liquid-host/user/admin-api/access

Body:

{
"targets": ["list", "of", "user", "ids"],
"targetType": "user",
"scope": ["admin:profile:all", "admin:profile:ban:write", "admin:profile:credits:write"],
"operation": "set"
}

The above request provides an user permissions to edit other user's profile info, ban other users in the system and create OAuth clients that can access client APIs. targetType can be either user or client which means it is also possible to assign scopes to OAuth clients. Use operations del or add for more granular control.

warning

As a general best practice, it is preferrable to assign permissions to users starting with the least privilege and then move up to more generic permissions if they need more. Assigning scopes like * and admin:all can give users more permissions than they actually require and can be catastrophic. Don't shoot yourself in the foot.

You can remove permissions by sending status attribute as false.

Extending the Scope list

If you have the need to add some additional scopes in the service, which you probably will (for instance, you might want to introduce a scope called delegated:chat:read which controls if users can read a chat in your chat microservice), you can do so by passing to Liquid a JSON file that contains the metadata about the new scopes.

Here's a sample scope-extensions.json file:

[
{
"name": "sub-scope",
"description": "A scope that directly gets attached under *",
"parent": "*"
},
{
"name": "delegated:chat:all",
"description": "View and manage your chats.",
"parent": "delegated:all"
},
{
"name": "delegated:chat:read",
"description": "View your chat history.",
"parent": "delegated:chat:all"
},
{
"name": "delegated:chat:write",
"description": "Manage your chat history.",
"parent": "delegated:chat:all"
}
]

The parent field specifies the hierarchy of permissions. In this case, if a user is assigned delegated:chat:all, they implicitly have the permissions delegated:chat:read and delegated:chat:write.

Once this file is ready, you can extend the Liquid scopes by providing the path to this file in the option system.scope-extension-file-path. Note that scope tree is printed by Liquid when you run the service. Check the logs to see if the extended hierarchy is as indented.

Connecting to liquid from other microservices

If the microservices that need to use Liquid as an auth service are running on Node.js, you can authenticate your routes by using the Liquid Node Authenticator package.

When you authenticate a user by using const tokenDetails = liquidAuthenticator.authenticate(token);, Liquid returns some important information about the token and the user associated with it.

You can get the user associated with the token by inspecting the tokenDetails.user field.

Under the hood, Liquid Node Authenticator (LNA) utilizes the API endpoint /oauth/introspect by sending the token in token field of the request body to get this information. If your microservice is not running on node, you can use this API to authenticate users from your microservice.

Each token comes with a scope field that contains all authorized scopes for the token. You can choose to disallow an API call if the scope field doesn't include the scope that is required for an API call. This is extremely simple to do in Liquid Node Authenticator. Simply call the checkTokenScope method with the token details:

// auth-middleware.js

// Some code...
const tokenDetails = await liquidAuthenticator.authenticate(token);
// Save token details to res.locals or somewhere accessible by the API code
// More code...

// api.js

// Some code...
// Get tokenDetails from res.locals

const allowed = await liquidAuthenticator.checkTokenScope(
"your:scope:name",
token /* tokenDetails object acqurired in authenticate() function */
);

if (allowed) {
// Scope is allowed, continue with action
} else {
// Scope is NOT allowed, send insufficient priviledge error
}

If for some reason, you want to do the scope checking by yourself (NOT RECOMMENDED), here's a sample implementation:

const response = await fetch(`https://liquid-host/user/scopes`);
const scopes = (await response.json()).data.scopes;

function isScopeAllowed(scope, allowedScopes) {
const scopeObject = scopes[scope];
if (!scopeObject) {
return false;
}
if (allowedScopes.includes(scopeObject.name) || allowedScopes.includes(scopeObject.parent)) {
return true;
} else if (scopeObject.parent) {
return isScopeAllowed(scopeObject.parent, allowedScopes);
} else {
return false;
}
}

console.log(
isScopeAllowed("delegated:chat:read", ["delegated:chat:all"])
); // true