API Authorization using Aserto and Zuplo
API Authorization
Consistent API management is emerging as one of the most important functions of a Platform Engineering team. With projects like Backstage, these teams have a way to expose and document their API estate.
Authorizing access to APIs is the next step in this journey. With Aserto’s API Authorization solution, onboarding new services is as simple as importing their OpenAPI definition and assigning users or groups various levels of access to those services, or to individual endpoints.
Enforcing authorization using an API gateway
Since Platform Engineering teams don’t control the code to every API, they often look for common control points. An API gateway is the perfect enforcement point for an authorization policy, since it can filter requests before they are forwarded to the service, thereby requiring zero changes to the API code.
Zuplo is a modern API gateway company, and we now have a first-class integration between Aserto and Zuplo. Check out this webinar, where Josh Swift and I discuss the partnership, and demonstrate how easy it is to add fine-grained authorization to your APIs.
Or if you'd like to try it out for yourself, follow this guide to use Aserto and Zuplo together to secure your APIs.
A step-by-step guide
For expediency, this walkthrough uses the Aserto hosted authorizer as the policy decision point that is called by the API gateway. In a production environment, you’d change the authorizer service endpoint to a Topaz instance that is running in your cluster, and connect that Topaz instance to the Aserto control plane. Everything else is basically the same.
With that said, let’s jump in!
Instantiate the API Authorization template
Go to the Aserto Console, and if you don’t have an account yet, sign up for a free account. Once you’ve verified it, pick an account name, and select the API Authorization template.
If you already have an Aserto account, you can install the API Authorization template here.
This results in the creation of a new policy instance named api-auth.
Your tenant now has the API Authorization model, which includes the user, group, service, and endpoint object types, and the relations and permissions on these types.
Finally, the template also loaded some sample users and groups, modeled after the Rick and Morty cartoon, as well as three sample services - Petstore API, Rick and Morty API, and Todo API.
Import an OpenAPI definition
Click on the api-auth policy instance, and select the Quickstart tab. The Citadel identity provider that contains the Rick and Morty users is already connected. In the next step, you can optionally import your own OpenAPI spec. If you have one ready, try it out… otherwise feel free to skip this step and rely on the existing three services that we’re using as sample data.
The OPA policy
Click on the Modules tab, and observe a single Rego module - the boilerplate policy-rebac.check module.
Compare that with the complexity of having to manage a custom policy for every service! As mentioned, we are transforming a policy problem into a data modeling problem, which we will explore next.
But it’s important to note that any additional attribute-based access control or environment-oriented access restrictions can be easily added to this boilerplate policy. For example, you can easily extend the policy to ensure that users who are “contractors” are only allowed to invoke endpoints on weekdays.
Since every OPA policy is a Topaz policy, you can bring the full power of OPA and Rego to bear on your custom authorization policies.
The API Authorization model
Click the Directory tab, and the Edit manifest button. This will show you the API Authorization model definition. The important type definitions are for service and endpoint.
types:
# user represents a user that can be granted role(s)
user:
relations:
manager: user
permissions:
### display_name: user#in_management_chain ###
in_management_chain: manager | manager->in_management_chain
# group represents a collection of users and/or (nested) groups
group:
relations:
member: user | group#member
# identity represents a collection of identities for users
identity:
relations:
identifier: user
# service represents a set of endpoints
service:
relations:
owner: user
deleter: user | group#member
creator: user | group#member
writer: user | group#member
reader: user | group#member
permissions:
can_get: reader | can_put
can_put: writer | can_post
can_patch: writer | can_post
can_post: creator | can_delete
can_delete: deleter | owner
# endpoint represents a specific API endpoint
endpoint:
relations:
# each endpoint picks the reader/writer/creator/deleter relation to the service
# based on the method (GET -> reader, PUT/PATCH -> writer, etc)
service-reader: service
service-writer: service
service-creator: service
service-deleter: service
# invoker allows a user or group to get access to invoke this specific endpoint
invoker: user | group#member
permissions:
can_invoke: invoker | service-reader->can_get | service-writer->can_put |
service-creator->can_post | service-deleter->can_delete
Let’s start at the bottom. An endpoint has a can_invoke permission, which is directly assignable by creating an invoker relationship to a user or a group. This allows an API administrator to entitle a user or a group directly on a discrete endpoint.
The endpoint also has relations called service-reader, service-writer, service-creator, and service-deleter which ladder up to the enclosing service. The default transformation that occurs when importing an OpenAPI definition is to set the relationship of the endpoint based on its HTTP method - a GET creates the service-reader relation, a PUT or PATCH creates the service-writer relation, a POST uses the service-creator relation, and a DELETE uses the service-deleter relation. This allows the can_invoke permission to ALSO be assignable via relationships that a user or group has to the service. You can of course customize this default transform if you have different conventions or needs.
Now, let’s look at the service type. A service has discrete permissions called can_get, can_put, can_patch, can_post, and can_delete which are assignable through the reader, writer, creator, deleter, and owner relations on the service. These permissions are additive, in the sense that a deleter can invoke DELETE endpoints, and can also do anything that a creator can do. A creator can POST, and can also do anything that a writer can do… and so on.
To put it all together, users (or groups) can be entitled at the level of an entire service, at the level of a class of endpoints on a service (e.g. all GET endpoints), or at the level of a discrete endpoint. This provides a lot of flexibility in API entitlement, while keeping things simple and consistent.
Next, let’s look at the user, group, service, and endpoint instance data.
Authorization data
Click on the Objects tab, and within that the Service type. You should see the three services that were automatically added by the template.
Let’s follow the trail of entitlements from users and groups to the services and endpoints. Click on the User type, which should show the five Citadel users - Beth, Jerry, Morty, Rick, and Summer.
Let’s click on Rick. As you can see, Rick is a member of the Global Deleters group.
Next, click on the Global Deleters group.
This group aggregates the deleters group for each service. This pattern makes Rick a super-user - he can invoke any endpoint in the system, since the members of the Petstore API Deleters, Rick and Morty API Deleters, and Todo API Deleters groups can invoke any endpoint on the respective services, and being a member of the Global Deleters group means that Rick is transitively a member of these groups.
Next, let’s look at Morty - click the User type and then click Morty. Morty is a member of the Petstore API Creators group, which means he can invoke any Petstore endpoint that is not a DELETE. This demonstrates the pattern of how to entitle a user on a set of methods within a service.
Finally, let’s go back to the Service type and click the Todo List API. This shows the six endpoints that are part of this API.
The group called Todo List API Readers is a reader of the service, meaning every member is entitled to invoke all the GET endpoints on this service. Click on the Todo List API Readers, and follow the trail of nested groups. As you can see, it includes the viewer-group, which comes from the Citadel IDP.
Clicking the viewer-group reveals its members - Beth and Jerry, as well as the members of the editor-group. Clicking the editor-group reveals its members - Morty and Summer, as well as members of the admin-group. And the admin-group includes Rick. So, transitively, every one of our protagonists are entitled to invoke any GET endpoint on the Todo API Service.
This pattern shows how to use nested groups in the IDP to control entitlements to classes of endpoints (in this case, the GET endpoints) in a service.
Integrating authorization with an API gateway
Let’s use Zuplo as an example of a modern API gateway. Create a free account, and use their Todo sample template. This should result in something like this:
Add inbound policies to the routes
Next, click the routes.oas.json file on the left navbar, and click the GET /v1/todos endpoint. Open the Policies chevron.
Add a new policy called API Key Authentication:
Add another below it using the policy type Aserto Authorization.
You can call the Aserto Authorization policy aserto-authz-inbound, and configure it using the values shown below.
{
"export": "AsertoAuthZInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"tenantId": "tenant-id",
"authorizerApiKey": "authorizer-api-key",
"policyName": "api-auth",
"serviceName": "todo"
"userSubPropertyPath": ".data.email"
}
}
Note that the values for tenantId and authorizerApiKey should come from the Settings tab of the api-auth policy instance in the Aserto Console.
You can repeat this process for all six routes, but after you’ve done it once, you can just select the policies you’ve already selected from the top of the policy selector (in other words, you only have to create and configure the aserto-authz-inbound once).
Now you can save your work by pressing command-S.
Set up API keys
Lastly, you’ll need to set up API Key consumers. Click the Services tab and the API Key Service.
Create a consumer for each of Rick and Morty:
Ensure that the metadata for Rick is the following:
{
"sub": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs",
"email": "rick@the-citadel.com"
}
The metadata for Morty has a slightly different sub claim and Morty's email:
{
"sub": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs",
"email": "morty@the-citadel.com"
}
Now we have the two users set up, which we can use in the Authorization header of the requests that we send the gateway. The API key inbound policy will check that the user has a valid API key, and make it available in the aserto-authz-inbound policy. This will be the subject that the Aserto policy will pass to the Aserto hosted authorization service, along with the tenant ID, API key, policy name, and service name that we configured above. The subject passed into the Aserto authorizer will come from the .data.email
field that we placed in the API key metadata.
Make sure you copy the API keys associated with Rick and with Morty by using the "copy handles" next to each one - we’ll use them in the next section.
Testing the APIs
We can finally test our APIs using Zuplo’s Test modal. Go back to the routes.oas.json file in the left navbar, click the DELETE /v1/todos/{todoId} route, and click the Test button.
Now, fill in an arbitrary value for the todo ID, and enter the Authorization header and its value:
In the screenshot you’ll see that we also put in some dummy headers - AuthorizationRick and AuthorizationMorty, just so we can copy-paste these headers (and their Bearer tokens) into the Authorization header value. This is a convenient way to flip between invoking the service as Rick and as Morty.
First, let’s try as Rick. Copy and paste the value of the AuthorizationRick header into the Authorization header value. Then click the Test button. You should see an HTTP 200 OK status, and the logs should show that Aserto returned the resulting allowed decision as true. This is as expected, since Rick is a superuser and can invoke all endpoints by virtue of being a Global Deleter.
Next, paste Morty’s bearer token into the Authorization header, and click Test. You should see an HTTP 403 Forbidden status, as expected, since Morty is a member of the viewer-group, which can only invoke the GET APIs on the Todo API service.
Break the glass
Finally, let’s simulate a “break the glass” scenario where Morty needs access to the DELETE /v1/todos/{todoId} endpoint.
Go to the Aserto Console, click the Directory tab, and the Endpoint type. Type “delete” in the Find input and click the todo:DELETE:/v1/todos/{todoId} endpoint.
Click the Outgoing relations tab, and the invoker relation:
Click the Add a relation button and select User for the type and Morty for the instance. Click Add relation and you should see Morty as a direct assignee of the invoker relation.
Go back to the Zuplo console, and click the Test button again. You should now see an HTTP 200 OK status code, indicating that Morty is now able to invoke the DELETE /v1/todos/{todoId} endpoint. If you go back to the Aserto Console and delete the relation you just added, Morty will once again receive an HTTP 403 Forbidden status.
Governance
Before we wrap up, it’s worth noting that with the ReBAC model, we can now trivially find out which users are able to invoke which endpoints.
Go back to the Aserto Console, click on the Directory tab, and the Evaluator. Select the request called “Find objects that a user can access”, and select Beth, the Endpoint type, and the can_invoke permission.
Clicking the “Play” button invokes the query, and shows that Beth can only invoke the three GET endpoints on the Todo API service.
This makes sense because she’s a member of the viewer-group, and doesn’t have any additional entitlements. Play around with other users such as Morty and Rick to get different results, and feel free to copy the request as a cURL and execute it from a terminal to see how to invoke this type of request as a REST call.
curl 'https://directory.prod.aserto.com/api/v3/directory/graph/endpoint/can_invoke/user?object_id=&subject_id=beth%40the-smiths.com' \
-H 'aserto-tenant-id: <your-tenant-id>' \
-H 'authorization: basic <your-dir-api-key>' \
-H 'content-type: application/json'
Lastly, we can ask the question in the opposite direction - which users can invoke an endpoint? Select the “Find users that can access an object” request, select the first endpoint (DELETE /v1/todos/{todoId}), and select the can_invoke permission. You should see only a single user - Rick - that is entitled to invoke that endpoint.
Summary
This tutorial covered a lot of ground - the API Authorization model, importing an OpenAPI spec, entitling users on services and endpoints, calling Aserto / Topaz from an API gateway, and answering governance questions.
This is only the tip of the iceberg, but should hopefully show the potential of setting up a scalable way to perform fine-grained API authorization for all your services, enforced at the API gateway.
Aserto has a CLI toolchain that enables organizations to easily onboard a new service from its OpenAPI spec, creating the possibility of a CI/CD pipeline that easily adds new services (or handles adding new endpoints) in an automated fashion.
Entitling users to endpoints can be done by adding users to IDP groups (which are imported into Aserto), and break-the-glass scenarios can be achieved in the Aserto Console, through the aserto/topaz CLI, through the REST or gRPC APIs, or one of our language SDKs.
We hope you enjoyed this tutorial. We have a longer-form video version here, and if you have any questions, feel free to contact us or join our community slack.
Happy hacking!
Related Content
Gateway-enforced API Authorization
Learn how platform engineering teams can enforce service, method, and endpoint-level API access in a scalable way, without changing application code.
Jul 20th, 2024
An “easy button” for API Authorization
Scaling a fine-grained authorization model for APIs can be tricky, especially when you have hundreds or thousands of them. Fortunately, Topaz makes it easy!
Jul 8th, 2024