javaspring-cloudnetflix-zuulspring-cloud-security

Spring Cloud: Microservice authentication works through Eureka Discovery, but not through Zuul


I'm kind of new to Spring Cloud. I'm trying to build a few microservices by using Spring Cloud Eureka Discovery and Zuul Gateway.

I can access the microservices and perform actions through Zuul Gateway, but only when there is no security involved. If my microservice is secured, then I cannot do anything through Zuul as it doesn't return back a JWT token. If I do it through Eureka discovery client it works like a charm.

Maybe there is something wrong with my Zuul configuration? Or maybe I have chosen an invalid way to secure the services? Thanks in advance!

Here is my Zuul Gateway configuration:

Application.class

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayServiceApplication.class, args);
    }

    @Bean
    public Filter shallowEtagHeaderFilter() {
        return new ShallowEtagHeaderFilter();
    }
}

application.properties

eureka.client.service-url.defaultZone=http://localhost:8010/eureka/
server.port=8011
zuul.prefix=/services

bootstrap.properties

spring.application.name=gateway-service
# specify where config server is up
spring.cloud.config.uri=http://localhost:8001

Here is my microservice JWT & security configuration:

WebSecurityConfig.class

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure (HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                // We filter the api/login requests
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                // And filter other requests to check the presence of JWT in header
                .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure (AuthenticationManagerBuilder auth) throws Exception {
        // Create a default account
        auth.inMemoryAuthentication()
                .withUser("**")
                .password("**")
                .roles("**");
    }
}

TokenAuthenticationService.class

public class TokenAuthenticationService {
    static final long EXPIRATIONTIME = 864_000_000; // 10 days
    static final String SECRET = "*";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";

    static void addAuthentication (HttpServletResponse res, String username) {
        String JWT = Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
    }

    static Authentication getAuthentication (HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            String user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();

            return user != null ?
                    new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()) :
                    null;
        }
        return null;
    }
}

JWTLoginFilter.class

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter (String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication (HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {

        AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class);
        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getUsername(),
                        creds.getPassword(),
                        Collections.emptyList()
                )
        );

    }

    @Override
    protected void successfulAuthentication (HttpServletRequest req,
                                             HttpServletResponse res,
                                             FilterChain chain,
                                             Authentication auth)
            throws IOException, ServletException {
        TokenAuthenticationService.addAuthentication(res, auth.getName());
    }
}

JWTAuthenticationFilter.class

public class JWTAuthenticationFilter extends GenericFilterBean {
    @Override
    public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        Authentication authentication = TokenAuthenticationService.getAuthentication((HttpServletRequest) servletRequest);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

Solution

  • try to define the below.

    zuul.sensitiveHeaders=Cookie,Set-Cookie
    

    In zuul, Cookie, Set-Cookie and Authroization headers are default sensitive headers. If you want to Authroization header in your api server, you need to redefine it without Authroization header like above.

    Also you can define it for each route. Please refer to the doc : http://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html [Cookies and Sensitive Headers]