We present a Java library that simplifies adopting the Open Policy Agent server to manage user authorization for a Spring Boot microservice, while also managing API Token (JWT) authentication.
Motivation
Spring Security assumes a fairly simplistic Role-Based access control (RBAC) where the service authenticates the user (via some credentials, typically username/password) and returns a UserDetails
object which also lists the Authorities
that the Principal
has been granted.
While it is also possible to integrate Spring Security with JSON Web Tokens (JWT) this is also rather cumbersome, and lacks flexibility.
Finally, integrating the app with an Open Policy Agent server with the relatively new Spring Reactive (WebFlux
) model is far from straightforward.
Ultimately, however, Spring Security “collapses” authentication and authorization into a single process, based on the UserDetails
abstraction, which sometimes does not allow sufficient flexibility.
This library allows a Web application to:
- clearly separate authentication from authorization;
- easily adopt JWTs (API Tokens) as a means of authentication;
- simplify integration with OPA for authorization;
- keeping the authorization logic (embedded in Rego policies) separate from the business logic (carried out by the application).
It also provides a blueprint to inject OPA authorization in a Spring Reactive (WebFlux) application.
All the code described below can be found at: https://github.com/massenz/jwt-opa, and is released under the Apache 2 open source license.
Architecture
To acquire an API Token the client needs to access one of the “authenticated” endpoints (as defined in the routes.authenticated
list property – see the RoutesConfiguration
class) and obtain a valid JWT from the JwtTokenProvider
; an example of how to do this (using a simple Spring Data repository, backed by MongoDB) is in the /login
controller in the example app (LoginController
): the SecurityConfiguration
class is what one would implement in any Spring Application with Spring Security enabled:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {
@Bean
public ReactiveUserDetailsService userDetailsService(ReactiveUsersRepository repository) {
return username -> {
return repository.findByUsername(username)
.map(User::toUserDetails);
};
}
}
Obviously, instead of accessing a local database, the application could use a WebClient
to access a remote service to retrieve any details (including an encoded password).
Once the user has been authenticated, we can generate a JWT API Token, and return it to the client:
@GetMapping
Mono<JwtController.ApiToken> login(
@RequestHeader("Authorization") String credentials
) {
return usernameFromHeader(credentials)
.flatMap(repository::findByUsername) // See Note.
.map(u -> {
String token = provider.createToken(u.getUsername(), u.roles());
return new JwtController.ApiToken(u.getUsername(), u.roles(), token);
})
.doOnSuccess(apiToken ->
log.debug("User {} authenticated, API Token generated: {}",
apiToken.getUsername(), apiToken.getApiToken()));
}
NoteAs you may notice, we are duplicating the round-trip to the DB for the User
data; this may (or may not) be a performance issue, especially on performance-sensitive APIs: an obvious solution would be to use either a co-located cache, or even an in-memory one, with a relatively short TTL.
Authorization via Open Policy Agent server

