Customizing Spring Security with Multiple Authentications

 

Spring Boot offers an easier way to create new web applications or web services. The Security module in the Spring framework enables us to plug in different authentication mechanisms. In some cases, we needed to provide multiple authentication mechanisms for our web service. These authentication mechanisms can be standard or custom.

springauth-1

We had a similar requirement while working on an in-house project to develop a web service for distributing and renewing Kerberos key tabs. The project is named Kite, and it is a web service built on Spring Boot. We initially added SPNEGO to authenticate users of our Kite service.

Enabling SPNEGO Authentication using Spring Security

To enable security and add SPNEGO, we needed to make changes to our pom.xml. The relevant POM changes are shown here:

<dependencies>
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>4.0.4.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>4.0.4.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.security.kerberos</groupId>
    <artifactId>spring-security-kerberos-web</artifactId>
    <version>1.0.1.RELEASE</version>
  </dependency>
</dependencies>

We also needed to add Java code to configure SPENGO authentication. The relevant parts of the Java code related to hooking up SPENGO authentication are shown below:

@Configuration @EnableWebSecurity 
public class SecurityConfig extends WebSecurityConfigurerAdapter { 

  @Override 
  protected void configure(HttpSecurity http) throws Exception { 
    http.exceptionHandling() 
   .authenticationEntryPoint(spnegoEntryPoint())
   .and().authorizeRequests().anyRequest().authenticated() 
   .and().formLogin().loginPage("/login").permitAll() 
   .and().logout().permitAll() 
   .and().addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()), BasicAuthenticationFilter.class); 
  } 

  @Autowired 
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(kerberosAuthenticationProvider()) 
    .authenticationProvider(kerberosServiceAuthenticationProvider()); 
  } 

  @Bean 
  public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
    KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider(); 
    SunJaasKerberosClient client = new SunJaasKerberosClient(); 
    provider.setKerberosClient(client); 
    provider.setUserDetailsService(dummyUserDetailsService()); 
    return provider; 
  } 

  @Bean 
  public SpnegoEntryPoint spnegoEntryPoint() { 
    return new SpnegoEntryPoint(); 
  } 

  @Bean 
  public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter( AuthenticationManager authenticationManager) {
    SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter(); 
    filter.setAuthenticationManager(authenticationManager); 
    return filter; 
  } 

  @Bean 
  public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() { 
    KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider(); 
    provider.setTicketValidator(sunJaasKerberosTicketValidator()); provider.setUserDetailsService(dummyUserDetailsService()); 
    return provider; 
  } 

  @Bean 
  public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
    SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator(); 
    ticketValidator.setServicePrincipal(kiteConfiguration.getSpnegoPrincipal()); 
    ticketValidator.setKeyTabLocation(new FileSystemResource(kiteConfiguration.getKeytab())); 
    ticketValidator.setDebug(true); return ticketValidator; 
  } 
} 

These POM and source code changes enable users to authenticate via Kerberos. Some of our users needed an alternate form of authentication, where the users present a one-time-use token. To hook up our custom token authentication, we took the following steps:

  1. Implement token authentication logic as TokenAuthenticationFilter by extending AbstractAuthenticationProcessingFilter
  2. .

  3. Plug in TokenAuthenticationFilter via FilterRegistrationBean.

Custom TokenAuthenticationFilter

The custom token-based authentication filter extends AbstractAuthenticationProcessingFilter. Here we override the doFilter to implement our custom authentication logic.

public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
 private static final String SECURITY_TOKEN_KEY = "token";
 private TokenManager tokenManager;

 public TokenAuthenticationFilter(TokenManager tm) {
   super("/");
   tokenManager = tm;
 }

 @Override
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;
   String token = request.getParameter(SECURITY_TOKEN_KEY);
   if (token != null) {
     Authentication authResult;
     try {
       authResult = attemptAuthentication(request, response, token);
       if (authResult == null) {
         response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
         return;
       }
     } catch (AuthenticationException failed) {
       response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
       return;
    }

    try {
      SecurityContextHolder.getContext().setAuthentication(authResult);
    } catch (Exception e) {
      logger.error(e.getMessage(), e);
      if (e.getCause() instanceof AccessDeniedException) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return;
      }
    }
  }
  chain.doFilter(request, response);// return to others spring security filters
}

 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response, String token)
 throws AuthenticationException, IOException, ServletException {
   AbstractAuthenticationToken userAuthenticationToken = authUserByToken(token);
   if (userAuthenticationToken == null)
     throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
    return userAuthenticationToken;
 }

 private AbstractAuthenticationToken (String tokenRaw) {
 AbstractAuthenticationToken authToken = null;
   try {
     String user = tokenManager.verifyAndExtractUser(tokenRaw);
     if (user != null) {
       user = user + "@REALM";
       Principal securityUser = new SecurityUser(user);
       return new PreAuthenticatedAuthenticationToken(securityUser, null, null);
     }
   } catch (Exception e) {
     logger.error("Error during authUserByToken", e);
   }
   return authToken;
 }
}

