jakarta-eejerseyglassfish-3tokenjacc

Glassfish @RolesAllowed with custom SecurityContext


The question i'm about to ask is a bit tricky and I haven't found any answer yet. Maybe because i'm looking for the wrong thing. But i hope you will help me on this.

I used the following tutorial to implement a custom SecurityContext that uses tokens instead of basic user/password authentication.

Basically it initializes and injects a ResourceFilterFactory that will itself inject a ResourceFilter at every HTTP request sent to the application.

This ResourceFilter searches for the "Authentication" header in the request, take its content and then authenticates the user. If the user is authenticated it is injected into the request using a SecurityContext.

I transformed the code in order to make it work as an EJB.

Here's the code :

web.xml

<init-param>  
<param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>  
<param-value>com.myapp.rest.filter.ResourceFilterFactory</param-value>
</init-param>

ResourceFilterFactory.java // This class injects the EJB SecurityContextFilter

@Named
@Stateless
@LocalBean
public class ResourceFilterFactory extends RolesAllowedResourceFilterFactory{

    @EJB(name="securityContextFilter")
    private SecurityContextFilter securityContextFilter;

    @PostConstruct
    private void init(){
        System.out.println("ResourceFilterFactory initialized");
    }

     @Override
        public List<ResourceFilter> create(AbstractMethod am) {
         //System.out.println("Creating resource filters list");
            List<ResourceFilter> filters = super.create(am);
            if (filters == null) {
                filters = new ArrayList<ResourceFilter>();
            }

            List<ResourceFilter> securityFilters = new ArrayList<ResourceFilter>(filters);
            securityFilters.clear();
            securityFilters.add(0, securityContextFilter);

            return securityFilters;
     }
}

SecurityContextFilter.java // This EJB is called at every HTTP request the server receives. It checks for the "Authentication" header and gets the user associated with the token.

@Named("securityContextFilter")
@Stateless
@LocalBean
@Provider
public class SecurityContextFilter implements ResourceFilter, ContainerRequestFilter {

    protected static final String HEADER_AUTHORIZATION = "Authorization";

    @EJB
    private AuthorizationService authorizationService;

    @Override
    public ContainerRequest filter(ContainerRequest req) {
        System.out.println("Auth header: " + req.getHeaderValue(HEADER_AUTHORIZATION));
        String sessionToken =  req.getHeaderValue(HEADER_AUTHORIZATION);
        UserEntity entity = null;
        try {
            //entity = authorizationService.getParaUsingSessionToken(sessionToken);
            // removing the part that retrieves the user from the database
            ExternalUser user = new ExternalUser();
            user.setEmailAddress("lol@lol.lol");
            user.setFirstName("lol");
            user.setLastName("LOL");
            user.setRole("arole");
            req.setSecurityContext(new org.company.server.rest.filter.SecurityContextImpl(user));
        } catch (AuthenticationException e) {
            System.out.println("authentication exception");
            ExternalUser user = new ExternalUser();
            req.setSecurityContext(new org.company.server.rest.filter.SecurityContextImpl(user));
        }

        return req;
    }

    @Override
    public ContainerRequestFilter getRequestFilter() {
        return this;
    }

    @Override
    public ContainerResponseFilter getResponseFilter() {
        return null;
    }

}

SecurityContextImpl.java // This class is injected into the request and has the isUserInRole() method that should be used by @RolesAllowed annotation

public class SecurityContextImpl implements SecurityContext {


    private final ExternalUser user;

    public SecurityContextImpl(ExternalUser user) {
        //System.out.println("SecurityContext created : " + user.getFirstName());
        this.user = user;
    }

    public Principal getUserPrincipal() {
        return user;
    }

    public boolean isUserInRole(String role) {
        System.out.println("Checking access rights : " + role + " / " + this.user.getRole());
        return user.getRole().equalsIgnoreCase(role);
    }

    public boolean isSecure() {
        return false;
    }

    public String getAuthenticationScheme() {
        return SecurityContext.BASIC_AUTH;
    }
}

ExternalUser.java // The entity created and populated with database user information.

@XmlRootElement
public class ExternalUser implements Principal {

    private String id;
    private String firstName;
    private String lastName;
    private String emailAddress;
    private boolean isVerified;
    private String phoneNumber;
    private String professionalId;
    private String role;

    public ExternalUser() {}

    public ExternalUser(UserEntity user) {
        this.setEmailAddress(user.getEmailAddress());
        this.setFirstName(user.getFirstName());
        this.setLastName(user.getLastName());
        this.setRole(user.getRole().toString());
        this.setPhoneNumber(user.getPhoneNumber());
        this.setProfessionalId(user.getProfessionnalID());
    }
    // Getters and setters boilerplate code...
}

Finally the Jersey WebService :

@Path("/account")
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
@Stateless
@LocalBean
public class UserRestService {

@Context
private SecurityContext security;

        @GET
        @Path("info")
        @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
        public Response getInfo() {
        ExternalUser user = (ExternalUser)security.getUserPrincipal();
        System.out.println("Email Address Of User : " + user.getEmailAddress());
            if (!security.isUserInRole("arole")){
                return Response.status(403).build();
            }
        return Response.ok(user).build();
        }
}

The WebService works well, i get the ExternalUser instance using the injected SecurityContext. But if I use the @RolesAllowed({"arole"}) annotation, Glassfish gives me this error :

INFO: JACC Policy Provider:Failed Permission Check: context (" org.company.app.server/org_company_app_server_internal ") , permission (" ("javax.security.jacc.EJBMethodPermission" "UserRestService" "getInfo,Local,org.company.server.rest.models.authentication.RestSession") ") 
WARNING: EJB5184:A system exception occurred during an invocation on EJB UserRestService, method: public javax.ws.rs.core.Response org.company.server.rest.services.UserRestService.getInfo()
WARNING: javax.ejb.AccessLocalException: Client not authorized for this invocation
    at com.sun.ejb.containers.BaseContainer.preInvoke(BaseContainer.java:1888)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:212)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:88)
    at com.sun.proxy.$Proxy140.getInfoPara(Unknown Source)
    at org.company.server.rest.services.__EJB31_Generated__UserRestService__Intf____Bean__.getInfoPara(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.sun.jersey.spi.container.JavaMethodInvokerFactory$1.invoke(JavaMethodInvokerFactory.java:60)
    at com.sun.jersey.server.impl.model.method.dispatch.AbstractResourceMethodDispatchProvider$ResponseOutInvoker._dispatch(AbstractResourceMethodDispatchProvider.java:205)
    at com.sun.jersey.server.impl.model.method.dispatch.ResourceJavaMethodDispatcher.dispatch(ResourceJavaMethodDispatcher.java:75)
    at com.sun.jersey.server.impl.uri.rules.HttpMethodRule.accept(HttpMethodRule.java:288)
    at com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)
    at com.sun.jersey.server.impl.uri.rules.ResourceClassRule.accept(ResourceClassRule.java:108)
    at com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)
    at com.sun.jersey.server.impl.uri.rules.RootResourceClassesRule.accept(RootResourceClassesRule.java:84)
    at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1469)
    at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1400)
    at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1349)
    at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1339)
    at com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:416)
    at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:537)
    at com.sun.jersey.spi.container.servlet.ServletContainer.doFilter(ServletContainer.java:895)
    at com.sun.jersey.spi.container.servlet.ServletContainer.doFilter(ServletContainer.java:843)
    at com.sun.jersey.spi.container.servlet.ServletContainer.doFilter(ServletContainer.java:804)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:256)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:217)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:279)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
    at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:655)
    at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:595)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:161)
    at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:331)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:231)
    at com.sun.enterprise.v3.services.impl.ContainerMapper$AdapterCallable.call(ContainerMapper.java:317)
    at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:195)
    at com.sun.grizzly.http.ProcessorTask.invokeAdapter(ProcessorTask.java:849)
    at com.sun.grizzly.http.ProcessorTask.doProcess(ProcessorTask.java:746)
    at com.sun.grizzly.http.ProcessorTask.process(ProcessorTask.java:1045)
    at com.sun.grizzly.http.DefaultProtocolFilter.execute(DefaultProtocolFilter.java:228)
    at com.sun.grizzly.DefaultProtocolChain.executeProtocolFilter(DefaultProtocolChain.java:137)
    at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:104)
    at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:90)
    at com.sun.grizzly.http.HttpProtocolChain.execute(HttpProtocolChain.java:79)
    at com.sun.grizzly.ProtocolChainContextTask.doCall(ProtocolChainContextTask.java:54)
    at com.sun.grizzly.SelectionKeyContextTask.call(SelectionKeyContextTask.java:59)
    at com.sun.grizzly.ContextTask.run(ContextTask.java:71)
    at com.sun.grizzly.util.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:532)
    at com.sun.grizzly.util.AbstractThreadPool$Worker.run(AbstractThreadPool.java:513)
    at java.lang.Thread.run(Thread.java:722)

