I'm upgrading an application to Spring Boot 2.0.3
.
But my login request is unauthorized:
curl -H "Accept:application/json" -H "Content-Type: application/json" "http://localhost:8080/api/users/login" -X POST -d "{ \"email\" : \"myemail@somedomain.com\", \"password\" : \"xxxxx\" }" -i
The response is a 401 Unauthorized access. You failed to authenticate
.
It is given by my custom entry point:
@Component public final class RESTAuthenticationEntryPoint extends BasicAuthenticationEntryPoint { private static Logger logger = LoggerFactory.getLogger(RESTAuthenticationEntryPoint.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException { logger.debug("Security - RESTAuthenticationEntryPoint - Entry point 401"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access. You failed to authenticate."); } @Override public void afterPropertiesSet() throws Exception { setRealmName("User REST"); super.afterPropertiesSet(); } }
The debugger shows the authenticate
method of my CustomAuthenticationProvider
is not called as I expect it to be:
@Component public class CustomAuthenticationProvider implements AuthenticationProvider { @Autowired CredentialsService credentialsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String email = authentication.getName(); String password = authentication.getCredentials().toString(); List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<SimpleGrantedAuthority>(); User user = null; try { user = credentialsService.findByEmail(new EmailAddress(email)); } catch (IllegalArgumentException e) { throw new BadCredentialsException("The login " + email + " and password could not match."); } if (user != null) { if (credentialsService.checkPassword(user, password)) { grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); return new UsernamePasswordAuthenticationToken(email, password, grantedAuthorities); } else { throw new BadCredentialsException("The login " + user.getEmail() + " and password could not match."); } } throw new BadCredentialsException("The login " + authentication.getPrincipal() + " and password could not match."); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
But the filter is exercised and a null token is found:
@Component public class AuthenticationFromTokenFilter extends OncePerRequestFilter { @Autowired private TokenAuthenticationService tokenAuthenticationService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { tokenAuthenticationService.authenticateFromToken(request); chain.doFilter(request, response); } } @Service public class TokenAuthenticationServiceImpl implements TokenAuthenticationService { private static Logger logger = LoggerFactory.getLogger(TokenAuthenticationServiceImpl.class); private static final long ONE_WEEK = 1000 * 60 * 60 * 24 * 7; private static final String TOKEN_URL_PARAM_NAME = "token"; @Autowired private ApplicationProperties applicationProperties; @Autowired private UserDetailsService userDetailsService; public void addTokenToResponseHeader(HttpHeaders headers, String username) { String token = buildToken(username); headers.add(CommonConstants.AUTH_HEADER_NAME, token); } public void addTokenToResponseHeader(HttpServletResponse response, Authentication authentication) { String username = authentication.getName(); if (username != null) { String token = buildToken(username); response.addHeader(CommonConstants.AUTH_HEADER_NAME, token); } } private String buildToken(String username) { String token = null; UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails != null) { Date expirationDate = new Date(System.currentTimeMillis() + ONE_WEEK); token = CommonConstants.AUTH_BEARER + " " + Jwts.builder().signWith(HS256, getEncodedPrivateKey()).setExpiration(expirationDate).setSubject(userDetails.getUsername()).compact(); } return token; } public Authentication authenticateFromToken(HttpServletRequest request) { String token = extractAuthTokenFromRequest(request); logger.debug("The request contained the JWT token: " + token); if (token != null && !token.isEmpty()) { try { String username = Jwts.parser().setSigningKey(getEncodedPrivateKey()).parseClaimsJws(token).getBody().getSubject(); if (username != null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); logger.debug("Security - The filter authenticated fine from the JWT token"); } } catch (SignatureException e) { logger.info("The JWT token " + token + " could not be parsed."); } } return null; } private String extractAuthTokenFromRequest(HttpServletRequest request) { String token = null; String header = request.getHeader(CommonConstants.AUTH_HEADER_NAME); if (header != null && header.contains(CommonConstants.AUTH_BEARER)) { int start = (CommonConstants.AUTH_BEARER + " ").length(); if (header.length() > start) { token = header.substring(start - 1); } } else { // The token may be set as an HTTP parameter in case the client could not set it as an HTTP header token = request.getParameter(TOKEN_URL_PARAM_NAME); } return token; } private String getEncodedPrivateKey() { String privateKey = applicationProperties.getAuthenticationTokenPrivateKey(); return Base64.getEncoder().encodeToString(privateKey.getBytes()); } }
My security configuration is:
@Configuration @EnableWebSecurity @ComponentScan(nameGenerator = PackageBeanNameGenerator.class, basePackages = { "com.thalasoft.user.rest.security", "com.thalasoft.user.rest.filter" }) public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private AuthenticationFromTokenFilter authenticationFromTokenFilter; @Autowired private SimpleCORSFilter simpleCORSFilter; @Autowired private RESTAuthenticationEntryPoint restAuthenticationEntryPoint; @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.authenticationProvider(new CustomAuthenticationProvider()); } @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling() .authenticationEntryPoint(restAuthenticationEntryPoint) .and() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(simpleCORSFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class) .headers().cacheControl().disable().frameOptions().disable() .and() .userDetailsService(userDetailsService) .authorizeRequests() .antMatchers(RESTConstants.SLASH + UserDomainConstants.USERS + RESTConstants.SLASH + UserDomainConstants.LOGIN).permitAll() .antMatchers(RESTConstants.SLASH + RESTConstants.ERROR).permitAll() .antMatchers("/**").hasRole(UserDomainConstants.ROLE_ADMIN).anyRequest().authenticated(); } }
The user details service is:
@Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private CredentialsService credentialsService; @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (username != null && !username.isEmpty()) { User user = credentialsService.findByEmail(new EmailAddress(username)); if (user != null) { return new UserDetailsWrapper(user); } } throw new UsernameNotFoundException("The user " + username + " was not found."); } }
Why is the custom authentication provider not authenticating the username and password ?
UPDATE: I read something interesting and puzzling in this guide
Note that the AuthenticationManagerBuilder is @Autowired into a method in a @Bean - that is what makes it build the global (parent) AuthenticationManager. In contrast if we had done it this way (using an @Override of a method in the configurer) then the AuthenticationManagerBuilder is only used to build a "local" AuthenticationManager, which is a child of the global one. In a Spring Boot application you can @Autowired the global one into another bean, but you can’t do that with the local one unless you explicitly expose it yourself.
So, is there anything wrong with my usage of the configure
method for setting up the authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider);
? Instead of the above configuration, I tried the following configuration:
@Autowired public void initialize(AuthenticationManagerBuilder authenticationManagerBuilder) { authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider); }
But it still didn't exercise the custom authentication provider upon a request.
I also tried to have the filter after as in:
http.addFilterAfter(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class);
instead of addFilterBefore
but it didn't change anything to the issue.
2 Answers
Answers 1
According to me when we implement our own ApplicationFilter by implementing GenericFilterBean we need to check if the token received from the request is valid or not. If it is not valid then we need to dump the token into the security context (for the authentication-provider to pick up). I haven't gone through your filter class. But this worked for me :
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httprequset=(HttpServletRequest)request; String uname=request.getParameter("username"); String pwd=request.getParameter("password"); String role=request.getParameter("role"); List<GrantedAuthority> l = new ArrayList<>(); l.add( new SimpleGrantedAuthority(role.toUpperCase()) ); UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(uname,pwd,l); token.setAuthenticated(false); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(httprequset, response); }
Answers 2
In WebSecurityConfiguration inside configure(HttpSecurity http) method:
http.authorizeRequests().antMatchers("/api/users/login").permitAll(); http.authorizeRequests().anyRequest().authenticated();
Add in the same order.
Explanation: Login and logout requests should be permitted without any authentication
A sample configure method that works is:
http.formLogin().disable().logout().disable().httpBasic().disable(); http.authorizeRequests().antMatchers("/logout", "/login", "/").permitAll(); http.authorizeRequests().anyRequest().authenticated(); http.addFilterBefore(new SomeFilter(), SecurityContextHolderAwareRequestFilter.class); http.addFilterBefore(new CORSFilter(env), ChannelProcessingFilter.class); http.addFilterBefore(new XSSFilter(),CORSFilter.class);
0 comments:
Post a Comment