I have gone through the similar questions asked here, but none seems to tackle the problem I am facing right now.
I have actually implemented two different processes of jwt for spring boot, yet keep facing the same problem.
Here is the AuthenticationFilter that filters the request and performs the authentication. I have added log functions to narrate what part of the code does not get called. I have implemented a different approach and it still never gets called, and funnily enough, it gets called elsewhere, not just in the class that handles the authentication or the one called by the one that handles the authentication. I use Java 8, spring boot 2.6.5.
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired JwtUtils jwtUtils;
#Autowired
MyUserDetailsService userDetailsService;
private static final Logger log = LoggerFactory.getLogger(AuthController.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)) {
log.info(jwt + " first");
String username = jwtUtils.getUsernameFromJwtToken(jwt);
log.info(username + " second");
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
log.info(userDetails + " third");
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getAuthorities());
log.info(authToken + " fourth");
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception e) {
throw new IllegalArgumentException("Please log in with your correct details.");
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
the log with "first" and the one with second get printed out, but the "third" doesn't. This is because UserDetails userDetails = userDetailsService.loadUserByUsername(username); doesn't get called and the code doesn't throw an error. It just doesn't get called.
Here is the JwtUtils class
#Component
public class JwtUtils {
private static final Logger log = LoggerFactory.getLogger(AuthController.class);
#Value("${jwt.secret:testing123}")
String secret;
#Value("${jwt.length:3600000}")
long duration = 3600000;
public String generateJwtToken(Authentication auth) {
UserDetails userDetails = (UserDetails) auth.getPrincipal();
return Jwts.builder().setSubject(userDetails.getUsername())
.setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS512, secret)
.setExpiration(new Date((new Date()).getTime() + duration))
.compact();
}
public String getUsernameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token)
.getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
throw new IllegalArgumentException("Invalid JWT signature: {}");
}
}
Here is my CustomUserDetailsService
#Service
#RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(MyUserDetailsService.class);
#Autowired
EmployeeRepository employeeRepo;
String prefixRole = "ROLE_";
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Employee employee = employeeRepo.findByUsername(username);
if (employee == null) {
throw new UsernameNotFoundException("No such employee, bro.");
}
try {
return User.withUsername(employee.getUsername()).password(employee.getPassword())
.authorities(getAllRoles(employee.getRoles())).accountExpired(false)
.accountLocked(false).credentialsExpired(false).disabled(false).build();
} catch (RuntimeException e) {
throw new IllegalArgumentException("Something bad happened and I don't know what it is.");
}
}
private List<GrantedAuthority> getAllRoles(Collection<Role> roles) {
List<GrantedAuthority> collection = new ArrayList<>();
for (Role role: roles) {
collection.add(new SimpleGrantedAuthority(prefixRole + role.getName()));
}
return collection;
}
}
The entryPoint
#Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
#Override public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
What could be the problem?
Related
If I understand good, I have custom Authentication Manager class, and in there I check if someone in api pass correct credentials, but I wonder why it didn't throw exception while I passed empty username and password.
#Component
public class AuthManager implements AuthenticationManager {
private final DetailsService detailsService;
private final Logger logger = LoggerFactory.getLogger(AuthManager.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.info("credentials: " + authentication.getCredentials());
logger.info("principals: " + authentication.getPrincipal());
if (authentication.getCredentials() == null || authentication.getPrincipal() == null) {
throw new BadCredentialsException("Credentials are wrong");
}
UserDetails user = loadUser(authentication);
return new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
}
private UserDetails loadUser(Authentication auth) {
return detailsService.loadUserByUsername(auth.getPrincipal().toString());
}
That's filter
#Component
public class UsernamePasswordJsonFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public UsernamePasswordJsonFilter(ObjectMapper objectMapper, AuthManager manager,
AuthSuccessHandler success, AuthFailureHandler failure) {
this.objectMapper = objectMapper;
setAuthenticationSuccessHandler(success);
setAuthenticationFailureHandler(failure);
setAuthenticationManager(manager);
setFilterProcessesUrl("/login");
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginDTO authenticationRequest = objectMapper.readValue(request.getInputStream(), LoginDTO.class);
Authentication auth = new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(),
authenticationRequest.getPassword());
logger.info("UsernamePasswordJsonFilter");
return getAuthenticationManager().authenticate(auth);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
If I pass correct username and password it works, but I just wonder why it didn't throw exception when credentials are empty, also there is no exception thrown in console, in case someone asks for it
When you use
LoginDTO authenticationRequest = objectMapper.readValue(request.getInputStream(), LoginDTO.class);
It return LoginDTO with login = "" and password = "". It is not a null.
"" is not the same as null. If u want to be null in LoginDTO, request should not have username or password field at all
if (authentication.getCredentials() == null || authentication.getPrincipal() == null)
Here you also to check to empty string. Let's say "".equals(authentication.getCredentials()) or authentication.getCredentials().isEmpty()
same with authentication.getPrincipal()
The User login is working well but I want to add a Customer Module to the project. I know that I need to write a custom UserDetails class to get the customer Username but I want to ask if I need to write another Custom JWT filter for the Customer Login validation. Presently this is the Filter class that I have for User Login. I have added a username and password field to the Customer entity.
#Component
public class JwtRequestFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private UserAccountService myUserDetailsService;
#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;
if (requestTokenHeader != null) {
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");
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
String authorities = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority)
.collect(Collectors.joining());
System.out.println("Authorities granted : " + authorities);
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
else {
System.out.println("Not Valid Token");
}
}
chain.doFilter(request, response);
}
}
As you can see the Filter is using the custom UserDetails to verify the username . How do I add the Customer userdetails service to the filter ? This is my first multiple login project please be lenient with me.
Differentiate between user and customer while logging. Accordingly, call the different service to get user details. More can be found here.
Spring Security user authentication against customers and employee
How do I add the Customer userdetails service to the filter?: inject it as you did with UserAccountService. If you do this way, you're using 1 filter (and of course, this filter is in 1 SecurityFilterChain), you could basically implement your filter like: trying to validate your user by myUserDetailsService and if it's not successful, continue with myCustomerDetailsService.
For multiple login project. The second way you could do is using 2 SecurityFilterChain. UserJwtFilter for 1 SecurityFilterChain and CustomJwtFilter for 1 SecurityFilterChain for example. People usually do this way for different login mechanisms Basic, OAuth2, SAML2. E.g:
Basic Authentication:
#Configuration
#Order(2)
public class BasicAuthenticationFilterChain extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/login", "/logout")
.and()
OAuth2 Authentication:
#Configuration
#Order(3)
public class OAuth2AuthenticationFilterChain extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/oauth")
.and()
In this case when a request with "/login" it'll be directed to BasicAuthenticationFilterChain, and a request with "/oauth" will go to OAuth2AuthenticationFilterChain. About Order: the lower is the higher priority and once the request's processed with a SecurityFilterChain, it won't go to another SecurityFilterChain. You can implement your project this way.
Conclusion: There are a lot of ways you can implement your idea with spring security, it depends on your choice.
it looks to me like you already did.
#Autowired
private UserAccountService myUserDetailsService;
But I would suggest using a Constructor instead of #Autowired. Spring will fill in the constructor parameters just the same. This could be very slim when you use the lombok library as well.
Using a constructor also makes mocking this a bit easier for testing.
Updated as discussed in the comments:
#Log //another lombok thing
#RequiredArgsConstructor
#Component
public class JwtRequestFilter extends Filter{
private final JwtTokenUtil jwtTokenUtil;
private final UserAccountService myUserDetailsService;
private final CustomerAccountService myCustomerDetailsService;
private static final String AUTH_HEADER = "authorization";
#Override
protected void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
String tokenHeader = ((HttpServletRequest) request).getHeader(AUTH_HEADER);
if(hasValue(tokenHeader) && tokenHeader.toLowerCase().startsWith("bearer ")){
jwtToken = requestTokenHeader.substring(7);
String username;
String jwtToken;
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
if (uSecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
if(isNull(userDetails)){
userDetails = myCustomerDetailsService.loadCustomerByUsername(username);
}
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
var token = createSecurityToken(userDetails);
SecurityContextHolder.getContext().setAuthentication(token);
} else {
throw new RuntimeException("Not a Valid Token.");
}
} else {
log.info("Authorization already present");
}
} catch (IllegalArgumentException e) {
throw new("Unable to get JWT Token",e);
} catch (ExpiredJwtException e) {
throw new("JWT Token has expired",e);
}
} else {
throw new RuntimeException("No valid authorization header found.");
}
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken createSecurityToken(UserDetails userDetails){
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
log.info("Authorities granted : {}", userDetails.getAuthorities());
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return token;
}
}
There are multiple versions of the same questions around and none seem to address the issue. I would like to get online users from spring security. I understand we need to Autowire SessionRegistry and use it. But still, it doesn't work. Here is the code.
Not sure whether it is due to custom Username, password authentication or due to custom password encoder or something else. Everything seems to be correct. Even getting the current logged in user's data works fine but not logged in user list.
SessionSecurityConfig.java
#EnableJpaRepositories(basePackageClasses = UsersRepository.class)
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SessionSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private PasswordEncoder passwordencoder;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private UsernamePasswordAuthProvider usernamepasswdauth;
#Bean
SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(usernamepasswdauth).userDetailsService(userDetailsService)
.passwordEncoder(passwordencoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.csrf().disable();
http.authorizeRequests() //
.antMatchers("/ua/*").permitAll() //
.antMatchers("/auth/*").authenticated() //
.and().requestCache() //
.requestCache(new NullRequestCache());
http.httpBasic().disable();
http.formLogin().disable();
http.logout().disable();
http
.sessionManagement()
.maximumSessions(1).sessionRegistry(sessionRegistry());
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
PasswordUpgrader.java
#Component
#Primary
public class PasswordUpgrader implements PasswordEncoder { // used to upgrade NTML password hashes to Bcrypt
private final static BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
#Autowired
JdbcTemplate jdbc;
public String encode(CharSequence rawPassword) {
byte[] bytes = NtlmPasswordAuthentication.nTOWFv1(rawPassword.toString());
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars).toLowerCase();
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
return false;
}
if (encodedPassword.equals(encode(rawPassword))) {
String sql = "update user_data set password=? where password=?";
jdbc.update(sql, new Object[] { bcrypt.encode(rawPassword), encode(rawPassword) });
return true;
} else {
return bcrypt.matches(rawPassword, encodedPassword);
}
}
}
UsernamePasswordAuthProvider.java
#Component
public class UsernamePasswordAuthProvider implements AuthenticationProvider {
Log logger = LogFactory.getLog(getClass());
#Autowired
private PasswordEncoder passwordencoder;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
Userdata userdata;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
String username = String.valueOf(auth.getPrincipal());
String password = String.valueOf(auth.getCredentials());
UserDetails user = userDetailsService.loadUserByUsername(username);
String encodedpassword = user.getPassword().toString();
logger.info("inside username passwd authentication");
if (encodedpassword != null && password != null && passwordencoder.matches(password, encodedpassword)) {
logger.info("inside username passwd authentication");
return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
}
throw new BadCredentialsException("Username/Password Incorrect");
}
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
UnauthController.java
#RestController
#RequestMapping("/ua")
public class UnauthController {
#Autowired
private UsernamePasswordAuthProvider usernamepasswdauth;
#PostMapping("/login")
public Map<String, Object> login(HttpServletRequest req, #RequestBody Map<String, Object> map) {
Authentication auth = usernamepasswdauth.authenticate(new UsernamePasswordAuthenticationToken(
map.get("username").toString(), map.get("password").toString()));
SecurityContextHolder.getContext().setAuthentication(auth);
map.put("sessionid", session.getId());
return map;
}
}
AuthController.java
#RestController
#RequestMapping("/auth")
public class AuthController {
#Autowired
Userdata user;
#Autowired
SessionRegistry sessionregistry;
Log logger = LogFactory.getLog(getClass());
#GetMapping("/onlineusers")
public List<String> authhello(Authentication authentication) {
logger.debug(user.getEmail()); // prints current logged in user's email.
logger.debug(sessionRegistry.getAllPrincipals());//returns empty
return sessionRegistry.getAllPrincipals().stream()
.filter(u -> !sessionRegistry.getAllSessions(u, false).isEmpty()).map(Object::toString)
.collect(Collectors.toList());
}
}
Tried Approaches:
Baeldung
Stackoverflow
StackOverflow
If you read carefully in documentation here, it is well-written(very secluded though). The cause of the problem is in the way data is handled after authentication. In default authentication provided by the spring security, after successful authentication the control is passed through a filter managing sessions. However, if you are using customized authentication and redirecting user after successful authentication that filter doesn't come into the way and that's why no sessions are added in the session registry and it returns empty list.
The solution is to set authentication strategy with session registry into session management configuration of spring security. This will lead to the the expected behaviour.
You'll find the code more helpful.
Method 1:
Spring security configuration for session
http
.sessionManagement()
.sessionAuthenticationStrategy(concurrentSession())
.maximumSessions(-1)
.expiredSessionStrategy(sessionInformationExpiredStrategy())
Define beans for
#Bean
public CompositeSessionAuthenticationStrategy concurrentSession() {
ConcurrentSessionControlAuthenticationStrategy concurrentAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
List<SessionAuthenticationStrategy> delegateStrategies = new ArrayList<SessionAuthenticationStrategy>();
delegateStrategies.add(concurrentAuthenticationStrategy);
delegateStrategies.add(new SessionFixationProtectionStrategy());
delegateStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry()));
return new CompositeSessionAuthenticationStrategy(delegateStrategies);
}
#Bean
SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
return new CustomSessionInformationExpiredStrategy("/login");
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
Here's the CustomSessionInformationExpiredStrategy.java
public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
private String expiredUrl = "";
public CustomSessionInformationExpiredStrategy(String expiredUrl) {
this.expiredUrl = expiredUrl;
}
#Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
HttpServletRequest request = sessionInformationExpiredEvent.getRequest();
HttpServletResponse response = sessionInformationExpiredEvent.getResponse();
request.getSession();// creates a new session
response.sendRedirect(request.getContextPath() + expiredUrl);
}
}
Method : 2
In spring security configuration, use the concurrentSession() from method 1.
http.sessionManagement().sessionAuthenticationStrategy(concurrentSession());
http.addFilterBefore(concurrentSessionFilter(), ConcurrentSessionFilter.class);
Here's CustomConcurrentSessionFilter.java
public class CustomConcurrentSessionFilter extends ConcurrentSessionFilter {
public CustomConcurrentSessionFilter(SessionRegistry sessionRegistry) {
super(sessionRegistry);
}
public CustomConcurrentSessionFilter(SessionRegistry sessionRegistry, SessionInformationExpiredStrategy sessionInformationExpiredStrategy) {
super(sessionRegistry, sessionInformationExpiredStrategy);
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
super.doFilter(req, res, chain);
}
}
Still scratching head for something? Find the working example at Github repo. Feel free to raise issues or contribute.
I am using WebSecurityConfigurerAdapter like this
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailServiceImpl userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
private ApplicationUserRepository applicationUserRepository;
public WebSecurity(UserDetailServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder, ApplicationUserRepository applicationUserRepository) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.applicationUserRepository = applicationUserRepository;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new AuthExceptionEntryPoint());
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(""/configuration/ui",
"/configuration/security"
"/webjars/**", "/users/social-sign-up", "client/**","/actuator/**",
"/instances","/assets/**","/home","/tables","/resources/**","/static/**",
"/css/**","/js/**","/scss/**","/templates").permitAll()
.antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_VERIFY_URL).permitAll()
.antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll().anyRequest().authenticated()
.and().addFilter(new JWTAuthenticationFilter(authenticationManager(), applicationUserRepository))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
Then I have a BasicAuthenticationFilter like this
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String header = req.getHeader(SecurityConstants.HEADER_STRING);
if (header == null || !header.startsWith(SecurityConstants.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(SecurityConstants.HEADER_STRING);
if (token != null) {
// parse the token.
String user = JWT.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes())).build()
.verify(token.replace(SecurityConstants.TOKEN_PREFIX, "")).getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
And then I have UsernamePasswordAuthenticationFilter like this
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private ApplicationUserRepository applicationUserRepository;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager,ApplicationUserRepository applicationUserRepository) {
this.authenticationManager = authenticationManager;
this.applicationUserRepository = applicationUserRepository;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
ApplicationUser creds = new ObjectMapper().readValue(req.getInputStream(), ApplicationUser.class);
System.err.println("Creds " + creds.getUsername() + ", " + creds.getPassword());
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(),
creds.getPassword(), new ArrayList<>()));
} catch (Exception e) {
// e.printStackTrace();
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = JWT.create().withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).sign(HMAC512(SECRET.getBytes()));
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
res.setStatus(HttpServletResponse.SC_OK);
String userName = ((User)auth.getPrincipal()).getUsername();
ApplicationUser au= applicationUserRepository.findByUsername(userName);
String json = new ObjectMapper().writeValueAsString(au);
res.getWriter().write(json);
res.getWriter().flush();
res.getWriter().close();
}
}
I am able to generate JWT token in HEADER. Like this
Authorization →Bearer awgaagarbrqe342tewrbwrewh.23tebvre34h4wbseb43qberqbqv.23gwrwvw4hw5445jmet76e-gqgqggq323t9003qgnibqp2389bvqp9q83bv9
What I am trying to achieve is whenever the token gets expired, the client sending the latest expired token will get a new token based on the token they have sent.
So my question is, how do I generate a refresh token or a mechanism that will take the old expired token and generate a new token?
Doing this would weaken the security of the application since new tokens can be retrieved from expired ones(invalid tokens). So you should try not to do it.
If you have to do it, keep a table in the db with the tokens and their validity, then when you get an invalid jwt token exception go to db check for the said token and see when it was expired. If it was 5 mins ago then you could probably renew it otherwise don't.
I have a JWTAuthFilter that extends OncePerRequestFilter where I am validating the token.
The validateToken method throws custom exceptions(CredentialsChangedException and TooManyDevicesException which extend org.springframework.security.core.AuthenticationException)
These exceptions are caught in the filter properly but when they move forward to the AuthenticationEntryPoint, the AuthenticationException turns into an instanceof InsufficientAuthenticationException and the custom error message that I want to return as a response is lost.
#Component
public class JwtAuthFilter extends OncePerRequestFilter {
private static final String BEARER = "Bearer ";
private static final Logger logger = LoggerFactory.getLogger(JwtAuthFilter.class);
#Autowired
private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Value ("${jwt.http.request.header}")
private String tokenHeader;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader(this.tokenHeader);
String username = null;
String jwtToken = null;
if((requestTokenHeader != null) && requestTokenHeader.startsWith(BEARER)) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
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);
}
}
}
catch (IllegalArgumentException e) {
logger.error("Unable to get username from JWT. ", e.getLocalizedMessage());
}
catch (ExpiredJwtException e) {
logger.warn("Expired JWT. ", e.getLocalizedMessage());
}
catch (Exception e) {
logger.error(e.getLocalizedMessage());
}
}
chain.doFilter(request, response);
}
}
#Component
public class JwtUnAuthorizedResponseAuthenticationEntryPoint implements AuthenticationEntryPoint , Serializable {
private static final long serialVersionUID = - 8970718410437077606L;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
if(e instanceof TooManyDevicesException) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, Constants.TOO_MANY_DEVICES);
}
else if(e instanceof CredentialsChangedException) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, Constants.CREDENTIALS_CHANGED);
}
else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, Constants.JWT_EXPIRED);
}
}
}
I want to send an appropriate unauthorized response from my filter, is there a way to do this?
I think once you caught the AuthenticationException in JwtAuthFilter, you should not move forward to the next filter as most probably an AnonymousAuthenticationFilter will sit in the later part of the filter chain and this filter will configure the current request to be an anonymous user if SecurityContextHolder is empty (i.e happen when authentication fail). The InsufficientAuthenticationException is most probably due to Spring considers the current request is an anonymous user who access some protected URL or methods.
Instead , once you catch AuthenticationException in your JwtAuthFilter , you should then call AuthenticationEntryPoint.commence() and end the filter chain . This is also how the BasicAuthenticationFilter is doing now .
So , I suggest revise the JwtAuthFilter to :
#Component
public class JwtAuthFilter extends OncePerRequestFilter {
#Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try{
//validate JWT
} catch (AuthenticationException e) {
logger.error(e.getLocalizedMessage());
SecurityContextHolder.clearContext();
this.authenticationEntryPoint.commence(request, response, e);
return;
}
}
}
As a workaround for now I simply added my custom error from the JwtAuthFilter as a request attribute and retrieved it in the AuthenticationEntryPoint