I inherited a half-written Spring Boot REST service that is using Spring Sec to implement JWT-based API authentication. Gradle security-related dependencies are:
'org.springframework.security:spring-security-jwt:1.0.9.RELEASE'
'org.springframework.security.oauth:spring-security-oauth2:2.2.1.RELEASE'
'io.jsonwebtoken:jjwt:0.9.0'
'org.springframework.boot:spring-boot-starter-security'
This app uses Spring Sec filters to implement the entire auth solution, and I'm trying to wrap my head around how it works, and for the life of me can't make sense of a few critical things :-/
Here's the code:
public class MyAppAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public MyAppAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
ApplicationUser creds = new ObjectMapper()
.readValue(req.getInputStream(), ApplicationUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
public class MyAppAuthorizationFilter extends BasicAuthenticationFilter {
public MyAppAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
#Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = Jwts.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
#Component
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private AccountDAO accountDAO;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountDAO.findByUsername(username);
if(account == null) {
throw new UsernameNotFoundException(username);
}
return new User(account.username, account.password, []);
}
}
What I'm not understanding is:
Can I assume that Spring Security automagically positions these filters in the correct sequence? That is: the MyAppAuthenticationFilter always gets called before the MyAppAuthorizationFilter?
I'm really confused by the authenticationManager.authenticate(...) call inside MyAppAuthenticationFilter#attemptAuthentication. How are creds.getUsename() and cred.getPassword() compared to user information stored in a database (or LDAP or anywhere else)? How does this mechanism relate to UserDetailsServiceImpl#loadByUsername(String)?
All of the logic in MyAppAuthorizationFilter#doFilterInternal doesn't make sense to me. To me, I read it as: check to see if there is a JWT token header on the request. If there isn't, then go ahead and make the request any way (!!!!). If there is, then go ahead and check that the JWT has a valid user as its subject. Shouldn't we be blocking the request if there's no JWT header on the request?
Related
I cant get this cookie verification filter to work in springboot. I can also see the cookie in postman but not browser.
I generate the cookie in the following:
#PostMapping("/signin")
public ResponseEntity<?> authenticateUser(#RequestBody User loginRequest) {
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); // gets error here
SecurityContextHolder.getContext().setAuthentication(authentication);
MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);
List<String> roles = userDetails.getAuthorities().stream().map(item -> item.getAuthority()).collect(Collectors.toList());
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.body(userService.findUserProfileUserByEmail(userDetails.getEmail()));
}
When a request is sent to a restricted access endpoint, it will be run through this filter in the SecurityConfiguration
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
This is the class:
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private MyUserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String email = jwtUtils.getEmailFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
System.out.println(SecurityContextHolder.getContext());
SecurityContextHolder.getContext().setAuthentication(authentication); // throws error here
System.out.println("a");
}
}
catch (Exception e) { logger.error("Cannot set user authentication: {}", e);
System.out.println(e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) { return jwtUtils.getJwtFromCookies(request); }
}
Here on line String jwt = parseJwt(request), it will always equal null.
I was told this may be an issue with the actual request itself, that it should contain {withCredentials: true} in Axios, though doing this raises other issues, and does not explain why this cookie exists and is visible in Postman.
After updating my spring boot from 2.0.3 to 2.2.1 somehow my Spring Security configuration stop working.
setup is as follow - I would like to have all my request processed by request filter where JWT token is validated and UserDetails are created. So config is pretty easy:
#Configuration
#EnableWebSecurity
public class NoAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated();
}
}
and filter class
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class JwtRequestFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(JwtRequestFilter.class);
#Autowired
private JwtUserDetailsService jwtUserDetailsService;
#Autowired
private JwtUtils jwtTokenUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String requestTokenHeader = "";
try{
requestTokenHeader = WebUtils.getCookie(request, "token").getValue();
} catch (NullPointerException ex ){}
if (requestTokenHeader != null && requestTokenHeader.contains(".")) {
jwtToken = requestTokenHeader;
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
log.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
log.error("JWT Token has expired");
}
}
if (username != null ) {
UserDetails userDetails = null;
userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
chain.doFilter(request, response);
}
#Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String[] AUTH_WHITELIST = {
"/v2/auth/",
"/api/packetdiscovery"
};
String path = request.getServletPath();
return (StringUtils.startsWithAny(path, AUTH_WHITELIST));
}
}
and by debugging I can see that everything work smoothly until request reach Controller where NullPointerException Is thrown on Principal
#PreAuthorize("hasAuthority('ROLE_USER')")
#GetMapping(value = "/userinfo")
public ResponseEntity<SessionOwner> getSesstionOwner(Principal user) {
return dashboardService.getSessionOwner(user.getName());
}
Could anyone give me advice on how to handle it?
I have a spring boot and spring security service.
I have extended WebSecurityConfigurerAdapter class and overridden configure method. But somehow it is not filtering correct request.
My url is something like -
localhost:8080/album/private/v1/getAlbumsByVendorId?vendorId=1
localhost:8080/vendor/private/v1/getVendor?vendorId=1
and also I have some URL which I do not want to authenticate.like below url.
localhost:8080/category/v1/getCategory
Only want to authenticate if the URL contains private.
But seems like my filter is getting invoked for all request.
is there something wrong in .antMatchers("/**/private/**")
Note - I don't have any context path as of now.
Added the classes.
Controller is just a dummy test controller.
#Configuration
#EnableWebSecurity
//#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtRequestFilter jwtRequestFilter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.cors().disable()
.authorizeRequests()
.antMatchers("/authenticate").permitAll()
.antMatchers("/**/private/**").authenticated()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);
}
}
#Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private JwtUserDetailsService jwtUserDetailsService;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
//Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
// if token is valid configure Spring Security to manually set authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the Spring Security Configurations successfully.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
#RestController
#CrossOrigin()
public class HelloWorldController {
#RequestMapping({ "/hello" })
public String hello() {
return "Hello World";
}
#RequestMapping({ "/private/test" })
public String hello2() {
return "Hello World-test";
}
#RequestMapping({ "/v1/private/test" })
public String hello3() {
return "Hello World-test-v1";
}
#RequestMapping({ "/v1/public/test" })
public String hello4() {
return "Hello World-test-v1-public";
}
}
By default, Spring Boot will secure all endpoints when Spring Security is on the classpath. We need to explicitly add an exclusion for all other endpoints to be permitted without authentication. Consider change is .anyRequest().permitAll(),
which means each request other than /**/private/** will be accessible to everyone. In other words, the filter will only apply to /**/private/**
Git Link
approach 1 (clean way)
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().disable()
.authorizeRequests()
.antMatchers("/authenticate").permitAll()
.antMatchers("/**/private/**").authenticated()
.anyRequest().permitAll()
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);
}
approach 2: only check for a token if Request comes from /private/ (not an ideal way)
JwtAuthenticationEntryPoint.java
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
System.out.println("Entry Request: "+request.getRequestURI());
System.out.println("Entry Contain: "+request.getRequestURI().contains("private"));
if(request.getRequestURI().contains("private")==true)
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
JwtRequestFilter.java
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
System.out.println("JWT Request: "+request.getRequestURI());
System.out.println("JWT Contain: "+request.getRequestURI().contains("private"));
String username = null;
String jwtToken = null;
//Remove comment for second approach
if(request.getRequestURI().contains("private")==false)
{
System.out.println("Do Noting, Permit It");
chain.doFilter(request, response);
}
else if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ") ) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
http://localhost:8080/v1/private/test **401**
http://localhost:8080/v1/public/test **200**
I am implementing a login page using Angular 7 and Spring Boot and I am with an issued processing a failed login. Basically I want to lock for a specific amount of time the login after X login attempt failures.
HttpSecurity configuration
#Override
protected void configure(HttpSecurity http) throws Exception {
logger.info("#### Configuring Security ###");
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/rest/users/authenticate");//this override the default relative url for login: /login
http
.httpBasic().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/rest/", "/rest/helloworld/**").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint()).and()
.addFilter(jwtAuthenticationFilter);
To process the login i created a Filter
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static Logger logger = Logger.getLogger(JWTAuthenticationFilter.class);
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserDto credentials = new ObjectMapper().readValue((request.getInputStream()), UserDto.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
credentials.getUserName(),
credentials.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//sucessfull authentication stuff
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
logger.info("Authentication failed");
ErrorMessage errorMessage = new ErrorMessage("access_denied", "Wrong email or password.");
String jsonObject = JSONUtil.toJson(errorMessage);
//processing authentication failed attempt
UserDto credentials = new ObjectMapper().readValue((request.getInputStream()), UserDto.class);
AuthenticationService authenticationService = Application.getApplicationContext().getBean(AuthenticationService.class);
int numFailedAttemptLogin = authenticationService.authenticationFailedAttempt(credentials.getUserName());
response.setStatus(403);
PrintWriter out = response.getWriter();
out.print(jsonObject);
out.flush();
out.close();
//super.unsuccessfulAuthentication(request, response, failed);
}
}
The login is working fine with no issues. My problem is with the unsuccessfulAuthentication method. When the user enters bad credentials, a BadCredentials exception is raised and unsuccessfulAuthenticationmethod is call. Here i need to access again to the request form to extract the username and process the authentication failed attempt and I am getting the following exception
java.io.IOException: Stream closed
This is because inside the attemptAuthentication method the request inputstream is read and obviously closed.
How can i access request body information inside the unsuccessfulAuthentication?
I tried SecurityContextHolder.getContext().getAuthentication() but it is null due the authentication failure.
Does anyone have any idea?
Best Regards
After following M.Deinum suggestion i was able to create a component that listens specific Exceptions:
#Component
public class AuthenticationEventListener implements ApplicationListener<ApplicationEvent> {
private static Logger logger = Logger.getLogger(AuthenticationEventListener.class);
#Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
logger.info(String.format("Event types: %s", applicationEvent.getClass()));
if (applicationEvent instanceof AbstractAuthenticationFailureEvent) {
String username = ((AbstractAuthenticationFailureEvent) applicationEvent).getAuthentication().getName();
if (applicationEvent instanceof AuthenticationFailureBadCredentialsEvent) {
logger.info(String.format("User %s failed to login", username));
//this.handleFailureEvent(username, event.getTimestamp());
}
}
}
}
This approach is using Exceptions to drive what to do in specific scenarios. I was able to achieve something similar keep using my JWTAuthenticationFilter like this
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserDto credentials = new ObjectMapper().readValue((request.getInputStream()), UserDto.class);
try {
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
credentials.getUserName(),
credentials.getPassword(),
new ArrayList<>())
);
} catch (BadCredentialsException bce) {
try {
handleBadCredentials(credentials, response);
throw bce;
} catch (LockedException le) {
handleUserLocked(credentials, response);
throw le;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
logger.info("Authentication failed");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.getWriter().print(authException.getLocalizedMessage());
response.getWriter().flush();
}
Thak you all for your time and help, much appreciated.
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.
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);
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);
}