Note that in authUserByToken, we created a SecurityUser object, and this needs to be of the format user@REALM.

Adding TokenAuthenticationFilter

To plug in the new authentication mechanism, we can use the FilterRegistrationBean.

The code snippet below is added to the SecurityConfig above:

@Bean
 public FilterRegistrationBean filterRegistrationBean() {
   FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
   TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter();
   filterRegistrationBean.setFilter(tokenAuthenticationFilter);
   filterRegistrationBean.setEnabled(true);
   return filterRegistrationBean;
 }

With these code changes, we can add our custom authentication logic in Spring in addition to the existing authentication mechanisms.

Experience the Lightning Bolt

 

Three months back we announced how we are transforming the shopping experience at eBay, enabling our users to browse with style and speed. Our goal was to provide an engaging experience not only to users who are within the eBay site, but also to mobile users accessing eBay from external platforms like Google and Twitter. This is where AMP technology comes into play. We implemented an AMP version for our new product browse experience, along with the regular mobile web pages, and launched them in June. At that time we did not make our AMP content discoverable to Google, as we had a few pending tasks to be completed. Also, AMP links surfaced in Google search results only for publisher-based content and not for eCommerce.

Things have changed now. Google announced that they are opening AMP beyond the news industry to include eCommerce, travel and so on. From our end, we wrapped up the pending items and linked the AMP pages from non-AMP pages to make them discoverable. Today we are happy to announce that users around the globe will start seeing eBay AMP links in Google search results and experience the lightning bolt — instant loading. We have close to 15 million AMP-based product browse pages, but not all will appear as AMP right away. This feature is being ramped up and will eventually surface. Check out some of the popular queries in a mobile browser — “iPhone 6 no contract” and “canon digital cameras,” for example. The AMP lightning bolt appears next to links as an indication. AMP for eCommerce is now a reality.

eBay AMP Product Browse Page eBay AMP link in Google search results (left); eBay AMP product browse page (right)

Between now and then

Following our initial launch in June, we did a couple of things to make AMP ready for prime time. We outline a few these efforts here.

Robust analytics system

Understanding how users interact with our pages is critical for us to provide the most optimized experience. The back-end system that powers the new product browse experience is designed in such a way that it constantly collects users’ screen activity, learns from it, and optimizes the experience for subsequent visits. For example, if users interact more often with a module that appears below the fold in the screen, then in future visits to the same browse page, that module will start appearing above the fold. Our non-AMP page has a custom analytics library that does the reporting to the back end.

AMP has a component (amp-analytics) for doing this. In our initial AMP launch, we used this component just to track page impressions. It provides a fairly exhaustive tracking mechanism. But what we wanted was more granular control at an element level, where each element dictates what it wants to track. We started working with the AMP team on this and came up with a spec. We went ahead implemented the spec and contributed it back to the open-source project. With the implementation in place, we were able to achieve a robust and advanced analytic system that reports user interactions like click, scroll, and visibility to our backend, which in turn optimizes the subsequent visits.

Feature parity

We mentioned in our previous blog that most of the code is shared between the AMP and non-AMP pages. Even with this code sharing, there were still small feature inconsistencies between the two versions. We closed these gaps, fixed the inconsistencies, and put a process in place to make sure they do not creep in. Having said that, there were certain UI components and behaviors that we were not able to achieve in the AMP version due to restrictions. Some of these components are eCommerce-specific. We are working with the AMP team to add them to the component list so everyone can benefit. A good example would be tabbed UI component, and there is already a feature request to get this implemented.

Streamlined build process

