Simplified custom authorization rules in Java EE

In a previous article we looked at implementing a Java EE authorization module using the JACC specification. This module implemented the default authorization rules as specified by the JACC-, Servlet- and EJB specifications. In this article we go beyond that default algorithm and take a look at providing our own custom authorization rules.

In order to implement custom rules, one would traditionally ship an entire JACC provider with factory, configuration and policy. Not only is this a lot of code (JACC doesn't have any code that can be reused, for the smallest change everything needs to be implemented from scratch), it's also problematic that a JACC provider is global for the entire application server, while authorization rules are almost always specific for an individual application. Even when you adhere to the best practice of using one application per server, it's still quite a hassle to re-install the JACC provider after every little change separately from the application.

For this article we're therefor going to employ a different approach; use CDI to delegate from a single JACC provider that's installed once, to an application specific CDI bean. Incidentally this is largely the same thing Soteria/JSR 375 is doing for authentication mechanisms, which the key difference that the Java EE authentication SPI -does- allow authentication modules to be installed per application.

For the actual custom authorization rule we took inspiration from a rather interesting question and answer on StackOverflow:

Is it possible to determine group membership of a user on demand instead of when logging in via a ServerAuthModule (JASPIC)?

As it appears, the answer to this question is yes ;)

In order to come to a solution for the above stated problem we first took the original JACC provider from the previous article and refactored it a little. The permissions that were previously put in the intermediate base class TestPolicyConfigurationPermissions were factored out to a separate struct like class:

public class SecurityConstraints {

    private Permissions excludedPermissions = new Permissions();
    private Permissions uncheckedPermissions = new Permissions();
    private Map<String, Permissions> perRolePermissions = new HashMap<String, Permissions>();

    // + getters/setters
Furthermore a new class was introduced that holds the caller (name) principal, the (mapped) roles and the raw set of unmapped principals (which are often server specific):
public class Caller {

    private Principal callerPrincipal;
    private List<String> roles;
    private List<Principal> unmappedPrincipals;

    // + getters/setters
Next we define an interface for the application to implement. Our JACC provider will call this at certain points during the authorization process:
public interface AuthorizationMechanism {
    
    default Boolean preAuthenticatePreAuthorize(Permission requestedPermission, SecurityConstraints securityConstraints) {
        return null;
    }
    
    default Boolean preAuthenticatePreAuthorizeByRole(Permission requestedPermission, SecurityConstraints securityConstraints) {
        return null;
    }

    default Boolean postAuthenticatePreAuthorize(Permission requestedPermission, Caller caller, SecurityConstraints securityConstraints) {
        return null;
    }
    
    default Boolean postAuthenticatePreAuthorizeByRole(Permission requestedPermission, Caller caller, SecurityConstraints securityConstraints) {
        return null;
    }
}
As can be seen we distinguish for authorization decisions before authentication has taken place and thereafter, and for those at the start of an authorization decision and right before it comes time to check for role based permissions. The difference between those last two moments is that for the latter the tests for excluded permissions (those granted to no one) and unchecked (permissions granted to everyone) have already been performed. (Note that methods have been used for now, but perhaps events are the better solution here)

With these artefacts in place we can now modify the JACC Policy class to request a bean via CDI that implements the AuthorizationMechanism interface, and if it exists call one of the appropriate methods. The following shows an excerpt of this:

boolean postAuthenticate = domain.getPrincipals().length > 0;

AuthorizationMechanism mechanism = getBeanReferenceExtra(AuthorizationMechanism.class);
Caller caller = null;

if (postAuthenticate) {
    caller = new Caller(
        roleMapper.getCallerPrincipalFromPrincipals(currentUserPrincipals),
        roleMapper.getMappedRolesFromPrincipals(currentUserPrincipals),
        currentUserPrincipals);
}

if (mechanism != null) {
    Boolean authorizationOutcome = postAuthenticate? 
        mechanism.postAuthenticatePreAuthorize(requestedPermission, caller, securityConstraints) :
        mechanism.preAuthenticatePreAuthorize(requestedPermission, securityConstraints);
    
    if (authorizationOutcome != null) {
        return authorizationOutcome;
    }
}

In the code above getBeanReferenceExtra uses CDI to fetch a bean of type AuthorizationMechanism. This method works around a bug in Payara where the so-called component namespaces aren't being set. This bug is pretty much the same as also existed for calling a JASPIC SAM. If the mechanism is not null, then depending on whether authentication has already happened or not either the postAuthenticatePreAuthorize or the preAuthenticatePreAuthorize is called.

The JACC Policy doesn't tell us explicitly if the call is before or after authentication, but we can deduct this by looking at the passed-in principals. The assumption is that before authentication there are no principals at all, and after authentication there's at least one (if there was no actual authentication being done, the so-called "unauthenticated caller principal" is added).

For further details see the full source code.

With the JACC provider in place we can now create a Java EE web application that takes advantage of it. We'll start with a custom JSR 375 IdentityStore that only authenticates a single user named "test" and doesn't return any groups:

@RequestScoped
public class CustomIdentityStore implements IdentityStore {
      
    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        
        if (usernamePasswordCredential.getCaller().equals("test") && 
            usernamePasswordCredential.getPassword().compareTo("secret1")) {
            
            return new CredentialValidationResult(
                new CallerPrincipal("test"), 
                null // no static groups, dynamically handled via authorization mechanism 
            );
        }
        
