In order to protect my Rest API endpoints, I implemented Spring Security using JWT authentication. My code "works" without any issues/exceptions but it would be great if I could get my implementation validated to ensure it is implemented as expected.
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class WebSecurityConfig {
private final CustomAuthenticationManager customAuthenticationManager;
private final AuthTokenFilter authTokenFilter;
private final AuthEntryPoint authEntryPoint;
#Value("${api.prefix}")
private String apiPrefix;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(null).and()
.authenticationManager(customAuthenticationManager)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(apiPrefix + "/auth/**").permitAll()
.antMatchers(apiPrefix + "/test/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling(
httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer
.authenticationEntryPoint(authEntryPoint)
);
http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
AuthTokenFilter.java
#Slf4j
#Component
#RequiredArgsConstructor
public class AuthTokenFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDAO userDAO;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = parseJwt(request);
if (Objects.isNull(jwt)) {
throw new AuthenticationCredentialsNotFoundException("Unable to extract JWT token from authentication header");
}
try {
if (jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDAO.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.authenticated(userDetails, null, new ArrayList<>()); // please check this line
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
} catch (AuthenticationException e) {
throw e;
} catch (Exception e) {
log.error("Cannot set user authentication: {}", e.getMessage(), e);
throw new CustomRTException("Error while validating jwt token", HttpStatus.UNAUTHORIZED);
}
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
CustomAuthenticationManager.java
#Slf4j
#Component
#RequiredArgsConstructor
public class CustomAuthenticationManager implements AuthenticationManager {
private final UserDAO userDAO;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UserDetails userDetails = userDAO.loadUserByUsername(authentication.getName());
if (passwordEncoder().matches(authentication.getCredentials().toString(), userDetails.getPassword())) {
throw new BadCredentialsException("Wrong Password");
}
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword());
}
}
(If any other classes such as UserDAO or AuthEntryPoint is required, let me know. I just shared the classes that I thought were relevant since others were fairly straightforward)
One of the biggest issues that I had was, in AuthTokenFilter even after validating the JWT token, Spring tries to call CustomAuthenticationManager#authenticate again which caused a NullPointerException at this line
authentication.getCredentials().toString() // credentials is null because after parsing the JWT token, I am unable to get the password
This issue was fixed in this line
UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.authenticated(userDetails, null, new ArrayList<>());
(authorities is empty list as I am currently do not need to grant any)
This code was pieced together following some migration guides from the now deprecated WebSecurityConfigurerAdapter therefore I just want to make sure everything is correct.
Related
my problem is that I get nothing in response after sending requests with wrong login credentials or when accessing the endpoint without authentication.
This is how it looks when I send a good request:
Good Response screen
And this is how it looks when the request body is invalid:
Invalid credentials request
MyUserDetails class:
public class MyUserDetailService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User("foo", "foo", List.of());
}
}
Security configure class:
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
#Autowired
private MyUserDetailService myUserDetailService;
#Autowired
private JwtFilter jwtFilter;
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/auth").permitAll()
.anyRequest().authenticated()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable();
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Jwt Filter class:
#Component
public class JwtFilter extends OncePerRequestFilter {
#Autowired
private JwtUtil jwtUtil;
#Autowired
private MyUserDetailService myUserDetailService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String requestToken = null;
if(authHeader != null && authHeader.startsWith("Bearer ")) {
requestToken = authHeader.substring(7);
username = jwtUtil.getUsernameFromToken(requestToken);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = myUserDetailService.loadUserByUsername(username);
if(jwtUtil.validateToken(requestToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
And finally my controller:
#PostMapping("/auth")
public AuthResponse auth(#RequestBody AuthRequest authRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
);
} catch (BadCredentialsException e) {
throw new Exception("Invalid Credentials", e);
}
final UserDetails userDetails = myUserDetailService.loadUserByUsername(authRequest.getUsername());
final String token = jwtUtil.generateToken(userDetails);
return new AuthResponse(token);
}
EDIT:
I forgot to paste dependancies.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.2.4'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
I also have AuthRequest and AuthResponse class which holds username, password, jwt token and all the constructors, getters and setters ( not worth to past it here imo)
You got to throw an exception while validating(following is what I use, just grab the concept, your classes and libraries might be different):
Jwts.parser().setSigningKey(your_jwt_secret).parseClaimsJws(token);
so if your token cannot get parsed throw this and send an error response:
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write("your_custom_error");
httpServletResponse.getWriter().flush();
The issue is with the app uses custom JWT authentication filter which extends UsernamePasswordAuthenticationFilter which accepts user credentials and generates a long-lived JWT in return.
The issue seems to be with permitAll() which should bypass custom Authorization filter.However in debug mode I could see call to custom JwtAuthorizationFilter first instead of custom JwtAuthenticationFilter Filter which eventually results with 403 forbidden Access denied response.
Note the .antMatchers(HttpMethod.POST, "/login").permitAll() line. /login endpoint should be accessible without JWT since the JWT has not yet been generated when the user has not yet logged in.
Below is my code
JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
//
private AuthenticationManager authenticationManager;
private final static UrlPathHelper urlPathHelper = new UrlPathHelper();
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl("/login");
}
/**
* Trigger when we issue POST request to login / we also need to pass in
* {"username: " username, "password": password} in the request body
*/
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// Grab credentials and map them to login viewmodel
LoginViewModel credentials = null;
try {
credentials = new ObjectMapper().readValue(request.getInputStream(), LoginViewModel.class);
} catch (IOException e) {
e.printStackTrace();
}
// Create login token
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
credentials.getUsername(), credentials.getPassword(), new ArrayList<>());
// Authenciate user
Authentication auth = authenticationManager.authenticate(authenticationToken);
return auth;
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// Grab principal
UserPrincipal principal = (UserPrincipal) authResult.getPrincipal();
// Create JWT Token
String token = JWT.create().withSubject(principal.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
.sign(HMAC512(JwtProperties.SECRET.getBytes()));
// add token in response
response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + token);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
logger.debug("failed authentication while attempting to access "
+ urlPathHelper.getPathWithinApplication((HttpServletRequest) request));
// Add more descriptive message
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed");
}
}
JwtAuthorizationFilter.java
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private UserRepository userRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
super(authenticationManager);
this.userRepository = userRepository;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//Read the Authorization header, where the JWT token should be
String header = request.getHeader(JwtProperties.HEADER_STRING);
//If header does not contain BEARER or is null delegate to Spring impl and exit
if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// If header is present, try grab user principal from db and perform authorization
Authentication authentication = getUsernamePasswordAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
// Continue filter execution
chain.doFilter(request, response);
}
private Authentication getUsernamePasswordAuthentication(HttpServletRequest request){
String token = request.getHeader(JwtProperties.HEADER_STRING)
.replace(JwtProperties.TOKEN_PREFIX, "");
if(token !=null){
//parse the token validate it
String userName = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()))
.build()
.verify(token)
.getSubject();
// Search in the DB if we find the user by token subject(username)
// If so, then grab user details and create auth token using username, pass, authorities/roles
if(userName != null){
User user = userRepository.findByUsername(userName);
UserPrincipal principal = new UserPrincipal(user);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, null, principal.getAuthorities());
return authenticationToken;
}
return null;
}
return null;
}
}
SecurityConfiguration.java
Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private UserPrincipalDetailsService userPrincipalDetailsService;
private UserRepository userRepository;
public SecurityConfiguration(UserPrincipalDetailsService userPrincipalDetailsService,
UserRepository userRepository) {
this.userPrincipalDetailsService = userPrincipalDetailsService;
this.userRepository = userRepository;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// remove csrf state in session because in jwt do not need them
.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers("/api/public/management/*").hasRole("MANAGER").antMatchers("/api/public/admin/*")
.hasRole("ADMIN").anyRequest().authenticated().and()
// add jwt filters (1. authentication, 2. authorization_)
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), this.userRepository));
// configure access rules
}
#Bean
DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService((UserDetailsService) this.userPrincipalDetailsService);
return daoAuthenticationProvider;
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Request,Response
Can someone suggest whats wrong here..Appreciate your help..Thanks in advance..!!!
It seems that your path is wrong. When you look at your body you can see that the path shows following: /login%0A. This seems that you have an extra character at the end of your URL. Just try to rewrite the URL in Postman.
please consider to use shouldNotFilter method from BasicAuthenticationFilter. It extends OncePerRequestFilter so you can use it in filtering class as below:
#Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// code here
}
I have a project which I had enabled jwt for authorization on it. The problem is that whenever I send an empty header request or expired authorization code in the header it doesn't send me the unauthorized error, it shows in the log that the token is not valid but allows the request to continue working. this is my configuration code:
#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(BCryptVersion.$2Y);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests().antMatchers("/authenticate","/user","/swagger-ui.html","/swagger-ui/**"
,"/v3/api-docs/**").permitAll().
anyRequest().authenticated().and().
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.logout().logoutSuccessUrl("/authenticate").logoutUrl("/logout").permitAll();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers(String.valueOf(HttpMethod.OPTIONS), "/**");
// ignore swagger
web.ignoring().mvcMatchers("/swagger-ui.html/**", "/configuration/**", "/swagger-resources/**", "/v2/api-docs");
}
}
and this is my jwt request filter:
#Component
public class JwtRequestFilter extends OncePerRequestFilter {
#Autowired
private JwtUserDetailsService jwtUserDetailsService;
private JwtTokenUtil jwtTokenUtil;
public JwtRequestFilter(JwtTokenUtil jwtTokenUtil) {
this.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 if (requestTokenHeader == null){
logger.info("Does not provide Authorization Header");
}
else if (!requestTokenHeader.startsWith("Bearer ")){
logger.warn("JWT Token does not begin with Bearer");
}
// 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);
}
}
and finally here is JwtAuthenticationEntryPoint:
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -7858869558953243875L;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
and this is the log that shows I did not send any token in header, but it allows the request:
any idea that what should I do?
for further information, I should say that this code was working but stopped working after a while and I didn't find any reason because I had no changes in these files for months.
The problem is the misconfiguration at this line
web.ignoring().mvcMatchers(String.valueOf(HttpMethod.OPTIONS), "/**");
It should be
web.ignoring().mvcMatchers(HttpMethod.OPTIONS, "/**");
As you might have noticed now that your configuration is actually ignoring all request paths from Spring Security filters. That is the reason that all unauthorized requests (which you expect) are allowed now.
You are missing addFilterAfter and also update your code as below.
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests().antMatchers("/authenticate","/user","/swagger-ui.html","/swagger-ui/**"
,"/v3/api-docs/**").permitAll().
anyRequest().authenticated().and().
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class).logout().logoutSuccessUrl("/authenticate").logoutUrl("/logout").permitAll();
}
Please refer https://github.com/techiesantosh/taskmanager-service/blob/develop/src/main/java/com/web/taskmanager/config/TaskConfig.java
I'm making a spring boot webserver which has spring security and jwt for user authentication/authorization via username and password. But seems like spring recognize /api/users/signup and /api/users/signin
as must-be-authenticated URL.
UserController.java:
#PostMapping("/signin")
public ResponseEntity<String> login(#ApiParam("Username") #RequestParam String username, //
#ApiParam("Password") #RequestParam String password) {
return ResponseEntity.ok(userService.signin(username, password));
}
#PostMapping("/signup")
public void signUp(#ApiParam("SignUp User") #RequestBody SignUpRequest request) {
User user = User.of(request.getUsername(), bCryptPasswordEncoder.encode(request.getPassword()), request.getEmail());
Role userRole = roleRepository.findByName(RoleName.ROLE_MEMBER).orElse(null);
user.setRoles(Collections.singleton(userRole));
userRepository.save(user);
}
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
public WebSecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// Disable CSRF (cross site request forgery)
http.csrf().disable();
// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Entry points
http.authorizeRequests()//
.antMatchers("/api/users/signin").permitAll()//
.antMatchers("/api/users/signup").permitAll()//
.antMatchers("/api/test/**").permitAll()
.antMatchers("/h2-console/**/**").permitAll()
// Disallow everything else..
.anyRequest().authenticated();
// If a user try to access a resource without having enough permissions
http.exceptionHandling().accessDeniedPage("/login");
// Apply JWT
http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider));
http.cors().disable();
// Optional, if you want to test the API from a browser
// http.httpBasic();
super.configure(http);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
JwtTokenFilter.java:
public class JwtTokenFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(httpServletRequest);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (CustomException ex) {
//this is very important, since it guarantees the user is not authenticated at all
SecurityContextHolder.clearContext();
httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage());
return;
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
MyUserDetailsService.java:
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final User user = userRepository.findByUsername(username).orElseThrow(() -> new CustomException("User doesn't exist", HttpStatus.NOT_FOUND));
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority(role.getName().getAuthority())
).collect(Collectors.toList());
if (user == null) {
throw new UsernameNotFoundException("User '" + username + "' not found");
}
return org.springframework.security.core.userdetails.User//
.withUsername(username)
.password(user.getPassword())
.authorities(authorities)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
}
}
When I request to both of these links as I told above. It's done quickly by giving me 401 HTTP error code while testing on postman.
Both this link and this link are not helpful at all.
You might want to try excluding these URLs from the WebSecurity section, instead, so that they do not get processed by Spring Security and your JwtTokenFilter at all.
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(final WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/api/users/signin").antMatchers("/api/users/signup");
}
}
I'm working with Spring Security 2.0.7. It had been implemented the UserDetailsService with a preAuthenticatedUserDetailsService bean.
It's working fine. Now I want to add a new custom error messages.
In the method loadUserByUsername I want to add some custom bussines logic.
For ex. based on some attribute, I don't want the user to log in so I throw a UsernameNotFoundException with a custom message.
Spring is the one who handdle the exception and set it to the session, but when I retrive the exception from the session with "SPRING_SECURITY_LAST_EXCEPTION" key I get a "Bad credentials" message.
At the moment is fixed with a nasty workarround, mostly I want to understand what happend!
Ideas?
Ps. I read a lot of this issue here in SO but mostly all with Spring security 3.0
Just to close it. As #M. Deinum mentioned in the comments. Spring handle this way for security purposes. The implementation can be seen in the ExceptionTranslationFilter class.
That problem was nagging me for a while before finding an efficient & easy solution.
The issue was resolved by using a custom AuthenticationFailureHandler (that is added to HttpSecurity configuration) to get the custom error message from UserDetailsService before sending it back to login page as error parameter
The custom AuthenticationFailureHandler is defined like following:
#Component("CustomAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String encodedErrorMessage = null;
// Get AuthenticationException that was thrown in UserDetailsService, retrieve the error message and attach it as encoded error parameter of the error login page
if (exception != null) {
String errorMessage = exception.getMessage();
encodedErrorMessage = Base64.getUrlEncoder().encodeToString(errorMessage.getBytes());
}
redirectStrategy.sendRedirect(request, response, "/login?error=" + encodedErrorMessage);
}
}
Then the CustomAuthenticationFailureHandler is configured in WebSecurityConfigurerAdapter (also the configureGlobal is updated) as following:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and().formLogin().loginPage("/login").permitAll()
.failureHandler(customAuthenticationFailureHandler) // Add failure handler class
// ...
// ...
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider impl = new DaoAuthenticationProvider();
impl.setUserDetailsService(userDetailsService);
impl.setPasswordEncoder(bCryptPasswordEncoder());
// setHideUserNotFoundExceptions is set to false in order to get the exceptions CustomAuthenticationFailureHandler
impl.setHideUserNotFoundExceptions(false);
return impl;
}
}
The exceptions related to user account (UsernameNotFoundException) are always thrown in UserDetailsService interface:
#Service
public class UserDetailsServiceImpl implements UserDetailsService{
private UserDetails getUserByUsername(String username) {
UserDTO userDTO = userService.findByUsername(username);
if (userDTO == null) {
// Custom error message when no account was found by the given username
throw new UsernameNotFoundException("No user account was found by the username [" + username + "]");
}
Date expirationDate = userDTO.getExpirationDate();
if(expirationDate != null) {
if(expirationDate.before(new Date())) {
// Custom error message when the account is expired
throw new UsernameNotFoundException("The user account [" + username + "] is expired");
}
}
// Can add more UsernameNotFoundException with custom messages based on functional requirements
// ...
List<GrantedAuthority> grantedAuthorities = getGrantedAutorities(userDTO);
return new org.springframework.security.core.userdetails.User(userDTO.getUsername(), userDTO.getPassword(), grantedAuthorities);
}
}
The exceptions are handled in CustomAuthenticationFailureHandler in order to send the custom error messages (encoded using Base64) like following :
#Component("CustomAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String encodedErrorMessage = null;
// Get AuthenticationException that was thrown in UserDetailsService, retrieve the error message and attach it as encoded error parameter of the error login page
if (exception != null) {
String errorMessage = exception.getMessage();
encodedErrorMessage = Base64.getUrlEncoder().encodeToString(errorMessage.getBytes());
}
redirectStrategy.sendRedirect(request, response, "/login?error=" + encodedErrorMessage);
}
}
Then the error message is retrieved in the Controller like following:
#GetMapping(value = {"/login", "/"})
public String login(Model model, #RequestParam(name="error", required = false) String error) {
if (error != null) {
byte[] decodedBytes = Base64.getDecoder().decode(error);
String decodedString = new String(decodedBytes);
model.addAttribute("error", decodedString);
}
return "login";
}