Home > Enterprise >  Spring Boot Authentication succeeds with embedded Tomcat, but returns 403 with Open/WAS Liberty
Spring Boot Authentication succeeds with embedded Tomcat, but returns 403 with Open/WAS Liberty

Time:07-15

I use Spring Security to authenticate/authorize against Active Directory. Below code works just fine if I run it in Spring embedded Tomcat.

But when I switch to Open/WAS Liberty server, I get 403 on authenticate (/auth endpoint):

My WebSecurityConfiguration class looks like:

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value("${active.dir.domain}")
    private String domain;

    @Value("${active.dir.url}")
    private String url;

    @Value("${active.dir.userDnPattern}")
    private String userDnPattern;

    private final Environment environment;

    public WebSecurityConfiguration(Environment environment) {
        this.environment = environment;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(activeDirectoryAuthenticationProvider()).eraseCredentials(false);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors(Customizer.withDefaults())
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .addFilter(getAuthenticationFilter())
                .addFilter(new AuthorizationFilter(authenticationManager()))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); 
    }

    @Bean
    public AuthenticationProvider activeDirectoryAuthenticationProvider() {

        String adSearchFilter = "(&(sAMAccountName={1})(objectClass=user))";

        ActiveDirectoryLdapAuthenticationProvider ad = new ActiveDirectoryLdapAuthenticationProvider(domain, url, userDnPattern);
        ad.setConvertSubErrorCodesToExceptions(true);
        ad.setUseAuthenticationRequestCredentials(true);
        ad.setSearchFilter(adSearchFilter);

        return ad;
    }

    //CORS configuration source
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {

        final CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowedOrigins(Arrays.asList("http://some.url"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(Arrays.asList("*"));

        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    //Customize the Spring default /login url to overwrite it with /auth.
    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
        filter.setFilterProcessesUrl("/auth");
        return filter;
    }
}

Here is my AuthorizationFilter class:

public class AuthorizationFilter extends BasicAuthenticationFilter {

    public AuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        String authorizationHeader = request.getHeader("Authorization");
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

    //Extracts username from Jwt token
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {

        String token = request.getHeader("Authorization");
        if (token != null) {
            token = token.replace("Bearer ", "");

            String username = Jwts.parser()
                    .setSigningKey("somesecret")
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();

            if (username != null) {
                return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
            }
        }

        return null;
    }
}

Here is my AuthenticationFilter class:

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public AuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        UserLoginRequestModel userLoginRequestModel = extractCredentials(request);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                userLoginRequestModel.getUsername()
                , userLoginRequestModel.getPassword()
                , new ArrayList<>());

        return authenticationManager.authenticate(token);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException, ServletException {

        String userId = ((UserDetails)auth.getPrincipal()).getUsername(); 
        Instant now = Instant.now();

        String jwtToken = Jwts.builder()
                .setSubject(userId)
                .setIssuer("me")
                .setAudience("myapp")
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(Date.from(now))
                .setExpiration(Date.from(now.plus(30000)))
                .signWith(SignatureAlgorithm.HS512, SecurityConstants.getTokenSecret())
                .compact();

        response.addHeader("Authorization", "Bearer "   jwtToken);
        response.addHeader("Access-Control-Expose-Headers", accessControlHeaders.toString());
    }

    private UserLoginRequestModel extractCredentials(HttpServletRequest request) {

        UserLoginRequestModel userLoginRequestModel = new UserLoginRequestModel();
        String authorizationHeader = request.getHeader("Authorization");

        try {
            if (authorizationHeader != null && authorizationHeader.toLowerCase().startsWith("basic")) {
                String base64Credentials = authorizationHeader.substring("Basic".length()).trim();
                byte[] decodedCredentials = Base64.getDecoder().decode(base64Credentials);
                String headerCredentials = new String(decodedCredentials, StandardCharsets.UTF_8);
                final String[] credentialsValues = headerCredentials.split(":", 2);
                userLoginRequestModel.setUsername(credentialsValues[0]);
                userLoginRequestModel.setPassword(credentialsValues[1]);
            } else {
                userLoginRequestModel = new ObjectMapper().readValue(request.getInputStream(), UserLoginRequestModel.class);
            }

            return userLoginRequestModel;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

In Postman, I call:

POST: http://localhost/myapi/v1/auth

And I pass it BasicAuth with username and password.

I get 403 Forbidden back if I run this on Open/WAS Liberty. Same code, with no change whatsoever, runs just fine in embedded Tomcat that comes with Spring and I get 200 OK.

CodePudding user response:

The reason I was experiencing this was that in my Liberty server.xml, I was missing defined context-path. As it looks like, Liberty does not consider context-path set up in your application.properties file.

Below is the context-path I have in my application.properties file. Unfortunatelly, Liberty does not read (or considers) it and just uses the app name as the context-path instead of using the setting in application.properties or application.yml file:

server.servlet.context-path=/myapi/v1

As a result, the above context-path will work just fine if deployment in Spring Boot embedded Tomcat container but not in Liberty container.

When you deploy it to OpenLiberty/WASLiberty, you might find that your endpoints will stop working and you get 403 and/or 404 errors.

In my example above, I have getAuthenticationFilter() method, in my WebSecurityConfiguration class. Below, I added little bit more comments to it to explain:

//Customize the /login url to overwrite the Spring default provided /login url.
private AuthenticationFilter getAuthenticationFilter() throws Exception {
    final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
    // This works fine on embedded tomcat, but not in Liberty where it returns 403.  
    // To fix, in server.xml <appllication> block, add 
    // <application context-root="/myapi/v1" ... and then both
    // auth and other endpoints will work fine in Liberty.
    filter.setFilterProcessesUrl("/auth"); 
    // This is temporary "fix" that creates rather more issues, as it 
    // works fine with Tomcat but fails in Liberty and all other
    // endpoints still return 404
    //filter.setFilterProcessesUrl("/v1/auth"); 
    return filter;
}

Based on the above context-path, on Tomcat, it becomes /myapi/v1/auth while on Liberty, it ends up being just /myapi/auth which is wrong. I think what Liberty does, it will just take the name of the api and add to it the endpoint, therefore ignoring the versioning.

As a result of this, AntPathRequestMatcher class matches() method will result in a non-matching /auth end point and you will get 403 error. And the other endpoints will result in 404 error.

SOLUTION

  1. In your application.properties, leave:

    server.servlet.context-path=/myapi/v1

, this will be picked up by embedded Tomcat and your app will continue to work as expected.

  1. In your server.xml configuration for Open/WAS Liberty, add matching context-root to the section like:
<application context-root="/myapi/v1" id="myapi" location="location\of\your\myapi-0.0.1.war" name="myapi" type="war">

, this will be picked up by Open/WASLiberty and your app will continue to work as expected on Liberty container as well.

  • Related