API Authorization using Open Policy Agent and Kong
On this page
Applying fine-grained authorization to APIs is a prevalent need, and there are several ways to implement it. This article will highlight several components in a separation of concerns architecture to create a full end-to-end approach. Each ingredient used in this architecture is either open-source or free to use as a community edition software.
A helpful overview of this setup is outlined in the Introducing the Neo-Security Architecture article. The Entitlement Management System article also describes the role a system like Open Policy Agent (OPA) plays in the overall architecture.
The scenario outlined in this article is leveraging an open-source plugin described in the Integrating with Kong Open Source article. This plugin is specialized in that it only handles the Phantom Token approach, which is very straightforward to configure. As such, the scenario below will use this pattern.
OPA will be used to externalize the authorization of access to the API using a policy-based approach. Kong will call OPA with an access request, and OPA will evaluate that request against its policy. A decision, typically something like {"allow": <true/false>}
will be returned by OPA, and Kong will use that information to allow or deny the call to the upstream API.
OPA uses claims in its policy to make that decision. The Curity Identity Server issues the claims as part of providing a token. Scopes and Claims are fully configurable in the Curity Identity Server, and the claims data can come from anywhere.
This article will describe the use of the above components and a combination of Scopes and Claims to achieve a multi-tier API authorization model where a coarse-grained authorization decision is made. If it passes, a fine-grained authorization decision is handled by OPA and its policy.
Prerequisites
All components used will be running in Docker using a docker compose file. The only prerequisite is to have Docker Desktop installed.
High-Level Architecture
-
A client needs access to data exposed by an API. This client could be any client, but for testing purposes, OAuth Tools could be used. The first step is to obtain an access token from the Curity Identity Server.
-
The opaque access token is passed in the
Authorization
header to the API endpoint exposed by the Kong Gateway. -
Kong receives the opaque access token, and
a) sends it to the Introspection endpoint of the Curity Identity Server.
b) Then, the Curity Identity Server issues a JWT.
-
Using the Phantom Token plugin, Kong will perform a coarse-grained authorization check using the scopes received in the JWT. If scopes configured as required in the plugin are not available, access will be denied, and a
HTTP 403
returned to the client.a) If the coarse-grained check is approved, the Kong OPA plugin will take over and pass the decoded and verified JWT to OPA together with the request method and requested API path. OPA holds a policy that will be evaluated against these input attributes and will make an authorization decision.
b) The decision is returned to Kong.
-
If authorization is allowed, Kong will call the upstream API and add the JWT in the Authorization header. Otherwise, an
HTTP 403
is returned to the client.
Quickstart
The full environment is available as a GitHub repository and can be started very easily.
- Pull down the git repo
git clone https://github.com/curityio/curity-kong-opa-demo
. - Build the environment
docker compose build
. - Start the environment
docker compose up
. - Add the following entry to your
/etc/hosts
file, so that you're able to correctly call the containers from your local machine:
127.0.0.1 opa-kong-tutorial-idsvr opa-kong-tutorial-kong opa-kong-tutorial-opa
- When the environment has started, go to
https://opa-kong-tutorial-idsvr:6749/admin
and log in with the user admin and password defined indocker-compose.yml
. Go through the basic wizard and make sure to enable SSL (Use Existing SSL Key
and selectingdefault-admin-ssl-key
works, or choose your own). Upload a valid license and upload the example policy,curity/curity-opa-kong-config.xml
. This policy can be merged but requires the wizard to be completed and committed first. - With the system configured, a client can obtain a token using the
www
client. Make sure to request theopenid
andrecords
scope. E.g., you can call the authorization endpoint with this request sent from a browser:
https://opa-kong-tutorial-idsvr:8443/oauth/v2/oauth-authorize?client_id=www&scope=openid%20records&response_type=code&redirect_uri=https://localhost:8080/cb
There are no users pre-populated in the environment. As part of the authentication process, create a user. The default OPA policy checks that user==owner
so authorization will fail if there is a mismatch. The owners (patient) of the records are detailed in api/server/data/records.json
. Either create a user that matches or make changes to records.json
.
Once you receive the authorization code, you can redeem it with a curl command:
curl -k --basic -u www:Password1 -d grant_type=authorization_code&redirect_uri=https://localhost:8080/cb&code=... https://opa-kong-tutorial-idsvr:8443/oauth/v2/oauth-token
- Use the access token and perform a GET request to the API exposed by Kong.
curl -Ss -X GET \http://opa-kong-tutorial-kong:8000/records/0 \-H 'Authorization: Bearer b37b14c7-a23b-4c4b-b59a-4f4bac9ba9af'
Alice owns record 0 and access will be granted. However, she does not own record 1. Therefore, access to this record will be denied.
Detailed Environment Configuration
The entire environment is composed of four docker containers.
1. The API
This is a simple Golang application exposing an API that serves up records. Naturally, this is not required, but using the provided API with its default data will match the configuration provided for the Curity Identity Server and Kong as well as the authorization policy enforced by OPA.
The data exposed by the API can be modified in api/server/data/records.json
. The data is read at every request to the API and is mapped via a volume mapping in docker-compose.yml
. With this mapping, the data can be changed on-the-fly without rebuilding or even restarting the container.
It would also be possible to change the data model of the records. In that case, api/server/data-model.go
must be modified and the API image rebuilt.
The API image is built as part of the docker compose build
command.
2. The Curity Identity Server
In this architecture, the Curity Identity Server operates as the token service responsible for issuing opaque tokens after a successful user authentication. The opaque token is then introspected by the Kong API Gateway using the phantom token plugin to effectively obtain a JWT to replace the opaque token.
The provided configuration is set to use an HTML Form Authenticator that uses an internally provided database as the credential store. This could be changed, and any supported Authenticator could be used. The configuration has two scopes configured: openid
and records
. This is to showcase that the Phantom Token plugin can be used for coarse-grained Authorization directly in the Kong Gateway and thus avoiding unnecessary calls to both OPA and the upstream API if the configured scopes are not available.
When OPA receives the Authorization request from the Kong Gateway, it will contain the JWT in the payload. The OPA policy includes several elements that use claims in the JWT itself. More details on this in the next section that describes the policy.
3. Open Policy Agent
The policy is the full policy in use by OPA when the environment starts. It's loaded from opa/policies/
at the start by the OPA container. opa/policies
only contains one policy file, records.rego
. However, it would be possible to add additional policy files to this directory that would be loaded when the container starts.
Calling http://opa-kong-tutorial-opa:8181/v1/policies
would show what policies are in place.
It is also possible to change the policy in place in the OPA runtime environment by making a PUT
request to http://opa-kong-tutorial-opa:8181/v1/policies/records/records.rego
.
OPA policies are written in rego, an easy-to-understand declarative policy language. Policies can be very complex and powerful, and this example is only touching on the surface of what's possible. However, it should offer an idea of what's possible related to protecting data serviced by an API.
This policy is expressed in such a way that exemplifies several features of the policy language.
First, a default decision is set with default allow = false
. This is not explicitly needed but clarifies that the default response means access is not allowed.
The allow {}
rule has pointers to other rules in the policy. All of these rules are going to result in a true
or false
result. Everything inside of the allow {}
rule is AND:ed
, meaning they all have to be true
for allow
to be true
. If they are not all true
, the decision will be false since that is the default value for allow {}
.
is_issuer
checks if the iss
claim in the token in the request (input.token.payload.iss
) to OPA contains an allowed value. The permitted value is defined in the issuers
variable in the policy.
With that in mind, several of the other rules are self-explanatory. The is_get
rule checks if the API call is a GET
request, and match_claims
checks that the scopes in the token are openid records
(the Phantom Token plugin in Kong also does this, so this should never be false).
One complex rule that applies a more fine-grained authorization approach is the is_owner
rule. The simplified version of this rule would be user==owner
. It checks that the record requested access for is owned by the user requesting it. In this case, the user is identified by a claim in the token, input.token.payload.sub
. The value is normalized to be all lower case using the lower()
function of rego. Then the owner of the record is determined. This is done by first parsing out the record_id
from the end of the path from the authorization request. For http://opa-kong-tutorial-kong:8000/records/0
, the record_id
is 0
. The record_id
is then passed to a defined function in the policy that calls the upstream API, gets the full content of that record, and parses out the patient
(e.g., the owner). That value is then also normalized to lower case. If the two values user
and patient
match, the result of this rule is true
.
Policy
package recordsdefault allow = falseallow {is_issueris_getis_recordsis_audmatch_claimsis_owner}## Allowed issuer(s)issuers = {"https://opa-kong-tutorial-idsvr:8443/oauth/v2/oauth-anonymous"}is_issuer {input.token.payload.iss == issuers[issuer]}is_get {input.method == "GET"}is_records {startswith(input.path, "/api/records")}is_aud {input.token.payload.aud = "www"}match_claims {input.token.payload.scope = "openid records"}## Calls record_owner method to verify if the request record is owned by the requesting user (sub)is_owner{record_id := trim_left(input.path, "/api/records/") ##trim the path to get record idlower(input.token.payload.sub) == lower(record_owner(record_id).patient)}## Method takes a record_id as input to resolve record datarecord_owner(record_id) = http.send({"url": concat("", ["http://opa-kong-tutorial-api:8080/api/records/", record_id]),"method": "GET","force_cache": true,"force_cache_duration_seconds": 86400 # Cache response for 24 hours}).body
4. Kong Gateway
The Kong API Gateway sits as a proxy in front of the API. In this environment, the Kong Gateway has two main responsibilities. The first is to execute the Phantom Token flow using the phantom token plugin and with that obtain a JWT. The second is to pass that JWT to the Open Policy Agent (OPA) for further authorization. If the response from OPA is successful, e.g., an allow=true
is returned in the result, Kong will continue the flow and call the upstream API to return the data.
With this flow in the Kong Gateway, the plugins must execute in the correct order. The Phantom Token plugin has a priority set to 1000 and OPA 899 by default. The higher the number, the earlier they are executed. So, in this case, the Phantom Token plugin will obtain a JWT first so that OPA can consume it.
The Kong Gateway image used is built as part of the docker compose build
command. This enables the two plugins to be bundled inside the image. In addition to that, the db-less mode of Kong is used, and a declarative configuration kong.yml
is made available through a volume in docker-compose.yml
. The kong.yml
configuration sets up the service for the API and configures the two plugins accordingly. This includes configuring the introspection endpoint, client and secret, and the policy path needed for the policy evaluation by OPA.
Conclusion
The Curity Identity Server, Open Policy Agent, and Kong Gateway all play their part in a robust architecture to provide fine-grained authorization for APIs. An identity server that leverages standards like OpenID Connect and OAuth2 is well suited to provide solid authentication and issue tokens with scopes and claims. An API Gateway like Kong can then consume this for coarse-grained authorization and further exchange of tokens to provide richer data that can hold PII information leveraged later in the chain. The API Gateway can pass that data on to an external authorization such as OPA that holds a fine-grained authorization policy capable of making an extremely detailed authorization decision. It makes a decision based on claims or attributes and shares the result with the API gateway, allowing or denying the upstream API to be called.
These components are great at what they do and can be configured to work together to provide strong security for APIs.
Resources
- GitHub repo of this environment
- The Phantom Token plugin
- The Kong OPA plugin
- OPA documentation
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial