This project studies the securing of the REST API of a Spring Boot resource server with Open Policy Agent (OPA) as authorization server and Keycloak as authentication server.
Be sure to check out the corresponding Medium article: Externalize your API Protection!
Although authentication and authorization can be combined (see for example Keycloak's authorization services), there is a recent trend to separate them. Another trend is to move the evaluation of authorization rules outside the resource server. A complete setup for protecting a REST API therefore consists of three services and a REST client:
The workflow for authenticating and authorizing a REST request consists of the following steps:
- On startup, the resource server retrieves the JWK Set needed for JWT validation from Keycloak.
- The REST client (curl) obtains a JWT for the user from Keycloak using the OAuth 2 password grant.
- The REST client accesses the REST API, passing the JWT as
Bearer
token. - The resource server validates the JWT and extracts the required information (e.g. user roles).
- Using data from the JWT and the request, the resource server builds an input document for OPA.
- The resource server queries OPA, passing the just built input document as JSON POST body.
- Given the query path and input data, OPA evaluates the corresponding rules and comes to a decision.
- OPA returns the decision as JSON document.
- The resource server extracts the decision from the result and grants or denies the access to the endpoint.
The figure also shows that there is no technical dependency between Keycloak and OPA. Also (and fortunately), OPA does not need to know anything about the users managed by Keycloak. Keycloak passes all required user information in (custom) JWT claims, especially the user's roles and group memberships.
Assume that the resource server manages information about sports teams. A user wants to modify details of team 123. After authentication with Keycloak, the API client obtains a JWT which, among other information, contains the following claims (step 2):
{
"roles": ["analyst"],
"groups": ["Team123"]
}
The JWT is passed as Bearer
JWT to the resource server (step 3):
PUT http://localhost:8090/teams/123
The resource server builds the following input JSON for OPA (steps 4 and 5)
{
"input": {
"method": "PUT",
"path": "/teams/123",
"roles": ["analyst"],
"groups": ["Team123"]
}
}
and sends it to OPA as POST
body of the following request (step 6):
http://localhost:8181/v1/data/demo/allow
The rule set evaluated by OPA in step 7 is:
package demo
import future.keywords.if
import future.keywords.in
# The default value is used when all rules with the same name are undefined
default allow := false
# Members of a team have read+write access to the data of (only) that team
allow if {
input.method in {"GET", "PUT"}
teamId := trim_prefix(input.path, "/teams/")
teamName := concat("", ["Team", teamId])
teamName in input.groups
}
# Users with "api-read" rights can read the data of all endpoints
allow if {
input.method == "GET"
"api-read" in input.roles
}
# Users with "api-full" rights have full access to all endpoints
allow if {
"api-full" in input.roles
}
Because the "team membership" rule matches, OPA returns the following JSON result (step 8):
{
"result": true
}
This lets the resource server grant access to resource /teams/123
(step 9).
You need local installations of
- Docker Compose
- Curl
docker compose up
This starts Keycloak (with a predefined realm), OPA (with a rule set), and the resource server.
Folder scripts contains a number of shell scripts that first obtain a JWT from Keycloak and then query the REST API of the resource server. Example calls:
sh scripts/get-teams.sh
sh scripts/post-team.sh
For development and testing, the OPA service can be used separately. It has an "eval" mode that allows the testing of rule sets (written in OPA's Rego language):
docker run --rm -v $(pwd)/rego:/rego openpolicyagent/opa eval -i /rego/demo-input.json -d /rego/demo-rules.rego data.demo.allow
OPA can also run as server:
docker run --rm -v ${PWD}/rego:/rego -p 8181:8181 openpolicyagent/opa run --server --addr :8181 /rego/demo-rules.rego
It can then be queried with a REST call, just as the resource server does:
curl -H 'Content-Type: application/json' localhost:8181/v1/data/demo/allow -d @rego/demo-body.json