WARNING: StandardWrapperValve[default]: PWC1406: Servlet.service() for servlet default threw exception
javax.ejb.AccessLocalException: Client not authorized for this invocation
    at com.sun.ejb.containers.BaseContainer.preInvoke(BaseContainer.java:1888)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:212)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:88)
    at com.sun.proxy.$Proxy140.getInfoPara(Unknown Source)
    at org.company.server.rest.services.__EJB31_Generated__UserRestService__Intf____Bean__.getInfoPara(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    ...

I think I need to configure Glassfish to make it use the SecurityContextImpl i created. I don't know why doesn't work as it is correctly injected and I can call its methods in my code.

I could just use the isUserInRole() method manually but I would just avoid the problem instead of facing it. I'm sorry for the long post, but now I think you have all the information needed to help me with this. Thank you in advance for your help.

Emeric


Solution

  • Ok I've just completed that part (in fact all parts except for the EmailGateway).

    Firstly I say thanks to Iain Porter for his work - its top quality. And I apologise for mistakenly referring to him as just 'Porter' before.

    There is something going on with the Stackoverflow code formatting so be aware that some code proceeds the code boxes.

    For your problem I did this:

    1. web.xml - I don't use except for Faces
    2. Instead of web.xml and to register the various providers I created a class AuthApplicationConfig.java

      @ApplicationPath("rest")
      public class AuthApplicationConfig extends Application {
      
          @Override
          public Set<Class<?>> getClasses() {
              Set<Class<?>> resources = new java.util.HashSet<>();
      
              // REST resources
              resources.add(HealthCheckResource.class);
              resources.add(PasswordResource.class);
              resources.add(UserResource.class);
              resources.add(VerificationResource.class);
      
              // Filters (Auth)
              resources.add(RolesAllowedDynamicFeature.class);
              resources.add(SecurityContextFilter.class);
      
              // Misc
              resources.add(GenericExceptionMapper.class);
              return resources;
          }
      }
      

    See https://java.net/jira/browse/JERSEY-1634 for why you need to register the classes even though they are annotated with @Provider

    1. ResourceFilterFactory.java - I didn't use
    2. SecurityContextFilter.java is slightly different:

      @Provider
      @Priority(Priorities.AUTHENTICATION)    // So it comes in before org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
      
      public class SecurityContextFilter implements ContainerRequestFilter {
          @Inject
          Logger logger;
      
          protected static final String HEADER_AUTHORIZATION = "Authorization";
      
          protected static final String HEADER_DATE = "x-java-rest-date";
      
          protected static final String HEADER_NONCE = "nonce";
      
          private AuthorizationService authorizationService;
      
          ApplicationConfig config;
      
          @Inject
          public SecurityContextFilter(UserRepository userRepository,
                  UserService userService, ApplicationConfig config) {
              delegateAuthorizationService(userRepository, userService, config);
              this.config = config;
      
          }
      
          /**
           * If there is an Authorisation header in the request extract the session
           * token and retrieve the user
           * 
           * Delegate to the AuthorizationService to validate the request
           * 
           * If the request has a valid session token and the user is validated then a
           * user object will be added to the security context
           * 
           * Any Resource Controllers can assume the user has been validated and can
           * merely authorize based on the role
           * 
           * Resources with @PermitAll annotation do not require an Authorization
           * header but will still be filtered
           * 
           * @param request
           *            the ContainerRequest to filter
           * 
           */
          @Override
          public void filter(ContainerRequestContext requestContext)
                  throws IOException {
              System.out.println("SecurityContextFilter / filter("+printContainerRequestContext(requestContext)+")");
              String authToken = requestContext.getHeaderString(HEADER_AUTHORIZATION);
              String requestDateString = requestContext.getHeaderString(HEADER_DATE);
              String nonce = requestContext.getHeaderString(HEADER_NONCE);
              AuthorizationRequestContext context = new AuthorizationRequestContext(
                      requestContext.getUriInfo().getPath(),
                      requestContext.getMethod(), requestDateString, nonce, authToken);
              ExternalUser externalUser = authorizationService.authorize(context);
              requestContext
                      .setSecurityContext(new SecurityContextImpl(externalUser));
              System.out.println(String.format(" END OF SecurityContextFilter / filter - AuthorizationRequestContext: %s, externalUser:%s", context,externalUser));
          }
      
          private String printContainerRequestContext(ContainerRequestContext requestContext) {
              return String.format("[ContainerRequestContext:%s]", requestContext);
          }
      
          /**
           * Specify the AuthorizationService that the application should use
           * 
           * @param userRepository
           * @param userService
           * @param config
           */
          private void delegateAuthorizationService(UserRepository userRepository,
                  UserService userService, ApplicationConfig config) {
              System.out.println("SecurityContextFilter - requireSignedRequests?"+config.requireSignedRequests());
              if (config.requireSignedRequests()) {
                  this.authorizationService = new RequestSigningAuthorizationService(
                          userRepository, userService, config);
              } else {
                  this.authorizationService = new SessionTokenAuthorizationService(
                          userRepository);
              }
          }
      
          @Inject
          public void setConfig(ApplicationConfig config) {
              this.config = config;
          }
      }
      
    3. SecurityContextImpl is the same as Iain defined it:

      public class SecurityContextImpl implements SecurityContext {
      
          private final ExternalUser user;
      
          public SecurityContextImpl(ExternalUser user) {
              this.user = user;
          }
      
          @Override
          public Principal getUserPrincipal() {
              return user;
          }
      
          @Override
          public boolean isUserInRole(String role) {
              if(role.equalsIgnoreCase(Role.anonymous.name())) {
                   return true;
              }
              if(user == null) {
                  throw new InvalidAuthorizationHeaderException();
              }
              System.out.println(String.format("SecurityContextImpl / isUserInRole - role:%s, user:%s", role, user));
              return user.getRole().equalsIgnoreCase(role);
          }
      
          @Override
          public boolean isSecure() {
              return false;
          }
      
          @Override
          public String getAuthenticationScheme() {
              return SecurityContext.BASIC_AUTH;
          }
      }
      
    4. UserResource is as Iain defined it and similar to your UserRestService:

      @Path("/user")
      // @Component
      @Produces({ MediaType.APPLICATION_JSON })
      @Consumes({ MediaType.APPLICATION_JSON })
      @RequestScoped
      public class UserResource {
          // A Social thing that is not needed
          // private ConnectionFactoryLocator connectionFactoryLocator;
          @Inject
          Logger logger;
      
          @Inject
          protected UserService userService;
      
          @Inject
          protected VerificationTokenService verificationTokenService;
      
          @Inject
          protected EmailServicesGateway emailServicesGateway;
      
          @Context
          protected UriInfo uriInfo;
      
          // @Inject
          // protected ApplicationConfig config;
      
          // @Autowired
          // public UserResource(ConnectionFactoryLocator connectionFactoryLocator) {
          // this.connectionFactoryLocator = connectionFactoryLocator;
          // }
      
          @PermitAll
          @POST
          public Response signupUser(CreateUserRequest request) {
              AuthenticatedUserToken token = userService.createUser(request, Role.authenticated);
              verificationTokenService.sendEmailRegistrationToken(token.getUserId());
              URI location = uriInfo.getAbsolutePathBuilder().path(token.getUserId()).build();
              return Response.created(location).entity(token).build();
          }
      
          @RolesAllowed("admin")
          @Path("{userId}")
          @DELETE
          public Response deleteUser(@Context SecurityContext sc, @PathParam("userId") String userId) {
              ExternalUser userMakingRequest = (ExternalUser) sc.getUserPrincipal();
              userService.deleteUser(userMakingRequest, userId);
              return Response.ok().build();
          }
          ...