How the Open Policy Agent (OPA) and the Curity Identity Server can work together to provide strong user authentication combined with policy-based authorization.

Open Policy Agent: Integration Overview

On this page

Open Policy Agent (OPA), an open-source authorization engine, has become increasingly popular to apply fine-grained authorization to microservices and APIs. This article will detail some of the ways the Curity Identity Server and OPA complement each other to provide strong user authentication combined with policy-based authorization.

What is Open Policy Agent (OPA)?

Open Policy Agent is a general-purpose authorization engine that leverages policies expressed in Rego.

Open Policy Agent: Integration Overview

The purpose of this article is not to explain how OPA works or how Rego policies are expressed in detail. Yet, it helps to have a high-level understanding of how the input, data, policy, and output all fit together.

The input is basically the request that is sent to OPA. It's encoded in JSON and could look like this example:

json
123
{
"message": "world"
}

The above is a very simplified example, and the structure of the input could be much more complex.

As mentioned, the policy language is Rego. It's also JSON-encoded, and a simple policy may look like the example below. Much more advanced functions are also possible using the Rego policy language.

json
12345
default hello = false
hello {
input.message == "world"
}

The policy example compares the value of input.mesage to the static string "world". If the comparison is true, the policy named hello is evaluated as true. The resulting response from the example input and policy is:

json
123
{
"hello": true
}

Naturally, since the default for the hello policy is defined as false, if the input message is anything other than "world", the response would instead be "hello": false.

More complex policies could result in additional responses. Here is an example that defines two policies:

json
12345678910
default hello = false
default foo = false
hello {
input.message == "world"
}
foo {
input.message2 == "bar"
}

With an input request that passes in two data entries:

json
1234
{
"message": "world",
"message2": "bar"
}

The result is:

json
1234
{
"foo": true,
"hello": true
}

There's a very handy Rego Playground you can use to define and test OPA use cases and policies.

The User Context & Access Tokens

By combining an OAuth and OpenID Connect (OIDC) solution with an Authorization solution like OPA, you get virtually no overlap in the responsibilities. OAuth and OIDC have robust capabilities in performing user authentication. On the other hand, OPA does not perform authentication but must know who the end-user is (or the calling system) to make a useful authorization decision.

The Curity Identity Server has many authentication options available out of the box and more options available as open-source in GitHub. This, coupled with Authentication Actions, allows for a strong user authentication where an entire workflow can be executed as part of the authentication process itself.

After all user authentication workflows are completed, the Token Service takes over. In a simplistic view, the Token Service will issue one or more tokens. The number and type of tokens used will depend on the flow. At a minimum, an Access Token is issued, but it could also be a refresh and ID token.

Claims and Scopes

Though this article will not cover specific details about Claims and Scopes, there are plenty of resources on those topics. The Claims and Scopes section is a great place to start.

In general, the Token Service issues tokens. It will issue Claims and populate the token according to what's configured as part of that process. Using the Curity Identity Server, you can fully customize the claims and scopes and set where the claims data is populated from. With this flexibility, it's possible to leverage any external system as the claims data provider. There are several options for this out-of-the-box in the Curity Identity Server, and it's also possible to create custom Claims Providers using the Curity Plug-in SDK. One such example is this Salesforce Claims Provider.

OAuth client configurations denote what Scopes a given client provides. When an application initiates a flow with that client, it can request scopes. A scope is a logical grouping of a set of claims. When a client requests consent for a given scope, the claims grouped by that scope are added to the token. What token the claims are added to depends on the configuration, as the Curity Identity Server has options for controlling this.

As an example, consider a client that can request consent for the records scope. This might provide two claims in the resulting token, records_read and records_write. Naturally, it's possible a user may want the records_write claim but would not be given that claim based on other policies and configurations.

In addition to claims provided as a result of a client requesting consent for one or more scopes, the issued token also contains additional standard claims depending on the flow and type of token. The below example of an Access Token's decoded payload shows the token expiration time (exp), the token issuer (iss), and other details.

json
123456789101112
{
jti P$7fcf74a1-41d8-4f5c-a544-58910be13119
delegationId 2d598af3-d90f-45af-8d01-cc3e536d0850
exp 1617912600
nbf 1617912300
scope group records
iss https://idsvr.example.com/oauth/v2/oauth-anonymous
sub alice
aud www
iat 1617912300
purpose access_token
}

