Sunday, March 15. 2020
JASPI authentication in elytron
Today's entry is dedicated to Java Authentication Service Provider Interface for Containers (JASPIC or just JASPI) in the wildfly server. Since version 15 the new elytron security provider allows JASPI configuration for applications. This post is going to explain how to move from the old security configuration (which also provided JASPI) to the new one.
JASPI or JASPIC is a standard JavaEE API to authenticate users into the container. In general a ServerAuthModule should be implemented and the method validateRequest gets the user data from the request and validates it against the users and roles repository (LDAP, database,...). For the entry I am going to use a silly example. My module is just a BASIC authentication, it reads the header Authorization to retrieve the username and password, and validates the data against a pre-configured list of users. In JASPI the initialize method receives a map of options, and that map will be used to pass the list of usernames, passwords and roles managed by my sample. Each map entry will have the following format:
"username" => "password[###role1,role2,...]"
It is just a toy module but enough to exemplify how to use it in the old and the new security system. Here you can download the jaspi-module maven project which generates the final library (jaspi-module-1.0.jar). Let's start the configuration in wildfly 18.
The first step is adding the module into the server.
module add --name=es.rickyepoderi.jaspi --resources=/path/to/jaspi-module-1.0.jar --dependencies=javax.servlet.api,javax.security.auth.message.api
Now configure the JASPI in a security domain.
/subsystem=security/security-domain=jaspi-module:add /subsystem=security/security-domain=jaspi-module/authentication=jaspi:add(auth-modules=[{code=es.rickyepoderi.jaspi.ExampleServerAuthModule, module=es.rickyepoderi.jaspi, flag=required, module-options={"user1" => "password1###user,role1", "user2" => "password2###user", "user3" => "password3"}}])
The security domain called jaspi-module adds my ServerAuthModule and initializes it with three users (user1, user2 and user3), and the first two are assigned to the role user while the third one is not.
Now it is the time to deploy an application. A hello-world-jaxrs app was developed that is just a hello world but secured with the previous module. The hello endpoint says hello to the logged user. For that, the standard web.xml is used.
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <security-constraint> <web-resource-collection> <web-resource-name>All resources</web-resource-name> <description>Protects all resources</description> <url-pattern>/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>user</role-name> </auth-constraint> <user-data-constraint> <transport-guarantee>NONE</transport-guarantee> </user-data-constraint> </security-constraint> <security-role> <description>Role required to log in to the Application</description> <role-name>user</role-name> </security-role> <login-config> <auth-method>BASIC</auth-method> </login-config> </web-app>
All the pages are protected and requires the user role (the one assigned to the first two users but not to user3). And finally the custom jboss-web.xml uses the jaspi-module security domain configured in the previous point.
<jboss-web> <security-domain>jaspi-module</security-domain> </jboss-web>
The application is deployed inside wildfly and can be tested using curl: the hello endpoint is not allowed for anonymous users; user1 can access and receives the hello; but user3 is forbidden (this user lacks the valid role).
curl http://localhost:8080/hello-world-jaxrs/api/hello <html><head><title>Error</title></head><body>Unauthorized</body></html> curl --anyauth --user user1:password1 http://localhost:8080/hello-world-jaxrs/api/hello Hello user1! curl --anyauth --user user3:password3 http://localhost:8080/hello-world-jaxrs/api/hello <html><head><title>Error</title></head><body>Forbidden</body></html>
So it is working. The old configuration for JASPI in wildfly is simple. A security domain was the only way of using this standard until wildfly 15.
Time to use the new JASPI elytron way. The old security domain is removed and a new jaspi-configuration is needed inside the elytron section.
/subsystem=security/security-domain=jaspi-module:remove /subsystem=elytron/jaspi-configuration=simple-configuration:add(layer=HttpServlet, application-context="default-host /hello-world-jaxrs", server-auth-modules=[{class-name=es.rickyepoderi.jaspi.ExampleServerAuthModule, flag=REQUIRED, module=es.rickyepoderi.jaspi, options={"user1" => "password1###user,role1", "user2" => "password2###user", "user3" => "password3"}}])
The new elytron configuration assigns JASPI to the application context, but the rest is more or less the same than in the old way (class and module, and the same configuration to define our three users).
Then JASPI needs a security domain to be created for the web layer undertow.
/subsystem=undertow/application-security-domain=jaspi-module:add(security-domain=ApplicationDomain, enable-jaspi=true, integrated-jaspi=false)
The domain uses the default ApplicationDomain, but please note that the option integrated-jaspi is set to false. This means that the JASPI module is enough, an ad hoc identity will be created independently of the domain realm. The default value true uses the realm in the ApplicationDomain to locate user and roles, so, besides the JASPI authentication, it also requires the user to be present in the realm.
Finally the same curl commands of step 4 can be executed. They should return the same output.
That is all for today. The new elytron security provides the old fashioned JASPI authentication since wildfly 15. The idea is more or less the same than in the old sub-system and, in general, the steps to configure it are very similar. Both configurations work, so, if you are using JASPI for your applications, better start using the new elytron way. The two little projects used in the entry (the JASPI module and the hello application) can be downloaded using the links provided before.
Best regards!
Saturday, February 29. 2020
Testing Spring Boot (Part II)
In the previous entry a simple Spring Boot application was presented. The main goal in the PoC was using Spring Boot but trying to limit the development to JavaEE technologies (servlet and jax-rs) plus some common security and web service libraries (keycloak to secure the endpoints and swagger to document them). The first part left some steps still missing, I wanted to check how easy was moving from one implementation to another (from the default tomcat servlet container to undertow, and from jersey to resteasy jax-rs implementation) and, finally, run the same application inside a wildfly server (check war and jar deployment). This second part of the series deals with those two missing points.
Moving from tomcat to undertow
Spring Boot by default uses tomcat as the embedded web servlet container. So, in the first part, tomcat was used because nothing was modified and the default dependencies were in place. In order to include undertow the tomcat dependency should be excluded from the jersey starter and the undertow one should be added. So now the application only used the following two spring starters.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
And it works, the startup logs show that undertow is used at port 8080. So moving from tomcat to undertow is perfect, just switching the starters, undertow is added and tomcat is excluded. Really nice!
Moving from jersey to resteasy
Now let's change the jax-rs implementation from the oracle/eclipse implementation (jersey) to the JBoss one (resteasy). As both are standard jax-rs implementations the change should have been transparent but, as it was commented in the previous entry, the jersey starter is using the private ResourceConfig configuration class. So that will not work for sure with the new implementation. First the jersey started should be substituted with the reasteasy one (jersey was commented out). Only the resteasy and the undertow starter remains and, inside the former, tomcat should also be excluded to maintain undertow working.
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-spring-boot-starter</artifactId>
<version>4.4.0.Final</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
Direct compilation doesn't work, because the ResourceConfig is not available in the resteasy implementation. Now the standard Application class should be used. In this case the jersey starter is doing the weird thing, the standard way should also be allowed, but it is not. We need a little change in the code, only in the application definition.
package es.rickyepoderi.springboot.rest;
import io.swagger.v3.jaxrs2.integration.JaxrsOpenApiContextBuilder;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import io.swagger.v3.oas.integration.OpenApiConfigurationException;
import io.swagger.v3.oas.integration.SwaggerConfiguration;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletConfig;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
//import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@ApplicationPath("api")
@Component
public class RestApplication extends Application { // extends Application { //extends ResourceConfig {
@Context
private ServletConfig servletConfig;
// it doesn't work with standard Application, why?
//https://stackoverflow.com/questions/47237180/how-to-register-resource-with-javax-ws-rs-core-applicaiton-in-spring-boot-starte
public RestApplication() {
//register(HelloEndpoint.class);
//register(OpenApiResource.class);
//register(RolesAuthorizationFilter.class);
try {
new JaxrsOpenApiContextBuilder()
.servletConfig(servletConfig)
.application(this)
.openApiConfiguration(new SwaggerConfiguration()
.openAPI(new OpenAPI()
.info(new Info()
.title("Hello App")
.description("Sample Spring Boot Hello App")
.version("0.0.1-SNAPSHOT")))
.prettyPrint(true)
.resourcePackages(Collections.singleton(HelloEndpoint.class.getPackage().getName())))
.buildContext(true);
} catch (OpenApiConfigurationException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<>();
classes.add(HelloEndpoint.class);
classes.add(OpenApiResource.class);
classes.add(RolesAuthorizationFilter.class);
return classes;
}
}
The Application class is used and the resources to integrate are returned in the getClasses method (instead of the register that jersey uses). The jersey configuration was maintained as comments to change between both implementations in my tests. After this change the application can be compiled and everything else works, our little app is running with undertow and resteasy.
Moving to a wildfly server
The last step was moving from the jar bundle that has been used until now to a war deployment inside wildfly. The first change is switching the packaging type in the pom.xml.
<packaging>war</packaging>
If the application is just generated like this the war is very big, with tons of libraries in the WEB-INF/lib directory that are not needed (they are already available inside the wildfly server). A lot of time was spent checking the maven dependencies (mvn dependency:tree -Dverbose and so on and so forth), playing with exclusions and scopes (provided scope is very useful here). I finished with the following pom.xml. Some points to comment:
The resteasy-spring-boot-starter and spring-boot-starter-undertow are defined as provided in order to have them for compilation and avoid the inclusion of their libraries. But, in spite of that, a lot of artifacts should be explicitly excluded in both starters.
As the previous two starters are defined as provided, the default spring-boot-starter-web is added (this way default spring libraries are incorporated to the war). Again a lot of exclusions are listed in order to remove duplicated or not needed artifacts.
The dependency reactive-streams is also needed in the war bundle. It seems that at deploy time wildfly checks for CDI annotations (that some spring classes seem to have) and processing them several NoClassDefFoundError exceptions are thrown (org/reactivestreams/Subscriber or org/reactivestreams/Processor).
<dependency> <groupId>org.reactivestreams</groupId> <artifactId>reactive-streams</artifactId> <version>1.0.3</version> </dependency>
The keycloak filter depends in bouncy castle and http-client, those libraries are inside wildfly but are private modules (they are not exposed by default to applications). I decided to add a dependency for both in the MANIFEST file and exclude those two artifacts.
<plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.3</version> <configuration> <archive> <manifestEntries> <Dependencies>org.apache.httpcomponents.core,org.bouncycastle</Dependencies> </manifestEntries> </archive> </configuration> </plugin>
Finally, with the final pom.xml, the resulting war file is about 14MB in size (the initial file was more than 30MB!). I suppose that you can strip it even more. But the summary is that a detailed review of the dependencies is needed to change from the standalone jar to the war bundle. This part is painful and necessary to create a correct war for wildfly with a decent size.
Video
The same steps done in the previous video are shown but in a different order. This time the war is deployed inside a wildfly 18. First the bundle is generated and deployed in the server. Then the context is accessed and keycloak intercepts it and requests the user to login. Same as before, user ricky can execute the endpoint but admin cannot. After logout the simple servlet in the application continues working, the endpoint returns 401 if not logged in and swagger keeps giving a correct definition for it. So everything is working again, but this time inside the wildfly JavaEE container.
In my humble opinion Spring Boot gives a good support for interchanging implementations (tomcat or undertow, jersey or resteasy, standalone or container deployment). There are several snags, jersey using a private initialization class (this issue is something very specific to this starter) or the mess with dependencies when deploying inside the container. But I think it is almost impossible to do it seamlessly, some minor changes are going to be needed for sure. I like the Spring Boot idea. That is all for today. The final project deployed inside wildfly can be downloaded from here.
Regards!
Sunday, February 16. 2020
Testing Spring Boot (Part I)
Today's entry is about Spring Boot. If you follow the blog you know that I am more engaged to JavaEE, but I wanted to test these new technologies and I decided to start with Spring Boot (maybe the most known framework of this type). I prefer standards (JavaEE) over other technologies (Spring) just because I think this is the way that things should be, but better if I start working with them. In my humble opinion the idea around Spring Boot is nice, particularly how the application is bundle in a single jar with all the needed components in it, really perfect to deploy in a container. So my idea with this series is the following:
Testing Spring Boot but try to limit its functionality to JavaEE specification (mainly servlets and jax-rs, which are the typical frameworks I use to create backend applications). I am going to restrict Spring usage to the minimum.
Including swagger and keycloak as external libraries (other libraries commonly used in my projects).
The app should work in the common Spring Boot configuration (Tomcat and Jersey) and using JBoss implementations (undertow and resteasy).
The app should work like standalone jar executable and container war bundle (inside Wildfly).
The goal is taking advantage of the Spring Boot packaging but developing with common JavaEE technologies. Trying to obtain the best of the two worlds. The last points are just to see how easy is combining those different scenarios (jar and war packaging, changing implementations,...). Please remember this is just my idea, and, of course, there will be other people that prefer the opposite solution (use the Spring framework exclusively).
Adding a Servlet
Let's start with a simple Servlet. Using jax-rs is the final goal, but I prefer to start with the basics. So using the Spring initializer a initial pom.xml was created.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/>
</parent>
<groupId>es.rickyepoderi.springboot</groupId>
<artifactId>springboot-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-sample</name>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
And then a simple Hello World servlet is created (The HelloBean is just a POJO used to say hello, it has the greeting and the name properties).
package es.rickyepoderi.springboot.servlet;
import es.rickyepoderi.springboot.HelloBean;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
try (PrintWriter writer = resp.getWriter()) {
writer.write(new HelloBean(req.getParameter("greeting"), req.getParameter("name")).toString());
}
}
}
In order to make it work with Spring the application is created with an annotation @ServletComponentScan defining the servlets (and other entities of the specification like filters or listeners) that should be initialized.
package es.rickyepoderi.springboot;
import es.rickyepoderi.springboot.servlet.HelloServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan(basePackageClasses = {HelloServlet.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
And it works. You can compile it and make it run with two simple commands:
mvn clean package
java -jar target/springboot-sample-0.0.1-SNAPSHOT.jar
And the Servlet is displayed in the http://localhost:8080/hello URL.
Adding Jax-RS
Now that the simple servlet is working let's move and add the restful web services. The idea is using plain jax-rs, so initially the jersey (jakarta/Oracle implementation) starter is incorporated to the pom.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
The first little drawback was that jersey integration is done using the ResourceConfig class (private class in the Jersey implementation) instead of the standard Application class. This will complicate the change to resteasy unnecessarily. But, it is really easy to use it. The REST application should also be tagged as a @Component to initialize it inside Spring.
package es.rickyepoderi.springboot.rest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Context;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@ApplicationPath("api")
@Component
public class RestApplication extends ResourceConfig {
public RestApplication() {
register(HelloEndpoint.class);
}
}
And finally the REST endpoint is developed. Another Hello World example.
package es.rickyepoderi.springboot.rest;
import es.rickyepoderi.springboot.HelloBean;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
@Path("/hello")
public class HelloEndpoint {
@GET
@Produces("application/json")
public HelloBean sayHello(@QueryParam("greeting") String greeting, @QueryParam("name") String name) {
return new HelloBean(greeting, name);
}
}
And again it works. Just compile and run the application and the rest endpoint is available in http://localhost:8080/api/hello.
Adding Swagger
Now it is time to add swagger to annotate my endpoint. First add the dependency.
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.1.1</version>
</dependency>
The framework will be added like in a common web application, so the previous RestApplication registers the swagger OpenApiResource and initialize the API to include the packages that should be scanned for endpoints.
public RestApplication() {
register(HelloEndpoint.class);
register(OpenApiResource.class);
try {
new JaxrsOpenApiContextBuilder()
.servletConfig(servletConfig)
.application(this)
.openApiConfiguration(new SwaggerConfiguration()
.openAPI(new OpenAPI()
.info(new Info()
.title("Hello App")
.description("Sample Spring Boot Hello App")
.version("0.0.1-SNAPSHOT")))
.prettyPrint(true)
.resourcePackages(Collections.singleton(HelloEndpoint.class.getPackage().getName())))
.buildContext(true);
} catch (OpenApiConfigurationException e) {
throw new RuntimeException(e);
}
}
And now it is turn to annotate all the endpoints and methods. In my case just the sayHello operation.
@GET
@Produces("application/json")
@Operation(summary = "Say Hello",
tags = {"hello"},
description = "Say hello to the given username and greeting",
responses = {
@ApiResponse(description = "The hello bean", content = @Content(schema = @Schema(implementation = HelloBean.class)))
})
public HelloBean sayHello(
@Parameter(description = "The user name") @QueryParam("greeting") String greeting,
@Parameter(description = "The greeting to use") @QueryParam("name") String name) {
return new HelloBean(greeting, name);
}
Finally the swagger-ui.html is added and (I suspect that) it is done in an unusual way for Spring Boot, the needed files were just included in the webapp directory (I just obtained the dist directory from the project and installed in the app directory, the index.html was modified to obtain the hello/openapi.json definition and renamed to swagger-ui.html). And again, everything continues working correctly.
Adding keycloak
The next step is adding keycloak to the restful endpoint. The servlet-filter adapter is going to be used, although there is a Spring and a Spring Boot adapter, the filter adapter is standard and it also works in common web applications. First add the dependency.
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
<version>8.0.2</version>
</dependency>
Then a new filter was created extending the default servlet filter.
package es.rickyepoderi.springboot.keycloak;
import javax.servlet.annotation.WebFilter;
import org.keycloak.adapters.servlet.KeycloakOIDCFilter;
@WebFilter(urlPatterns = {"/api/hello", "/keycloak/*"})
public class KeycloakFilter extends KeycloakOIDCFilter {
public KeycloakFilter() {
super(new ClasspathKeycloakConfigResolver());
}
}
The filter is applied to the /api/hello endpoint and the internal /keycloak/* and a configuration resolver is used to obtain the keycloak.json from the classpath.
package es.rickyepoderi.springboot.keycloak;
import java.io.InputStream;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.spi.HttpFacade;
public class ClasspathKeycloakConfigResolver implements KeycloakConfigResolver {
private final KeycloakDeployment deployment;
public ClasspathKeycloakConfigResolver() {
InputStream is = getClass().getResourceAsStream("/keycloak.json");
deployment = KeycloakDeploymentBuilder.build(is);
}
@Override
public KeycloakDeployment resolve(HttpFacade.Request req) {
return deployment;
}
}
And, as it is a common filter, it should also be added to the @ServletComponentScan annotation to be integrated as another servlet class.
@SpringBootApplication
@ServletComponentScan(basePackageClasses = {HelloServlet.class, KeycloakFilter.class})
public class Application {
Finally a index.html was added to be used as a public application with keycloak (I did the trick of using the same client as the public and the bearer-only application, ideally there should be two applications, but the same can be used). And again everything works.
RBAC security
The last thing I wanted to add is some security based on roles. Usually in JavaEE the restful endpoints are annotated as a @Stateless local EJB and RBAC security is easily added to them (@PermitAll, @DenyAll, @RolesAllowed annotations). But enterprise beans is a technology I do not like a lot, the security trick only uses local and stateless and a full JavaeEE application server like wildfly gives that framework out of the box, so it is very easy to use. Nevertheless a similar technique can be achieved using a jax-rs ContainerRequestFilter (a filter that can be added to the rest web services). This entry explains this idea in detail. So the following filter is added.
package es.rickyepoderi.springboot.rest;
import java.io.IOException;
import java.lang.reflect.Method;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
public class RolesAuthorizationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Context
private HttpServletRequest httpServletRequest;
private void notAllowed(ContainerRequestContext ctx) {
ctx.abortWith(Response.status(Response.Status.FORBIDDEN).build());
}
private void checkRoles(ContainerRequestContext ctx, String[] rolesAllowed) {
if (rolesAllowed != null) {
for (String role : rolesAllowed) {
if (httpServletRequest.isUserInRole(role)) {
// user is allowed
return;
}
}
}
// user is not of allowed roles => deny
notAllowed(ctx);
}
@Override
public void filter(ContainerRequestContext ctx) throws IOException {
// look for method/class annotations to allow access
Method method = resourceInfo.getResourceMethod();
Class clazz = method.getDeclaringClass();
if (method.isAnnotationPresent(DenyAll.class)) {
// deny at method level
notAllowed(ctx);
} else if (method.isAnnotationPresent(PermitAll.class)) {
// allowed at method level => ok
} else if (method.isAnnotationPresent(RolesAllowed.class)) {
// check roles allowed
checkRoles(ctx, ((RolesAllowed) method.getAnnotation(RolesAllowed.class)).value());
} else if (clazz.isAnnotationPresent(DenyAll.class)) {
// deny at class level
notAllowed(ctx);
} else if (clazz.isAnnotationPresent(PermitAll.class)) {
// allowed at class level => ok
} else if (clazz.isAnnotationPresent(RolesAllowed.class)) {
// check roles allowed in the class
checkRoles(ctx, ((RolesAllowed) clazz.getAnnotation(RolesAllowed.class)).value());
}
// no annotation is like PermitAll => allowed
}
}
This way the endpoint can be annotated (at class and/or method level) to define what users can execute the endpoints. For example in my little hello example the class is annotated to only be executed by the Users role.
@Path("/hello")
@RolesAllowed("Users")
public class HelloEndpoint {
The RolesAuthorizationFilter should also be registered in the RestApplication to be incorporated to the request execution.
Video
And that is all. The application is a typical rest backend application which is annotated using swagger and secured with keycloak and RBAC. The only difference is that this time the app uses Spring Boot. The video shows first the servlet. Then the endpoint is accessed but it is not allowed because it requires authentication. Finally the index page is requested, the javascript keycloak adapter asks for login, and sending the bearer token the user ricky can execute the method (this user belongs to Users role) but admin receives a forbidden response (roles are working as expected). Finally you can see the swagger-ui which reads OK the defining annotations.
As a little summary, this entry presents a modern JavaEE application (rest web services, swagger, OIDC and RBAC) but using Spring Boot instead of a common container. The application is almost a JavaEE app but deployed with Spring Boot. The maven project can be downloaded from here. In a second entry the application will be moved to JBoss implementations (undertow and resteasy) and finally the application will be deployed inside wildfly (instead the full jar bundle). Let's see how complicated that second part is.
Regards!
Comments