Development of Security Proxy. Dynamic Rights

Hello everyone! The classic approach to authorization is when its control is placed inside a specific service in the form of static rules. That is, the role and rights check from the JWT token is hardcoded into the code. In the first versions of our services, this was done. Later, the idea was born to remove this load from them and transfer it to a thin proxy service, which DevOps will deploy as a sidecar to the protected service. GraphQL was chosen as the technology.

My name is Sergey Kotelnikov, I am the lead backend developer in the product team called «Mycelium» at Discovery Labs. I will tell you how we ensured the security of a ready-made business service without changing it.

Disadvantages of the previous solution

The previous solution with the proxy service had disadvantages:

  1. Although we transferred the responsibility for user authorization to SecurityProxy, the business service was responsible for selecting requests for protection in the GraphQL API. That is, the schema provided by the business service had to be marked up with special directives that we invented ourselves. They are not in the standard.

  2. The service "knew" about the protection features, at least about the need for markup in the GraphQL schema and the meaning of rights for the operation. That is, protection requires adding and changing the service code itself with the synchronization of the service rights database (grants).

This is not a problem for corporate development, when the services are written by the same people who created and maintain the authorization solution itself. But if the services are written by others (companies), without support for directives at the library level, which want to comply with the standard, then managing this is much more difficult.

Solution

To eliminate the aforementioned shortcomings, we decided to set authorization rules for the service not from the service's own schema, but through the administration utility. So that the administrator assigns rights to users using roles, as well as assigns rights to the service's operations, without interfering with its structure. That is, we a priori treat the business service as a black box, without trying to understand its structure or change it.

Technical Implementation

Participants in the process: consumer, business service, its SecurityProxy, Grants service (with DB), and administration utility.

First Launch. Service Registration

At the first launch, the Security Proxy reads the schema from its business service. Our schema is standard, it does not contain any directives for authorization. SecurityProxy requests authorization rules for this schema from the Grants service. Since this is the first launch, the Grants service "knows nothing" about this schema. Then SecurityProxy transmits information about it to Grants. Grants records in the DB: service ID, schema hash, list of fields for authorization. Thus, SecurityProxy notifies the system about the registration of a new service or schema.

Note: by default, access to unmarked fields is absent. That is, without assigning rights, the service will be unavailable.

Assigning Authorization Rules

The administrator assigns authorization rules to the new service and notifies SecurityProxy of the change. After that, SecurityProxy reads the rules for its schema from Grants and "understands" which fields and according to which rules need to be checked. The service becomes protected.

Types of rules:

  • AllowAnonymous — access is allowed without verification;

  • Authenticate — access is allowed upon authentication;

  • Authorize — access is allowed with a special right in the JWT token.

For example, there is a Clients service that allows you to get and change client data. Suppose it has operations (in terms of GraphQL fields):

query client(id) — request for client data

mutation updateClient(id, data) — change client data

The administrator sets the domain CLIENTS, with operations GetClient and UpdateClient. Then assigns these rights to a role, for example, "Office Employee". The administrator also assigns the service Clients the right Authorize(CLIENTS.GetClient) for query client, and Authorize(CLIENTS.UpdateClient) for mutation updateClient.

After that, only an employee with the specified role will have access to the specified business service operations.

Changing the business service schema

In practice, services are refined, and their methods change. For a GraphQL schema, this means adding new fields, queries, and mutations. In this case, the schema formally changes. Therefore, it is important that the fields that have not changed retain their configuration in the authorization rules. And new or changed fields would require new authorization settings.

SecurityProxy, having read the new schema, sends its hash to Grants. Grants, having discovered that it already has authorization rules for this service, but only for the previous version of the schema (a different hash in the database), informs SecurityProxy about this. It passes the new schema to Grants, which transfers unchanged rules from the old schema. Accordingly, new queries and mutations will require configuration from the administration utility.

For example, a new query family was added to the Clients service. After launching the service with the new schema, this query will not be available. Suppose it does not require protection by a specific right. Then the administrator can set the authorization type Authenticate for such a query. After the change and notification of SecurityProxy, this query will be allowed upon successful authentication (the token has been verified).

If any field has changed (backward incompatibility), then the previous rule for this field becomes irrelevant and a new setting by the admin will be required.

Notifying SecurityProxy of changes

The approach in which the business service notifies a common center of its existence is called Service Discovery. In this case, its proxy, SecurityProxy, acts on behalf of the business service. The central security service is Grants. SecurityProxy notifies Grants of its existence, passes the schema (new or changed), and reads the authorization rules.

How can Grants notify SecurityProxy about changes in authorization rules? Options:

  1. The simplest: the administrator simply notifies the support service in the usual manner about the need to restart SecurityProxy. Upon subsequent startup, the service will re-read the information from Grants and start working according to the new rules. The disadvantage of this approach is the human factor. The advantage is reliability.

  2. Pooling. You can set up a periodic request from SecurityProxy to Grants to get fresh changes. Advantages are automation and reliability. The disadvantage is that the update will not be instantaneous, but will depend on the pooling timeout.

  3. Events. Notify about changes through some mechanism (Kafka, RabbitMQ, etc.). Advantages are relatively instantaneous changes. The disadvantage is another system = another point of failure.

  4. Events through Webhooks. SP registers in Grants, which in turn notifies the required SP instance via webhook. In this case, no new system is involved.

In practice, you can combine options.

Connecting REST

The described mechanism for the GraphQL service also works for REST services. The fundamental difference is that for GraphQL, the entry points are the fields of types, starting with the basic query and mutation, while for REST, these are operations (GET, POST, etc.) and endpoints. Therefore, authorization rules are set in accordance with REST entities.

In this case, the requirement for the REST service is support for the OpenAPI schema. There is a nuance here: if for GraphQL the presence of a schema is mandatory according to the standard, then for REST there is no such requirement. But to understand what resources the REST service provides, a schema is required. Probably, you can do without it by explicitly specifying operations and endpoints in the admin panel, but the risk of desynchronization with this approach is too high. Let's consider an example. The Client service provides requests:

GET /api/client/{id} — read client

POST /api/client/{id} — update client

SecurityProxy reads the OpenAPI schema with this information and passes it to Grants. The administrator assigns authorization rules for these requests: Authorize(CLIENTS.GetClient) and Authorize(CLIENTS.UpdateClient). SecurityProxy receives the rules and starts requiring specific permissions in the JWT token for these resources. The service is now protected.


As a result of all the described changes, we have eliminated the shortcomings present in the previous version of "Mycelium". Now its dynamic rights system allows working with standard GraphQL and OpenAPI schemas and is easy to maintain regardless of the origin of the protected services.

Comments