During the initial launch, we put manual effort into managing assets (CSS and JavaScript) between the AMP and non-AMP versions. In the AMP version, there should be no JavaScript, and all CSS should be inline, whereas in the non-AMP version, both CSS and JavaScript should be bundled and externalized. Doing this manually was not ideal. Our asset pipeline tool, Lasso, had a solution for this —  conditional dependencies. We created an AMP flag that gets initialized to true if the request is AMP and then set as a Lasso flag. The pipeline gets access to it and automatically bundles, externalizes, or inlines resources based on the conditions. This was a big time saver and ended up being very efficient.

The road ahead

We are not done yet; in fact, we are just getting started. We have a bunch of tasks lined up.

  • Beyond AMP — We know AMP pages are fast. But what about the subsequent pages the user visits? Currently when users click on a link in the AMP page, a new tab opens, and the destination page is loaded there. In our case, the mobile web version of the destination page is loaded. We want that experience also to be as fast and consistent as the AMP experience. There is an AMP component (amp-install-serviceworker) to achieve this goal, and our top priority is to leverage this utility and create a seamless transition from the AMP to the target page. We are also discussing with the Google team about how to avoid the new tab and continue the experience in the same window.
  • Cache freshness — AMP content is served from Google AMP cache, and the cache update policy can be found here. What this means to eBay is, for popular product queries, users always see fresh content. But for certain extremely rare queries, a few users may end up seeing stale content. While this is not a common scenario, there is an AMP component (amp-fresh) in the works to fix this. We will be integrating this component as soon as it is ready. In the meanwhile, we have a script that we manually run for a few products to update the AMP content in cache.
  • Unified version — Currently we have two versions of the new browse pages — AMP and non-AMP. The AMP version shows up to users searching in Google, and the non-AMP version shows up to users searching within eBay. Although both of them are highly optimized, look the same, and share most of the code, updating both versions is still a maintenance overhead. In addition, we always need to watch out for feature parity. In the future, based on how AMP pages are performing, we may choose to have one mobile version (AMP) and serve it to all platforms.

We are very excited to provide the AMP experience to our mobile users coming from Google. We have been playing with it for a while, and it is indeed lightning fast. Mobile browsing can be slow and sometimes frustrating, and this is where AMP comes in and guarantees a consistent and fast experience. We hope our users benefit from this new technology.

Senthil Padmanabhan | Principal Engineer at eBay

eBay Releases Dynamic Application Security Testing Proxy as Open Source

 

In an effort to contribute to the open-source community for security, Global Information Security (GIS) at eBay released its DAST Proxy as open-source software. DAST Proxy is a life-cycle management tool for dynamic application security scans that has a unique feature set. It is available for download and contribution under the MIT License at https://github.com/eBay/DASTProxy.

What is DAST Proxy?

DAST Proxy has work flows that help users record browser actions and submit them to a backend scan engine, such as AppScan. It updates the user with the scan status and publishes the scan results. It supports automation integration and has a set of RESTful web services that can be seamlessly integrated into any existing Selenium (or any other automation framework) functional test cases for security testing. DAST Proxy also works with all the browser-based test cases for both web and mobile applications.

DAST Architecture

dast-architecture

How Does DAST Proxy work?

This section explains how to conduct a manual dynamic security scan using DAST Proxy.

To start, the user is required to have two browsers installed. On Browser 1, the user obtains a proxy host and port generated by the DAST server on DAST Proxy’s home page. The user then inserts this host and port into Browser 2’s proxy settings. Once the proxy is set up, DAST Proxy records all the web traffic between Browser 2 and the QA server and stores it in a HAR file. The same file is then submitted to the back-end scan engine for thorough dynamic security testing. DAST Proxy polls the back-end engine for the status and resultant vulnerabilities and stores them in the database, which is accessible to the user via the DAST Proxy dashboard.

dast-manual-flow-latest

DAST Proxy features

  • Recording the scan and submitting it to a back-end scan engine, such as AppScan
  • Dashboard with list of scans, vulnerabilities, and payloads
  • Integration with JIRA system
  • Ability to rerun the scans from the dashboard
  • Support for manual API-end point testing with browser plug-ins, such as Postman

Features in the pipeline

  • ZAP (OWASP Zed Attack Proxy project) integration
  • Selenium integration
  • NT OBJECTives integration