Security Filters and Java Servlets in AEM 6.1+

Published

The other day I was trying to create a custom Sling Servlet inside of AEM, and found myself bumping up against several security measures that AEM had in place to secure its various API endpoints. These include things like a CSRF check (which was added and enabled by default in AEM 6.11), the Referrer Header Filtering service, and the basic Sling HTTP Authentication Service.

After some research and configuration, I eventually got the custom Servlet published. Theses were the general steps I followed:

  1. Creating and deploying the servlet
  2. OSGi Configuration for the various AEM security filters protecting requests
  3. Enabling POST requests pass-through on Dispatcher and CloudFront caching layers

Diagram displaying the various layers that an HTTP request must pass through between the web browser and a custom Sling Servlet. The request originates in the Web Browser, then passes through the CloudFront and Dispatcher caches, then into the AEM Publish Instance and through its various Security Filters, then finally reaching the Custom Servlet.

Above you can see each of the layers that an HTTP requests needs to pass through in order to reach the Custom Sling Servlet. I worked this problem from right-to-left, starting with the deployment of the Sling Servlet, then moving on to the OSGi configuration of the security filters, and then finally configuring POST pass-through on the AEM Dispatcher and CloudFront caches.

Creating and Deploying the Servlet

I deployed a simple servlet that would handle both GET and POST requests. I later revisited the servlet to include the custom functionality we needed, but this base servlet allowed me to at least confirm that my GET and POST requests were getting through.

package me.callsen.taylor.aemservlets.forms;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;

import javax.servlet.ServletException;

@SlingServlet(
    paths = [ "/bin/aemservlets/testendpoint" ], 
    methods = [ "GET" , "POST" ]
)
class AemSampleServlet extends SlingAllMethodsServlet {

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
        response.getWriter().write("GET request received & handled by servlet");
    }

    @Override
    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
        response.getWriter().write("POST request received & handled by servlet");
    }

}

Once the servlet was deployed, I was able to test connectivity in a web browser that had already been authenticated with AEM. I opened https://localhost:4502/bin/aemservlets/testendpoint and saw the expected “GET request received & handled by servlet” response.

However I noticed that when trying to POST to the same address with Postman, I received a 403 Forbidden error, even after adding in an Authorization header for AEM’s default admin/admin password. Upon inspecting the AEM server logs I saw that my request was failing because “org.apache.sling.security.impl.ReferrerFilter Rejected empty referrer header for POST request to /bin/aemservlets/testendpoint”.

OSGi Configuration for the AEM Security Filters

While working through this issue in AEM 6.2, there were 3 security layers that my POST requests need to pass through. In the order I encountered them, they were:

  1. The Referrer Header Filter Service
  2. The CSRF Framework check
  3. The Basic HTTP/Sling Authentication Service

1. The Referrer Header Filter Service

The first layer of security that my request was bumping up against was the Referrer Header layer, which essentially ensured requests were originating from an accepted Origin. For example, an AJAX request placed against this endpoint from an AEM page would have an accepted Origin. However since my requests were originating externally using Postman, they were being blocked.

I opted to continue enforcing the Referrer Header Filter Service for my servlet since it was being created to handle AJAX requests on AEM form pages. However, I did come across OSGi configurations that could be implemented to to allow external referrers.

In the Felix Console Configuration Manager (http://localhost:4502/system/console/configMgr), modifying the “Apache Sling Referrer Filter” configuration and removing “POST” from the Filter Methods (filter.methods) is the easiest way to side-step this filter. A more secure route would be to add accepted domains to the “Allow Hosts” parameter (allow.hosts).

Screen capture of the AEM OSGi Configuration Manager for the Referrer Filter, with mydomain.com added as an Allowed Host, and the POST removed from the Filter Methods.

2. The CSRF Framework

I tried my request in Postman again and bumped into the next layer of security, the CSRF Framework. In the logs I was seeing: “com.adobe.granite.csrf.impl.CSRFFilter isValidRequest: empty CSRF token – rejecting”.

There are various options out there to play nice with AEM’s CSRF framework, including this guide which details how to make an AJAX request to the CSRF token endpoint (/libs/granite/csrf/token.json), and include the returned token in your servlet request as the “CSRF-Token” header. If going this route, make sure to enable pass through for this header in your Apache/Dispatcher and caching layers.

Another approach is to whitelist your servlet path in the “Adobe Granite CSRF Filter” OSGi configuration. Simply add the servlet path as an Excluded Path (filter.excluded.paths).

Screen capture of the AEM OSGi Configuration Manager for the CSRF Filter, with the servlet path added as an Excluded Path.

NOTE: For anyone that likes to include their OSGi configurations in the codebase/repository, I had difficulty getting this particular OSGi configuration to inherit the default/non-specified properties – beware that you may need to include all of the default properties on the repository node as well, especially the filter.enable.safe.user.agents = false property. Omitting this property could lead to difficulties when replicating code packages between Author and Publish instances.

3. Sling Authentication (HTTP Auth)

After configuring the Referrer and CSRF Filters, my POST request was making it through to the servlet, which was returning the expected “POST request received & handled by servlet”. I realized that I still had the HTTP Authorization header present on my requests. After removing the Authorization header, a “403 Forbidden” response came back.

This may not always be necessary, especially if your servlet will be used only by authenticated users (e.g. in the Authoring interface). Since I was building a servlet to handle AJAX form submissions, I needed to allow requests from non-authenticated users originating on Publish instances.

I was able to accomplish this by configuring the “Sling Authentication Service” in the Felix Console Configuration Manager. I added the servlet path to the Authentication Requirements list (sling.auth.requirements), with a “-” at the front, which specified that authentication was not required for this path.

Screen capture of the AEM OSGi Configuration Manager for the Sling Authentication Service, with the servlet path added as an Authentication Requirement.

Once I configured the Sling Authentication service, I resumed seeing the “POST request received & handled by servlet” response again.

Enabling POST request pass-through on Dispatcher and CloudFront Caches

Next I needed to add a line to the filters section of my dispatcher any file to allow POST requests to pass through unobstructed. The line was pretty simple, and included the request type and a GLOB for the URL path (since it included a wildcard for future servlets).

/0120 { /type “allow” /glob “POST /bin/aemservlets/*” }

Finally, the last step was to configure CloudFront to accept and pass along POST requests. In order to do this, we needed to create a new Behavior inside of our existing Distribution. We kept the Origin the same as our Default Behavior, but made a few non-default configurations highlighted in red. These included allowing all HTTP Methods, and forwarding on GET query strings (just incase they are needed in the future).

Screen capture of an AWS CloudFront Behavior creation screen, with the servlet path as the path, origin set to the default AEM Origin, all HTTP Methods allowed, and Query Strings being forwarded.

Once these updates were made to Dispatcher and CloudFront, we were able to see the POST requests coming through on Publish instances. Hopefully deploying the next servlet will be less of a hassle 🙂

References

  1. https://docs.adobe.com/docs/en/aem/6-1/develop/security/csrf-protection.html