Information security is one of the most importart concerns facing an increasily connected world. And since APIs are the glue of the digital landscape, API security is more important than ever.
In this scenario, user identity and access management are core concepts to deal with. In API architectures, in particular with the emerging microservices approach, the real challenge is to enable a strong security and identity management support while keeping efficiency and reliability.
In a microservices environment, the services are scoped and deployed in multiple containers in a distributed setup, and the service interactions are frequent and remote, mostly over HTTP. In this distributed and stateless world, even the user identity has to be managed in a distributed and stateless way.
What we need here is a solution that allows reliable user identity management and authorization checking without additional overhead, and using the JSON Web Tokens (JWT for short) can be the answer.
Quoting from the jwt.io introduction, a JWT can be defined as follows:
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.
In a world where service-to-service communication security is a real concern, using a JSON Web Token offers many advantages:
I'll take the simple API application example of my previous article, Spring Boot, Jersey, and Swagger: Always Happy Together, as a starting point to show you how to use the Holon Platform to secure API operations using JWT. The Holon Platform JWT support relies on the jjwt library to encode, decode and verify the JWT tokens.
The source code of the updated example API application can be found on GitHub, in the jwt branch of the spring-boot-jaxrs-swagger-api-example repository: https://github.com/holon-platform/spring-boot-jaxrs-swagger-api-example/tree/jwt.
In a typical scenario, the JSON Web Tokens emission, interchange and consumption process involves at least three actors: the issuer, the protected resource and the client.
The issuer is in charge to issue a JSON Web Token as a response to a valid authentication request. This step can include the user account credentials validation.
The produced JSON Web Token can contain detailed user information, including for example user profile information and authorizations, in addition to system and validation related data, such as the issuer name and the expiration time. These information are called claims in the JWT terminology, and they constitute the JWT payload. In addition to the set of reserved claims, you can provide a number of custom claims to represent the user identity and account information.
The issuer role can be easily intepreted by a OAuth2 UAA (User Account and Authentication) server, which returns a JWT instead of an opaque and randomly generated token.
In this example, we'll simulate the JSON Web Token generation by a UAA server, using the Holon Platform authentication and authorization APIs from a JUnit test class. The Holon Platform authentication architecture relies on the Authentication interface to represent the authenticated principal (which can represent an user but also another entity). The Authentication
structure supports permissions to represent the authorization grants and generic parameters to specify the account details.
To build a JSON Web Token from an Authentication
, the JwtTokenBuilder class can be used. This builder will translate the Authentication
permissions and parameters into JWT claims.
The JWT token builder needs to know which kind of token to build (signed or not), which algorithm has to be used to sign the token and any additional reserved claim (such as the token unique id, the issuer name and the expiration time) to add to the token itself before encoding it. This information can be collected and provided to the builder using the JwtConfiguration interface and the Holon Platform Spring Boot support can be used to build a JwtConfiguration
using the application configuration properties.
To enable the Holon Platform JWT support, we just have to add the holon-auth-jwt
artifact dependency to the dependecies section of our project's pom
:
<!-- Holon JWT support -->
<dependency>
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-auth-jwt</artifactId>
</dependency>
Now we can use a set of Spring Boot configuration properties, with the holon.jwt
prefix, to auto-configure a JwtConfiguration
instance which will be made available as a Spring component. The list of all the configuration properties is available here.
In this example, we want to setup the issuer name to be used, the expiration time and the signature algorithm to use. For the sake of simplicity, in this example we'll use a symmetric signing algorithm (HMAC with SHA-256), so we only need a single secret key, which has to be shared between the parties.
So you want to modify the application.yml
configuration file adding the following configuration properties:
holon:
jwt:
signature-algorithm: HS256
sharedkey-base64: X7Cukx67arP4UQSjPBC8zEkpht8hc4FZZwiIJIz2Fr0=
expire-hours: 1
issuer: example-issuer
The expire-hours
property is used to specify an expiration time of one hour, but other configuration properties are available to use a different unit of time (for example expire-minutes
, expire-seconds
, expire-ms
).
From now on, we can simply obtain the JwtConfiguration
which represents these configuration properties as an injected dependency.
So, let's see an example about a JSON Web Token generation from an Authentication
object (you can find more examples in the ApiTest unit test class of the source repository):
@Autowired
private JwtConfiguration jwtConfiguration;
@Test
public void jwtBuilder() {
Authentication authc = Authentication.builder("testUser") // principal name
// permissions
.withPermission("ROLE1").withPermission("ROLE2")
// user details
.withParameter("firstName", "Test")
.withParameter("lastName", "User")
.withParameter("email", "test@holon-platform.com")
// build the Authentication
.build();
// Build the JSON Web Token using the provided configuration and Authentication
String jwtToken = JwtTokenBuilder.buildJwtToken(jwtConfiguration, authc,
UUID.randomUUID().toString() // Token id claim (jti)
);
}
The JWT produced in the example above will look like this:
eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MDg3Njc1NzAsImp0aSI6ImNkYzNmMmQ3LWY5MWEtNGExZi1iY2Y0LTM1MGM3OWM2Y2FiYyIsInN1YiI6InRlc3RVc2VyIiwiaXNzIjoiZXhhbXBsZS1pc3N1ZXIiLCJleHAiOjE1MDg3NzExNzAsImZpcnN0TmFtZSI6IlRlc3QiLCJsYXN0TmFtZSI6IlVzZXIiLCJlbWFpbCI6InRlc3RAaG9sb24tcGxhdGZvcm0uY29tIiwiQVRIJHJvb3QiOmZhbHNlLCJBVEgkcHJtcyI6WyJST0xFMSIsIlJPTEUyIl19.4iXLnGHClfVPHF48Bz1aIyoOgsou8Usn2yXf4NUut_M
This token can be easily transmitted using the HTTP protocol, using for example an Authorization
header.
See this example for a simple JWT authorization server which issues JSON Web Tokens as the response of a POST invocation providing username and password using the HTTP Basic
scheme authorization header.
Now it's time to protected our API operations using JWT.
We'll leverage again on the Holon Platform authentication architecture, using the Realm security abstraction and the Holon Platform Spring Boot support to allow API resources access only to authenticated clients.
We'll use HTTP Authorization
header with the Bearer
scheme to obtain the JWT provided by the client. The JSON Web Token will be validated according to the JWT configuration properties (checking the sign, the issuer name and the expiration time), then the JWT claims can be used to obtain the required user identity information.
We want to implement a simple authorization control too, so the permission claims will be used to obtain the user roles and check if it was granted to access an API operation.
First of all, we have to setup the Holon Platform security Realm, specifying:
AuthenticationTokenResolver
to translate the JWT into an authentication token which can be interpreted by the Realm's authenticators.Authenticator
which the Realm has to use in order to perform the account authentication.Authorizer
, responsible for authorization control. The default Authorizer relies on the Authentication
permissions to check the user authorization against the required permissions/roles.
The Realm
is configured as a Spring component in the Application
class and will be automatically located and used by the Holon Platform authorization and authentication architecture:
@Bean
public Realm realm(JwtConfiguration jwtConfiguration) {
return Realm.builder()
// HTTP Bearer authorization schema resolver
.withResolver(AuthenticationToken.httpBearerResolver())
// JWT authenticator using the provided JwtConfiguration
.withAuthenticator(JwtAuthenticator.builder().configuration(jwtConfiguration).build())
// default authorizer
.withDefaultAuthorizer().build();
}
The JwtConfiguration
bean is automatically created using the application confguration properties as seen before.
Finally, we enable the API endpoint protection simply by using the Holon Platform @Authenticate
annotation on the JAX-RS resource class:
@Authenticate // require authentication
@Component
@Path("/api")
public class ApiEndpoint {
// content omitted
}
The Holon Platform JAX-RS module automatically setup Jersey enabling the following behaviour:
@Authenticate
is only accessible by a valid, JWT authenticated, client request. If the @Authenticate
annotation is declared at class level, all the JAX-RS class methods inherits the authentication setup. If the authentication is not present or not valid, a 401 - Unauthorized
status error response is returned.getUserPrincipal()
method. The Holon Platform translates back the JWT claims into the Authentication permissions and parameters, so they can be simply accessed from the JAX-RS method.javax.annotation.security.*
annotations (@PermitAll
, @DenyAll
, @RolesAllowed
) are enabled to perform role-based resource access control, using the Authentication permissions provided by the JWT. If authorization control is not successfull, a 403 - Forbidden
status error response is returned.Realm
component can be injected in the JAX-RS endpoint to leverage on the Realm
API operations using the current Authentication
.
For example, to declare that no role is required to access the original /ping
operation, the @PermitAll
annotation can be used:
@PermitAll // no specific role required
@GET
@Path("/ping")
@Produces(MediaType.TEXT_PLAIN)
public Response ping() {
return Response.ok("pong").build();
}
The ROLE2 authorization is required to access the following API operation, and the JAX-RS SecurityContext
is used (injected through the standard JAX-RS @Context
annotation) to obtain the authenticated principal name:
@RolesAllowed("ROLE2") // ROLE2 is required
@GET
@Path("/user")
@Produces(MediaType.TEXT_PLAIN)
public Response userOperation(@Context SecurityContext securityContext) {
// get the user principal name from the JAX-RS SecurityContext
String principalName = securityContext.getUserPrincipal().getName();
return Response.ok(principalName).build();
}
Finally, we add an API operation which uses the Authentication
obtained from the JAX-RS SecurityContext
to read the custom JWT claims ("firstName"
, "lastName"
, "email"
) and the Holon Platform Realm
for additional authorization control. The example response is the JSON representation of a UserDetails
example class:
@Inject
private Realm realm;
@RolesAllowed("ROLE2") // ROLE2 is required
@GET
@Path("/details")
@Produces(MediaType.APPLICATION_JSON)
public UserDetails userDetails(@Context SecurityContext securityContext) {
// the Holon platform Authentication is available as the JAX-RS SecurityContext user principal
Authentication auth = (Authentication) securityContext.getUserPrincipal();
// use Realm to perform additional authorization checks
boolean hasRole1 = realm.isPermitted(auth, "ROLE1");
UserDetails userDetails = new UserDetails();
userDetails.setUserId(auth.getName());
userDetails.setRole1(hasRole1);
// read the JWT claims, translated into Authentication parameters
auth.getParameter("firstName", String.class).ifPresent(p -> userDetails.setFirstName(p));
auth.getParameter("lastName", String.class).ifPresent(p -> userDetails.setLastName(p));
auth.getParameter("email", String.class).ifPresent(p -> userDetails.setEmail(p));
return userDetails;
}
The last actor is the client, which obtains the JSON Web Token from the issuer and uses it to perform the API calls, providing it as HTTP Authorization
header Bearer
token.
In a service-to-service communication scenario, the JWT obtained from the original API request can be passed around to perform additional inter-service calls. No additional overhead, such as querying the database, is needed to obtain the identity information, which is encapsulated in the JWT payload.
See the ApiTest class to learn how to use the Holon Platform RestClient to perform RESTful API invocations providing the JWT as Authorization
header Bearer
token.
In the previous post, we've seen how to use the Holon Platform Swagger support to create and provide a standard API documentation. Now we want to complete the API documentation, adding the authorization information. Swagger specification version 3 will be used in this example.
Let's suppose to use OAuth2 to obtain the JWT bearer token. In this example, the OAuth2 authorization server is available at the https://example.org/api/oauth2
URL.
Our authorization example roles (ROLE1
and ROLE2
) will be represented as OAuth2 scopes.
Using the standard Swagger annotations, we'll add API authorization definitions in the JAX-RS endpoint class this way:
@SecurityRequirement(name = "jwt-auth", scopes = "ROLE1")
@RolesAllowed("ROLE1")
// other annotations omitted
public Response protectedOperation() {
return Response.ok("protected").build();
}
Thanks to the Holon platform Swagger auto-configuration, the API documentation can be obtained in JSON format from the URL:
http://hostname:9999/api/docs
Using the Swagger Editor to display the API documentation, it will appear like this:
The API authorization scheme can be inspected clicking on the lock icons, for example:
We've seen how JWT can be a lightweight and versatile alternative to other traditional authentication systems, mostly in the stateless API and microservices world, and how the Holon Platform can make its implementation simple and reliable.
The source code of this example API application can be found on GitHub, in the jwt branch of the spring-boot-jaxrs-swagger-api-example repository: https://github.com/holon-platform/spring-boot-jaxrs-swagger-api-example/tree/jwt.
See the previous post to learn how to create the API example application using Spring Boot, Jersey and the Holon Platform JAX-RS module.