Reputation: 980
Jersey 2.22 API with token authentication + role based authorization (the way I secured the API is based on the accepted answer from this post: Best practice for REST token-based authentication with JAX-RS and Jersey . It might be better to read it before trying to understand my question) :
here is my web.xml:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- LISTENERS -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>JerseySpringServlet</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>ca.toto.api.filters</param-value>
</init-param>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>ca.toto.api.restapi</param-value>
</init-param>
<init-param>
<param-name>jersey.config.server.provider.classnames</param-name>
<param-value>ca.toto.api.filters.AuthenticationFilter;ca.toto.api.filters.AuthorizationFilter;com.toto.api.restapi.TaskRestService</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>JerseySpringServlet</servlet-name>
<url-pattern>/filters/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>JerseySpringServlet</servlet-name>
<url-pattern>/restapi/*</url-pattern>
</servlet-mapping>
When I make a call to my tasks web service, the flow goes in the first filter (AuthenticationFilter) without any problem (@Priority(Priorities.AUTHENTICATION)), validates my token, gets the user from the decoded token then registers it as the Principal then passes in the second filter, AuthorizationFilter (@Priority(Priorities.AUTHORIZATION)) where I get the user from the security context, get his role then check if he has permission to make the call. If yes, exit the filter normally if no, use javax.ws.rs.container.ContainerRequestContext.abortWith method to send a response with status 403:
@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {
...
try {
boolean isAllowed = false;
// Check if the user is allowed to execute the method
// The method annotations override the class annotations
if (methodRoles.isEmpty()) {
logger.info("Checking permissions on CLASS level");
isAllowed = checkPermissions(classRoles);
} else {
logger.info("Checking permissions on METHOD level");
isAllowed = checkPermissions(methodRoles);
}
// Throw an Exception if the user has not permission to execute the method
if(isAllowed == false) {
logger.warn("USER IS NOT ALLOWED TO COMPLETE THE REQUEST. ABORT.");
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build());
}
} catch (Exception e) {
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build());
}
When the user has the right role, the service is called and I get the correct response with correct info. My problem is that when my variable isAllowed equals to false, I get a 404 instead of a 403 and I can't figure out why...
here is my TaskRestService service definition:
@Path("/tasks")
@Secured({RoleEnum.admin})
public class TaskRestService {
...
@GET
@Produces(MediaType.APPLICATION_JSON)
@Transactional(readOnly = true)
public List<Task> getTasks(@QueryParam("code") String code) {
... }
Upvotes: 2
Views: 2494
Reputation: 209112
You should set this Jersey init-param jersey.config.server.response.setStatusOverSendError
to true
. Here's what it states in the Javadoc
Whenever response status is 4xx or 5xx it is possible to choose between
sendError
orsetStatus
on container specificResponse
implementation. E.g. on servlet container Jersey can callHttpServletResponse.setStatus(...)
orHttpServletResponse.sendError(...)
.Calling
sendError(...)
method usually resets entity, response headers and provide error page for specified status code (e.g. servlet error-page configuration). However if you want to post-process response (e.g. by servlet filter) the only way to do it is callingsetStatus(...)
on containerResponse
object.If property value is true the method
Response.setStatus(...)
is used over defaultResponse.sendError(...)
.Type of the property value is
boolean
. The default value isfalse
.
So what happens is the error cause the container to try and send you to an error page, and when there is non configured, you get a 404. So when you set the property to true
, it cause the use of setStatus
rather then sendError
NOTE: If you are not using a web.xml, for a ResourceConfig
, you can use the property(property, value)
method. For an Application
subclass, you can override Map<String, Object> getProperties()
public class MyApp extends ResourceConfig {
public MyApp() {
property(ServletProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, true);
}
}
public class MyApp extends Application {
@Override
public Map<String, Object> getProperties() {
Map<String, Object> props = new HashMap<>();
props.put("jersey.config.server.response.setStatusOverSendError", true);
return props;
}
}
Upvotes: 7