        return INVALID_RESULT;
    }
    
}
We also add a custom JSR 375 HttpAuthenticationMechanism, one that's just used for testing purposes:
@RequestScoped
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {
    
    @Inject
    private IdentityStore identityStore;

    @Override
    public AuthStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) throws AuthException {

        if (request.getParameter("name") != null && request.getParameter("password") != null) {

            CredentialValidationResult result = identityStore.validate(
                new UsernamePasswordCredential(request.getParameter("name"), request.getParameter("password")));

            if (result.getStatus() == VALID) {
                return httpMessageContext.notifyContainerAboutLogin(
                    result.getCallerPrincipal(), result.getCallerGroups());
            } else {
                throw new AuthException("Login failed");
            }
        } 

        return httpMessageContext.doNothing();
    }
    
}
Note that we could have omitted both the custom identity store and custom authentication mechanism and instead configured the default embedded store and the default BASIC authentication mechanism, but now we more clearly see what's exactly happening in the authentication process. Also note that the HttpAuthenticationMechanism is a CDI based HTTP specific variant of the ServerAuthModule that was mentioned in the SO question cited above.

Let's now finally look at our custom authorization rule, which basically looks as follows:

@ApplicationScoped
public class CustomAuthorizationMechanism implements AuthorizationMechanism {
   
    @Override
    public Boolean postAuthenticatePreAuthorizeByRole(Permission requestedPermission, Caller caller, SecurityConstraints securityConstraints) {
        
        return getRequiredRoles(securityConstraints.getPerRolePermissions(), requestedPermission)
                .stream()
                .anyMatch(role -> isInRole(caller.getCallerPrincipal().getName(), role));
    }
}
What's happening here is that our custom code is being asked to authorize the caller for some requested permission. Such permission can e.g. be a WebResourcePermission for a given protected path like /foo/bar.

In order to do the on demand membership check as was asked for in the SO question we start with using the getRequiredRoles method and the collected SecurityConstraints to obtain a list of roles that are required for the requested permission. We then look if the current caller is in any of those roles using the isInRole method. This method can do whatever backend lookup is needed, but for an example application we'll mock it using a simple map where we put caller "test" in roles "foo", "bar" and "kaz".

For completeness, the getRequiredRoles method gets the roles that are required for a requested permission as follows:

return perRolePermissions
            .entrySet().stream()
            .filter(entry -> entry.getValue().implies(requestedPermission))
            .map(e -> e.getKey())
            .collect(toList());
In order to test if the authorization rule works we add a protected Servlet as follows:
@DeclareRoles({ "foo", "bar", "kaz" })
@WebServlet("/protected/servlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
public class ProtectedServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        response.getWriter().write("This is a protected servlet \n");

        String webName = null;
        if (request.getUserPrincipal() != null) {
            webName = request.getUserPrincipal().getName();
        }

        response.getWriter().write("web username: " + webName + "\n");

        response.getWriter().write("web user has role \"foo\": " + request.isUserInRole("foo") + "\n");
        response.getWriter().write("web user has role \"bar\": " + request.isUserInRole("bar") + "\n");
        response.getWriter().write("web user has role \"kaz\": " + request.isUserInRole("kaz") + "\n");
    }

}
The Servlet shown here is protected by the role "foo", so with the above given IdentityStore which doesn't return any groups/roles for our caller "test", we would normally not be able to access this Servlet. Yet, with the custom authorization rule in place we're able to access this Servlet just fine when authenticating as user "test".

Next we added a preAuthenticatePreAuthorize method to our authorization mechanism with some logging and also added logging to our existing postAuthenticatePreAuthorizeByRole method. We then deployed the application as "custom-authorization" to Payara 4.1.1.162 and requested http://localhost:8080/custom-authorization/protected/servlet?name=test&password=secret1. This resulted in the following log entries:

