Quantcast
Channel: Arjan Tijms' Weblog
Viewing all articles
Browse latest Browse all 76

Simplified custom authorization rules in Java EE

$
0
0
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 collectedSecurityConstraints 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


Viewing all articles
Browse latest Browse all 76

Trending Articles