From OPA's perspective, Claims are essentially attributes that can be used as input to making a fine-grained authorization decision. Next, let's consider how OPA handles Scopes, Claims, and other attributes.

Consuming The Access Token

OPA takes a request input consumed in the OPA engine and evaluated against the policy in place. An input is a JSON payload, but it can be very flexible in its structure. Here's a simple example:

json
123456789
{
"input": {
"path": "/records",
"method": "GET",
"user": "alice",
"department": "sales"
...
}
}

In the above example, a set of attributes are passed as input to the OPA policy engine. These attributes must originate from somewhere — for example, by the application calling OPA. This will work in some cases but not others — it depends on the use case and the scenario.

When an application is leveraging a token-based architecture and handles authentication through an OAuth/OIDC server, an Access Token and possibly also an ID Token will be available. This aligns exceptionally well with the capabilities of OPA. Instead of having the application potentially connect to several sources to resolve attribute values for various purposes, the client application can simply pass a token to OPA in conjunction with other parameters required for authorization.

OPA has built-in capabilities to decode and validate a JWT. This allows a JWT to be passed as part of the input. The above example input payload could instead look like this (truncated JWT for readability):

json
1234567
{
"input": {
"path": "/records",
"method": "GET",
"jwt": "eyJraWQi..."
}
}

For now, this is not a huge change, at least not visibly. However, an important distinction here is that the Access Token (in this case a JWT) passed in the input contains claims asserted by a Claims Authority. The iss claim mentioned earlier notes who the Claims Authority is. This means that the token consumer, OPA in this case, can validate and thereby trust the claims presented in the token. With this setup, there's more trust that the claims are valid and appropriately issued if compared to the application itself looking up values from different data sources. In addition, the token (with the claims) can be centrally revoked and thereby deemed untrusted resulting in limited or no access.

The above JWT (eyJraWQi...) can be decoded, and the resulting payload looks like this:

json
1234567891011121314
{
jti d46c8156-31c8-42dc-9d84-7076272efe46
delegationId 9ea639bd-97dd-4fe7-9ea7-948d2109547b
exp 1614871539
nbf 1614871239
scope openid records
iss http://localhost:8443/oauth/v2/oauth-anonymous
sub 302d6ae338229ef75d5403fc229c0a9c8c5ca1dbba12a2f1ef9e2d94e0b2e3cd
aud opa-demo
iat 1614871239
purpose access_token
records_write false
records_read true
}

After decoding and validating the JWT, OPA can consume the claims and leverage these when evaluating the policy for a fine-grained access decision.

Of course, you must balance providing enough information in the token without leaking critical information — especially PII data to a client that can't hold a secret. From this perspective, it might not be desirable to issue a token that contains all this data and especially not PII data if operating in a regulated environment.

Phantom or Split Token

Instead of issuing a JWT, the Token Service could issue what's called an opaque or by-reference token. This is basically a GUID passed around as a reference that the consumer can look up or introspect to get more information. The response from introspection can be a JWT that can be passed to OPA, just as described above. This approach fits nicely when the Curity Identity Server and OPA are deployed in conjunction with an API Gateway.

Two patterns that can be used for this approach are the Phantom or Split token pattern. In the Phantom Token approach, the gateway can introspect an incoming opaque token and, in return, receive a JWT. Similarly, the Split token approach would result in the gateway holding the different pieces for a JWT. In which case, the gateway could assemble them. In both cases, OPA will receive a JWT as part of the input request.

Having the gateway enforce the authorization decision is a pervasive architectural approach. In such a scenario, the API gateway can also optionally cache the JWT for optimized performance. However, it should be noted that caching the token puts strain on securing the API gateway cache since it potentially holds a token with PII information.

Conclusion

The Curity Identity Server is a robust complement to a fine-grained Authorization solution such as Open Policy Agent (OPA). OPA handles authorization decisions based on an externalized authorization policy expressed in the feature-rich Rego policy language. Attribute data is used in conjunction with the policy to determine if access should be allowed or not. The attributes used as input in the OPA engine can be claims present in an access token. The token can be decoded and validated directly using functions within the OPA policy. When the token is decoded, the claims within the token are readily available for OPA to leverage as attributes.

Resources

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