preAuthenticatePreAuthorize called. Requested permission: ("javax.security.jacc.WebUserDataPermission" "/protected/servlet" "GET")
preAuthenticatePreAuthorize called. Requested permission: ("javax.security.jacc.WebResourcePermission" "/protected/servlet" "GET")

validateRequest called. Authentication mandatory: true

postAuthenticatePreAuthorizeByRole called. Requested permission: ("javax.security.jacc.WebResourcePermission" "/protected/servlet" "GET")

postAuthenticatePreAuthorizeByRole called. Requested permission: ("javax.security.jacc.WebRoleRefPermission" "org.omnifaces.authorization.ProtectedServlet" "foo")
postAuthenticatePreAuthorizeByRole called. Requested permission: ("javax.security.jacc.WebRoleRefPermission" "org.omnifaces.authorization.ProtectedServlet" "bar")
postAuthenticatePreAuthorizeByRole called. Requested permission: ("javax.security.jacc.WebRoleRefPermission" "org.omnifaces.authorization.ProtectedServlet" "kaz")
What we see happening here is that our authorization mechanism is initially called twice before authentication. First to see if the request can be aborted early by checking if the protocol (http/https) is allowed, and secondly to see if the resource is allowed to be accessed publicly. If the resource is allowed to be accessed publicly, authentication is still being asked for, but it's not mandatory then.

For the request to "/protected/servlet" authentication is mandatory as shown above. Since authentication is thus mandatory, our authorization mechanism is called again after authentication for the same requested WebResourcePermission permission, but this time the caller data corresponding to the authenticated identity is also available. Our method will return "true" here, since ("javax.security.jacc.WebResourcePermission" "/protected/servlet" "GET") will turn out to require the role "foo", and our mock "isInRole" method will return "true" for caller "test" and role "foo".

We therefor continue to our requested resource, which means the servlet will be invoked.

Interesting to note here is that HttpServletRequest#isUserInRole is not implemented internally by checking a simple list of roles, but by calling our authorization module with a requested permission ("javax.security.jacc.WebRoleRefPermission" "org.omnifaces.authorization.ProtectedServlet" "[role name]"). The three isUserInRole calls that the servlet shown above does therefor show up in the log as three calls to our authorization mechanism, each for a different role.

Also interesting to note is that our authorization mechanism handles a requested permission to access "/protected/servlet" and a requested permission for isUserInRole("foo") completely identical. If necessary the authorization mechanism can of course inspect the requested permission, but for this use case that wasn't necessary.

To contrast the call to the protected servlet, let's also take a quick look at the log entries resulting from a call to a public servlet that's otherwise identical to the protected one:

preAuthenticatePreAuthorize called. Requested permission: ("javax.security.jacc.WebUserDataPermission" "/public/servlet" "GET")
preAuthenticatePreAuthorize called. Requested permission: ("javax.security.jacc.WebResourcePermission" "/public/servlet" "GET")

validateRequest called. Authentication mandatory: false

postAuthenticatePreAuthorizeByRole called. Requested permission: ("javax.security.jacc.WebRoleRefPermission" "org.omnifaces.authorization.PublicServlet" "foo")
postAuthenticatePreAuthorizeByRole called. Requested permission: ("javax.security.jacc.WebRoleRefPermission" "org.omnifaces.authorization.PublicServlet" "bar")
postAuthenticatePreAuthorizeByRole called. Requested permission: ("javax.security.jacc.WebRoleRefPermission" "org.omnifaces.authorization.PublicServlet" "kaz")
What we see here is that our authorization mechanism is initially called twice again, but since the default authorization algorithm will grant access to the anonymous caller the subsequent authentication is not mandatory and our authorization mechanism will also not be called a second time for the same resource permission.

For more details see the full source code of the example application.

Conclusion

We've seen that custom authorization rules allow us to do things that are normally not thought possible using Java EE security. Java EE's security model is clearly much more advanced than just the basic ability to check for roles, but much of its power has likely been untapped by many.

The reason for this is the relative obscurity of the JACC specification, the difficulty to install a provider and the very large amount of work required for even the smallest amount of custom authorization logic. The approach presented in this article allows us to re-use an existing JACC provider and provide custom authorization logic with a minimal amount of code.

What we presented here is only a prototype, but it may serve as a basis for authorization in the Java EE Security API (JSR 375).

Arjan Tijms