Once the client has an API Token, it can be used to authorize any other request: this is done via the OpaReactiveAuthorizationManager
as a ReactiveAuthorizationManager
, chained after the JwtReactiveAuthorizationManager
which takes care of validating the API Token.
All of this is done transparently by the jwt-opa
library, without having to change anything in the actual application.
@Override
public Mono<AuthorizationDecision> check(
Mono<Authentication> authentication,
ServerHttpRequest request
) {
return authentication.map(auth -> {
return makeRequestBody(auth.getCredentials().toString(), request);
})
.flatMap(body -> client.post()
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body)
.exchange())
.flatMap(response -> response.bodyToMono(Map.class))
.map(res -> {
Object result = res.get("result");
if (StringUtils.isEmpty(result)) {
return Mono.error(unauthorized());
}
return result.toString();
})
.map(o -> Boolean.parseBoolean(o.toString()))
.map(AuthorizationDecision::new);
}
Simplified code excerpt, please see the OpaReactiveAuthorizationManager class for the full code
the client
is a Spring WebClient
configured to connect to the OPA Server as configured via the OpaServerConfiguration
configuration, which reads the following properties from application.yaml
:
opa:
server: "localhost:8181"
policy: kapsules
rule: allow
This will eventually send a TokenBasedAuthorizationRequestBody
(encoded as JSON) to the following endpoint:
http(s)://localhost:8181/v1/data/kapsules/allow
The allow
rule will eventually grant/deny access to the requested endpoint (given the HTTP Method and, optionally, the request’s body content).
See OPA Policies below, and the OPA Documentation for more details on Rego and the server REST API.
Authorizing Requests
As shown above, the check()
method sends a JSON body
to the OPA server, generated by:
makeRequestBody(auth.getCredentials().toString(), request);
this is a TokenBasedAuthorizationRequestBody.RequestBody
object which carries the path
and HTTP method
of the HTTP request, alongside the JWT, which can then be used by the OPA policy to evaluate the grant:
public class TokenBasedAuthorizationRequestBody {
// The OPA server requires every POST body to the Data API to be wrapped inside
// an "input"} object, we use this class to simplify the construction
// of the JSON body.
public static class RequestBody {
TokenBasedAuthorizationRequestBody input;
}
public static class Resource {
String path;
String method;
}
@JsonProperty("api_token")
String token;
Resource resource;
}
maps to something like:
{
"input": {
"resource": {
"path": "/users",
"method":"GET"
},
"api_token":"eyJ0eXAiOi...PCHQ"
}
}
The api_token
, once decoded inside the Rego policy (using the io.jwt.decode()
helper method), carries the following fields:
"token": [
{
"alg": "ES256",
"typ": "JWT"
},
{
"iss": "demo-issuer",
"roles": [
"SYSTEM"
],
"sub": "admin"
},
"e8b8bb5d5....37bf2a63c21d"
]
The user (or Principal
or Subject
) is in the "sub"
field, and the Issuer
("iss"
) is configured to be the authority generating the Token (see JWT Handbook for more details): the Token is decoded as an array, containing, in order, the header
, body
and signature
.
In client applications using the jwt-opa
library, tokens can be generated using the JwtTokenProvider
class.
Signing Keys
To sign the JWT we use Elliptic Cryptography
3; this a family of asymmetric encryption algorithms which require a private/public key pair.
Briefly, an “elliptic cryptography” key pair can be generated with:
- generate the EC param
openssl ecparam -name prime256v1 -genkey -noout -out ec-key.pem
- generate EC private key
openssl pkcs8 -topk8 -inform pem -in ec-key.pem -outform pem \ -nocrypt -out ec-key-1.pem
- generate EC public key
openssl ec -in ec-key-1.pem -pubout -out public.pem
In the repository there is a utility keygen.sh
script that will do this for you.
Save both keys in a private
folder (not under source control) and then point the relevant application configuration (application.yaml
) to them:
secrets:
keypair:
private: "private/ec-key-1.pem"
pub: "private/ec-key-pub.pem"
3See the excellent “JWT Handbook” by Auth0 for a primer on EC.
Web Server (Demo app)
This is a very simple Spring Boot application, to demonstrate how to integrate the jwt-opa
library; there is still some work to refine it, but by and large, it gives a good sense of what is required to integrate a Spring Reactive app with an OPA server:
- implement a
SecurityConfiguration
@Configuration
class; - implement a mechanism to retrieve
UserDetails
given ausername
; and - implement something similar to the
LoginController
to serve API Tokens to authenticated users.
In future releases of the jwt-opa
library we may also provide “default” implementations of some or all of the above, if this can be done without limiting too much client’s options; or maybe they could be provided in a jwt-opa-starter
extension library.
Trying out the demo
Supporting Services
The sample app (webapp-example
) requires the following services:
- Mongo (to store users); and
- OPA Policy Server.
Use the following to run the servers locally:
docker run --rm -d -p 27017:27017 --name mongodb mongo:4
docker run --rm -d -p 8181:8181 --name opa openpolicyagent/opa:0.25 run --server
See Docker Hub – OPA image for details and the most recent stable version to use.
After starting the server (./gradlew bootRun
), you will see in the log the generated password
for the admin
user:
2021-02-04 23:23:17 INFO 90860 ... Initializing DB with seed user (admin). Use the generated password: 342dfa7b-4
Note
The system user does not get re-created, if it already exists: if you lose the random password, you will need to manually delete it from Mongo directly:
docker exec -it mongo mongo
> show dbs;
...
opa-demo-db 0.000GB
> use opa-demo-db
> db.users.find()
{ "_id" : ObjectId("5ff8173b20953c451f10a384"), "username" : "admin", ...
> db.users.remove(ObjectId("5ff81..."))
and then restart the server to recreate the admin user.
Alternatively, just stop & restart the Mongo container (but all data will be lost).
To access the /login
endpoint, you will need to use Basic
authentication:
$ http :8080/login --auth admin:342dfa7b-4
this will generate a new API Token, that can then be used in subsequent HTTP API calls, with the Authorization
header:
$ http :8080/users Authorization:"Bearer ... JWT goes here ..."
(see the HTTPie project documentation to learn how to use the http
command; or just use curl
).
OPA Policies
See the OPA documentation and in particular the Rego Playground and REST API for more details on how to implement fine-grained authorization policies using the OPA Server.
Some examples are stored in src/main/rego
and can be uploaded to the OPA policy server via a POST
request, with the policy request in the body itself (text/plain
encoded); for example, the following extracts the roles
from the JWT and then allows any incoming request if the user has a SYSTEM
role:
default allow = false
token := t {
t := io.jwt.decode(input.api_token)
}
user := u {
some i
u = token[i].sub
}
roles := r {
some i
r = token[i].roles
}
# System administrators can modify all entities
is_sysadmin {
some i
roles[i] == "SYSTEM"
}
# System accounts are allowed to make all API calls.
allow {
is_sysadmin
}
Examples of policy evaluations are in src/test/policies_tests
as JSON files; they can be executed against the policy server using the /data
endpoint:
POST http://localhost:8181/v1/data/kapsules/valid_token
{
"input": {
"user": "myuser",
"role": "USER",
"token": "eyJ0eXAi....iCzY"
}
}
The OPA Policy Reference has more examples and details on built-in commands and utilities.
Complete code
The full code for both the jwt-opa
library and the example
webapp can be found on Github at: https://github.com/massenz/jwt-opa.
Feel free to submit ideas and suggestion via GitHub Issues, and contribute to the code via Pull Requests.
Leave a Reply