Spring Security: Custom UserDetailsService not being called (using Auth0 authentication)
Looking at your Adapter code you are generating JWT token in configure itself. Am not sure whats apiAudience, issuer but it generated sub of JWT I guess. Your issue is that you want to change JWT sub as per your Database.
I have recently implemented JWT security in Spring Boot Application.
And I am setting UserName after fetching it from Database.
I have added code with pkg info for clarity.
// My adapter class. Its same as your's except one thing that I have added a Filter to it. In this Filter I am authenticating JWT token. This filter will be called each time a Secured Rest URL is fired.
import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import com.dev.myapp.jwt.model.CustomUserDetailsService;
import com.dev.myapp.security.RestAuthenticationEntryPoint;
import com.dev.myapp.security.TokenAuthenticationFilter;
import com.dev.myapp.security.TokenHelper;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private CustomUserDetailsService jwtUserDetailsService; // Get UserDetail bu UserName
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint; // Handle any exception during Authentication
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// Binds User service for User and Password Query from Database with Password Encryption
@Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( jwtUserDetailsService )
.passwordEncoder( passwordEncoder() );
}
@Autowired
TokenHelper tokenHelper; // Contains method for JWT key Generation, Validation and many more...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
.exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);
http.csrf().disable();
}
// Patterns to ignore from JWT security check
@Override
public void configure(WebSecurity web) throws Exception {
// TokenAuthenticationFilter will ignore below paths
web.ignoring().antMatchers(
HttpMethod.POST,
"/auth/login"
);
web.ignoring().antMatchers(
HttpMethod.GET,
"/",
"/assets/**",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
);
}
}
// User Service to get User Details
@Transactional
@Repository
public class CustomUserDetailsService implements UserDetailsService {
protected final Log LOGGER = LogFactory.getLog(getClass());
@Autowired
private UserRepo userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User uu = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
} else {
return user;
}
}
}
// Unauthorized access handler
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
// Filter Chain for Validating JWT Token
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;
public class TokenAuthenticationFilter extends OncePerRequestFilter {
protected final Log logger = LogFactory.getLog(getClass());
private TokenHelper tokenHelper;
private UserDetailsService userDetailsService;
public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
this.tokenHelper = tokenHelper;
this.userDetailsService = userDetailsService;
}
@Override
public void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
String username;
String authToken = tokenHelper.getToken(request);
logger.info("AuthToken: "+authToken);
if (authToken != null) {
// get username from token
username = tokenHelper.getUsernameFromToken(authToken);
logger.info("UserName: "+username);
if (username != null) {
// get user
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenHelper.validateToken(authToken, userDetails)) {
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // Adding Token in Security COntext
}
}else{
logger.error("Something is wrong with Token.");
}
}
chain.doFilter(request, response);
}
}
// TokenBasedAuthentication class
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
public class TokenBasedAuthentication extends AbstractAuthenticationToken {
private static final long serialVersionUID = -8448265604081678951L;
private String token;
private final UserDetails principle;
public TokenBasedAuthentication( UserDetails principle ) {
super( principle.getAuthorities() );
this.principle = principle;
}
public String getToken() {
return token;
}
public void setToken( String token ) {
this.token = token;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public Object getCredentials() {
return token;
}
@Override
public UserDetails getPrincipal() {
return principle;
}
}
// Helper class for JWT generation and Validation Logic
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.dev.myapp.common.TimeProvider;
import com.dev.myapp.entity.User;
@Component
public class TokenHelper {
protected final Log LOGGER = LogFactory.getLog(getClass());
@Value("${app.name}") // reading details from property file added in Class path
private String APP_NAME;
@Value("${jwt.secret}")
public String SECRET;
@Value("${jwt.licenseSecret}")
public String LICENSE_SECRET;
@Value("${jwt.expires_in}")
private int EXPIRES_IN;
@Value("${jwt.mobile_expires_in}")
private int MOBILE_EXPIRES_IN;
@Value("${jwt.header}")
private String AUTH_HEADER;
@Autowired
TimeProvider timeProvider; // return current time. Basically Deployment time.
private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;
// Generate Token based on UserName. You can Customize this
public String generateToken(String username) {
String audience = generateAudience();
return Jwts.builder()
.setIssuer( APP_NAME )
.setSubject(username)
.setAudience(audience)
.setIssuedAt(timeProvider.now())
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
return (
username != null &&
username.equals(userDetails.getUsername())
);
}
// If Token is valid will extract all claim else throw appropriate error
private Claims getAllClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.error("Could not get all claims Token from passed token");
claims = null;
}
return claims;
}
private Date generateExpirationDate() {
long expiresIn = EXPIRES_IN;
return new Date(timeProvider.now().getTime() + expiresIn * 1000);
}
}
For this Log
No authentication manager set. Reauthentication of users when changing passwords
Since you have not implemented a methods with Name loadUserByUsername. You are getting this log.
Edit 1:
I am using Filter Chain just to Validate Token and add User in Security Context which will be extracted from Token....
Am using JWT and you are using AuthO, only Implementation is Different. Am added full implementation for a complete work flow.
You focus on implementing authenticationManagerBean and configureGlobal from WebSecurityConfig class to use UserService.
and TokenBasedAuthentication class implementation.
Other things you can skip.
Maybe this is an spring-boot context initialization issue, meaning the @Autowired
annotation cannot be resolved during the initialization of the Configuration class.
You could try the @ComponentScan()
annotation on top of your Configuration class and load your MyUserDetailsService
explicitly. (see: https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html#using-boot-importing-configuration). Having done this I would recommend the following in your Configuration class:
@Autowired
private MyUserDetailsService userService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
Hope this can help you out.
I ended up asking Auth0 support about this, and they say that it's currently not possible to modify the principal without modifying the library source.
They provide an alternative approach, however, which is to use a JWT validation library (e.g. https://github.com/auth0/java-jwt) instead of their Spring Security API SDK.
My solution will be to modify my code to work with just the token as principal.