Comments

  1. Arjam what about PAC4J? (http://www.pac4j.org/) Did you tried this security library?

    ReplyDelete
  2. Thanks for another great article. But reading this left me with a somewhat open-ended question: From the average web-app's perspective, is there any tangible benefit in utilizing JACC this way? We can now redefine what a Servlet or EJB role (or even a Servlet user-data-constraint for that matter) actually means, or even dynamically add constraints not originally present, and it certainly sounds cool -- the question is why does it matter? I mean, we will still be authorizing clients based on our local domain model ("user can edit foo if user_roles table maps user to "manager", or foo_owner column references user") one way or another; why bother involving the container? We could still have interceptors or other mechanisms in place for isolating authorization from business logic without reliance on JACC. To me, the centralizing power of JACC only seems to "click" in a scenario where the app is inherently capable of utilizing some truly external, organization-wide or at least cross-app, policy decision point (e.g. a XACML policy) for its authorization needs, with as little mapping between foreign and local attributes as possible. What am I missing?

    ReplyDelete
    Replies
    1. That's a good question and one I've been asking myself as well.

      The answer is I think that, yes indeed, we can do something very close to this using interceptors or other mechanisms (Servlet filters), but where would these go to fetch their authorization logic from?

      If you embed them directly in those interceptors, they won't play ball very well with interceptors from other parties or with the default annotations like @RolesAllowed or security constraints in web.xml.

      So, it's IMHO not so much a matter of involving the container, but redirecting to a central repository which a universal interface / API, which many different components can query, and which many other different components can plug-in to.

      E.g. the JACC managed authorization repository can be queried by Servlet, JAX-RS, EJB, Java EE Security, our own security interceptors, and much more, and extensions to this authorisation repository can be provided by just as many components (a pure low-level JACC provider, but also CDI based extensions such as proposed in this article).

      Basically, it's a kind of "security backbone".

      Naturally a new kind of backbone could be designed, but JACC is already there, and has had a large number of rough edges polished over the many years. Now all we need (it's easier said then done, of course), is make it a little bit more practical by addressing some usability gaps in the current spec.

      Hope this helps and thanks for appreciating the article!

      Delete
    2. Thank you for bringing this additional perspective to my attention. Yes, from the standpoints of framework consolidation and standardization, your presented notion of a "security backbone" is certainly appealing.

      I think part of my struggles here stem from trying too hard (and failing) to use @RolesAllowed -- perhaps even JACC itself -- for "everything". It could additionally be due to lack of experience, and that I don't quite know at this point what exactly it is that I seek to accomplish. A bit of context on how my current authorization playground roughly looks like:
      - The SAM directly (i.e., not via JASPIC callbacks) populates the client subject with arbitrary principals. It only distinguishes the one representing the caller, so as to satisfy JASPIC's requirement for AuthStatus.(SEND_)SUCCESS. It doesn't bother signaling group principal semantics to the container.
      - The JACC policy delegate establishes different kinds of mappings, currently based on the underlying domain model, but meant to be "outsourceable", if need be:
      -- app-principal_to_app-role, where
      ---- app-principals are principals that may imply the ones established by the SAM.
      ---- app-roles are not Java EE roles but rather roles in an RBAC sense, are modeled after Java SE permissions, and can imply other roles.
      -- app-principal_to_app-permission, where app-permissions are the so-called authorization primitives being tested for by the app's business methods. Each app-permission, a, implies a WebRoleRef- or EJBRoleRefPermission, b, iff a.getName().equals(b.getActions()).
      -- app-role_to_app-permission, likewise.
      - Business methods are only supposed to use @RolesAllowed("app-permission-name").
      - The authorization mechanism checks whether any principals imply the ones given for evaluation, and, if so, whether any of them is (via the aforementioned mappings) associated with an app-permission implying the given (Web|EJB)RoleRefPermission.

      So I was basically hoping to "embed" all authorization logic within a single Policy#implies call -- ideally one that the container undertakes on the app's behalf as part of its pre-dispatch decision or immediately afterwards, before method execution actually begins. And it does work. The lack of expressive power in @RolesAllowed and is(User|Caller)InRole is however the key limitation. The policy needs to know both the action (usually represented by a Java SE permission's actions) and "authorization object" (represented by its class and/or name) to effectively decide. With a Java EE role it is quite hard to embed both pieces of information within a string, in a manner both consistent and extensible. Hence I am currently evaluating some alternatives (there are in reality overwhelmingly many options):
      - Query the policy directly with arbitrary app-/business-relevant permissions, either from business methods or interceptors. Strictly speaking though, it wouldn't be a "container-managed" solution anymore (whatever that implies).
      - Invent some new kind of configuration for affecting the pre-dispatch decision, such that the policy "knows" that when checking, say, an EJBMethodPermission for method updateFoo(String id, String newBarValue), it must also
      evaluate whether FooPermission(id, "update") is granted (in addition to the default container-provided configuration).
      - Only query JACC for standard Java EE security constraints and leave "higher-order" logic to the application.
      - Stop trying to come up with a universal authorization model. Query the policy when it practically makes sense, and prefer the domain model when it doesn't.

      In summary, I have yet "a bit" of experimentation left to carry out in order to arrive at a conclusion (which will probably be as definite as an "it depends" anyway, as is the case with most things in life :P).

      Delete
  3. Something I noticed while playing on Payara (4.1.2.174): During a policy's evaluation of an access request to an EJB, b0, suppose that it needs to call a (different) EJB, b1, itself. Calling the latter EJB causes a recursive invocation of the policy, wherein it has to self-grant the corresponding EJBMethodPermission. So far, so good. Upon returning from the b1 pre-dispatch authorization check and the actual b1 business method invocation, however, both the "javax.ejb.EnterpriseBean" and "javax.ejb.arguments" policy context handlers' values remain set to those corresponding to b1, rather than b0. While somewhat counter-intuitive, I doubt that this behaviour can be considered a bug, given that the spec makes no hard promises to providers interacting with application components, particularly components being subject to access control themselves. Perhaps this could be a minor item for clarification in a future JACC or Java EE Security spec iteration -- something along the lines of "during component invocations embedded within a pre-dispatch policy delegation, the container must either appropriately restore context after each such invocation has completed, or skip recursive policy delegation" (since the policy will have no option but to unconditionally self-grant anyway).

    ReplyDelete
    Replies
    1. That's a good observation really, and indeed something we should think about a little bit more. Especially when evaluation is brought closer to the application code this will get more important.

      For now it's an open question of how/when to continue. JACC, JASPIC and Java EE Security are all in the process of being transferred. When that's done and the Eclipse foundation gives the green signal I hope we can finally start with the revisions.

      For JASPIC at least a long overdue clarification is that a SAM is allowed to call a component like an EJB, request a datasource from JNDI and use the CDI bean manager. While those things already hold in practice, and JSR 375 requires this explicitly, JSR 196 is formally silent on this topic.

      Another open topic is giving JACC knowledge about JAX-RS, maybe introducing some WsResourcePermission or something like that.

      Delete
  4. Thank you for the example, it is working quite well.

    I have a special case. I use an abstraction layer in a bean to provide a generic api method. The method is secured by the role "PerformUseCase"
    but specific logic needs to be done using isCallerInRole.

    Something like:


    @Stateless
    @RolesAllowed("PerformUseCase")
    public class UseCaseBean {

    @Resource
    private SessionContext sessionContext;

    public performUserAction(UserAction userAction) {
    if (!sessionContext.isCallerInRole(userAction.getUseCase())) {
    throw new Exception();
    }

    if ("DeleteUseCase".equals(userAction.getUseCase())) {
    delete();
    }
    if ("EditUseCase".equals(userAction.getUseCase())) {
    edit();
    }
    }
    }


    Because there is no RolesAllowed annotation for "DeleteUseCase" and "EditUseCase" the custom jacc provider is not aware of roles "DeleteUseCase" and "EditUseCase" ("securityConstraints.getPerRolePermissions()" does not contain the roles), but it is aware of the "PerformUseCase" role.

    This can be solved by using @DeclareRoles({"DeleteUseCase", "EditUseCase"}) which makes the call to isCallerInRole work because now the "securityConstraints.getPerRolePermissions()" contains the values.

    It can also be solved by writing my own logic in CustomAuthorizationMechanism postAuthenticatePreAuthorizeByRole, like 'if getRequiredRoles does not return a result check an additional list of roles for the caller.getCallerPrincipal()'

    The first approach keeps the CustomAuthorizationMechanism implementation clean, but requires you to define all possible roles on a bean using @DeclareRoles.

    The second approach makes the CustomAuthorizationMechanism even more application specific.

    Do you have any experience with such a situation? Any tip / opinion? Would it be 'valid' to use isCallerInRole if the role is not directly defined in the application and only in some database?

    ReplyDelete

Post a Comment

Popular posts from this blog

Implementing container authentication in Java EE with JASPIC

Jakarta EE Survey 2022

What’s new in Jakarta Security 3?