How to generate new JWT token from old token in Spring Boot? - java

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.

Related

Access denied after changed password with spring boot security JWT

I'm implementing spring boot security for my application. The login function works fine, but somehow whenever I test the change-password function, it's always failed, I checked in the database the password has been updated with the new HashString, but I cannot login with both old-password and new-password. Please help !
Here is the change-password API
#PutMapping("/reset-password")
public ResponseEntity resetPassword(#RequestBody String password){
//extract user name
String username = SecurityContextHolder.getContext().getAuthentication()
.getPrincipal().toString();
ApplicationUser applicationUser = applicationUserRepo.findByUsername(username);
if(applicationUser == null ){
return ResponseEntity.badRequest().body("Could not find UserName");
}
applicationUser.setPassword(bCryptPasswordEncoder.encode(password));
applicationUserRepo.save(applicationUser);
Logs log = new Logs (TimeConverter.getVietnamCurrentTime(),"PASSWORD", "User", applicationUser.getUsername(), applicationUser.getUsername() );
logsRepo.save(log);
return ResponseEntity.ok().body("Password changed successfully!");
}
Here is my implementation of WebSecurityConfigurerAdapter
#EnableWebSecurity
#Configuration
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailsServiceImpl userDetailsService;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurity(UserDetailsServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, LOGIN_URL).permitAll()
//.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(ImmutableList.of("*"));
configuration.setAllowedMethods(ImmutableList.of("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
AuthenticationFilter
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(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) {
System.out.println("fucked");
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) {
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.addHeader("Access-Control-Expose-Headers","*");
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
response.getWriter().write("blalfaf");
super.unsuccessfulAuthentication(request, response, failed);
}
}
Try using:
addFilterBefore(new JWTAuthenticationFilter(authenticationManager(), UsernamePasswordAuthenticationFilter.class)
instead of just
addFilter(....
in your WebSecurity.configure(.....
This will put your own filter before the original UsernamePasswordAuthenticationFilter. Maybe your filter is last in line and the login is failing before.

I can not retrieve the current custom user object from Spring Security Context

I'm using LDAP authentication provider (active directory) + JWT authorization filter.
I have my custom user object implementing UserDetails, also my user service extends UserDetailsService. But when I do:
Usuario principal = (Usuario) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Principal is just a string (the username), not my user object.
This is my configuration:
SecurityConfig:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final LdapProperties ldapProperties;
private final LdapUserMapper ldapUserMapper;
private final UserService userService;
public SecurityConfig(LdapProperties ldapProperties, LdapUserMapper ldapUserMapper, UserService userService) {
this.ldapProperties = ldapProperties;
this.ldapUserMapper = ldapUserMapper;
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// Entry points
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.GET, "/v2/api-docs",
"/configuration/ui",
"/swagger-resources",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/swagger-resources/**",
"/swagger-ui.html").permitAll()
//TODO review
.anyRequest().authenticated();
// JwtWebSecurityConfigurer... TODO ?
// Filters
http.addFilter(new AuthenticationFilter(authenticationManager())); // ldap
http.addFilter(new AuthorizationFilter(authenticationManager())); // jwt
http.cors();
http.csrf().disable();
// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Need to provide Authorization header
http.httpBasic();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(ldapAuthenticationProvider());
auth.userDetailsService(userService);
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
corsConfiguration.setAllowedMethods(Arrays.asList(CorsConfiguration.ALL));
//TODO configure properly
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
#Bean
public AbstractLdapAuthenticationProvider ldapAuthenticationProvider() {
String urls = "";
for (String url : ldapProperties.getUrls()) {
urls += url + " ";
}
urls = urls.trim();
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
ldapProperties.getBaseEnvironment().get("domain"),
urls,
ldapProperties.getBase()
);
provider.setUserDetailsContextMapper(ldapUserMapper);
provider.setConvertSubErrorCodesToExceptions(true);
// comment to connect as anonymous
provider.authenticate(
new UsernamePasswordAuthenticationToken(ldapProperties.getUsername(), ldapProperties.getPassword())
);
return provider;
}
}
LdapUserMapper:
#Component
public class LdapUserMapper implements UserDetailsContextMapper {
private final UserService userService;
#Autowired
public LdapUserMapper(UserService userService) {
this.userService = userService;
}
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
Usuario result = (Usuario) userService.loadUserByUsername(username);
//TODO compare roles ? set bloqueado ? ...
return result;
}
#Override
public void mapUserToContext(UserDetails userDetails, DirContextAdapter dirContextAdapter) {
}
}
It was my authorization filter.
#Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String token = req.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.replace("Bearer ", "");
Authentication authentication = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(final String token) {
try {
DecodedJWT decodedToken = JWT.require(Algorithm.HMAC512(jwtSecret)).build().verify(token);
String username = decodedToken.getSubject();
return new UsernamePasswordAuthenticationToken(username , null, new ArrayList<>());
} catch (JWTVerificationException ex) {
log.error(ex.getMessage());
}
return null;
}
I wasn't aware that I only was passing the username as the Principal object. So I changed it to this:
Usuario usuario = (Usuario) userService.loadUserByUsername(decodedToken.getSubject());
I also removed
auth.userDetailsService(userService);
From AuthenticationManagerBuilder config. That was wrong. My auth provider is LDAP, not the DB.

Spring Boot REST Api JWT AuthorizationFilter does not validate the token

I am made a sample spring boot app implementing JWT token authentication which is working partially. That means it does not let the request access the endpoints until generating the token by sending user details using /login url. Once the token is received, the token is sent with a header called Authorization. So untill the first url all with this header, it does not allow to access endpoints. But after the 1st call I can access the enpoints without the Authorization header which contains the JWT token.
SecurityConfig.java
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService customUserDetailsService;
#Autowired
public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
this.customUserDetailsService = customUserDetailsService;
System.out.println("from SecurityConfig constructor");
System.out.println(this.customUserDetailsService.loadUserByUsername("batman").getUsername());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("from configure");
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/sign_up").permitAll()
.antMatchers("/*/floor1/**").hasRole("USER")
.antMatchers("/*/floor2/**").hasRole("ADMIN")
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), customUserDetailsService));
}
}
JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
// {"username":"batman","password":"123"}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
System.out.println(">>>>> AuthenticationFilter: checking user credentials....");
ApplicationUser applicationUser = new ObjectMapper().readValue(request.getInputStream(), ApplicationUser.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(applicationUser.getUsername(), applicationUser.getPassword()));
} catch (IOException e) {
System.out.println(">>>>> AuthenticationFilter: error in checking user credentials....");
throw new RuntimeException(e);
} catch (Exception e) {
System.out.println(">>>>> AuthenticationFilter: error in checking user credentials....");
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println(">>>>> AuthenticationFilter: successfulAuthentication creating token...");
ZonedDateTime expirationTimeUTC = ZonedDateTime.now(ZoneOffset.UTC).plus(SecurityConstants.EXPIRATION_TIME, ChronoUnit.MILLIS);
String token = Jwts.builder().setSubject(((User)authResult.getPrincipal()).getUsername())
.setExpiration(Date.from(expirationTimeUTC.toInstant()))
.signWith(SignatureAlgorithm.HS256, SecurityConstants.SECRET)
.compact();
response.getWriter().write(token);
response.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
System.out.println(">>>>> AuthenticationFilter: successfulAuthentication token created and added to response");
}
}
JwtAuthorizationFilter.java
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private final CustomUserDetailsService customUserDetailsService;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, CustomUserDetailsService customUserDetailsService) {
super(authenticationManager);
this.customUserDetailsService = customUserDetailsService;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(SecurityConstants.HEADER_STRING);
System.out.println(">>>>> AuthorizationFilter doFilterInternal: checking the availability of toke header...");
if(header == null || !header.startsWith(SecurityConstants.TOKEN_PREFIX)){
System.out.println(">>>>> AuthorizationFilter doFilterInternal: header is null or not start with token prefix");
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = getAuthenticationToken(request);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthenticationToken(HttpServletRequest request){
System.out.println(">>>>> AuthorizationFilter UsernamePasswordAuthentication: validating the token...");
String token = request.getHeader(SecurityConstants.HEADER_STRING);
if(token == null){
System.out.println(">>>>> AuthorizationFilter UsernamePasswordAuthentication: error: token is null");
return null;
}
String username = Jwts.parser().setSigningKey(SecurityConstants.SECRET).parseClaimsJws(token.replace(SecurityConstants.TOKEN_PREFIX, "")).getBody().getSubject();
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
ApplicationUser applicationUser = customUserDetailsService.loadApplicationUserByUsername(username);
return username != null ? new UsernamePasswordAuthenticationToken(applicationUser, null, userDetails.getAuthorities()) : null;
}
}
in JwtAuthorizationFilter.java it returns true where the token is check for null. So it is supposed to prevent accessing endpoints
and give an error to the client. But it does not. It allows the request to slip through the filter
and access the endpoint. Please help me if i am missing something here.
Complete sample project: https://github.com/xandar6/jwt

Can call API with any JWT token

I am writing an app where front end users will make calls to the back end through an API. I have implemented JWT where users can register and when they try to login they will get a JWT in response and I can make other calls then to the API with the JWT in the header. If the JWT is left out of the header, the call will be failed. The basics work as expected.
The issue I face tho is that I can generate my own custom JWT, assign it to a header and be able to successfully call the back end.
I have followed numerous tutorials online and finding this topic pretty confusing and very complex to fully grasp. I have no doubt it is something simple I am missing from my code but I can't see what.
Here is what I have done so far;
AuthenticationFilter
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
User credentials = new ObjectMapper().readValue(request.getInputStream(), User.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 {
String token = Jwts.builder()
.setSubject(((org.springframework.security.core.userdetails.User) authResult.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
.compact();
response.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
AuthorizationFilter
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(HEADER_STRING);
if(StringUtils.isBlank(header) || ! StringUtils.startsWith(header, TOKEN_PREFIX)){
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if(StringUtils.isNotBlank(token)){
String user = Jwts.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if(StringUtils.isNotBlank(user)){
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
WebSecurity
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurity(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
protected 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;
}
}
As I said I am finding this topic fairly hard, so I'm sure there could be simple errors within my code. If you can help or shed light on my problem in any way, it'd be much appreciated
This appears to be the critical code from your servlet filter which checks the JWT:
String token = request.getHeader(HEADER_STRING);
if (StringUtils.isNotBlank(token)){
String user = Jwts.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if (StringUtils.isNotBlank(user)){
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
If you look closely at what you are doing, you will see that your logic is to extract the subject from the JWT. This much is correct. But you then authorize that user so long as the subject is not blank. In other words, any user will be authorized by your current logic. Instead, you would typically do something like this:
if (StringUtils.isNotBlank(user)){
// check that user against a database/cache
// if the account is active etc. THEN authorize the user
}
Typically, after extracting the subject/username from the JWT, you would hit the database/cache to check if that user's account is still active. If not, then you would return 401 to the calling app. A JWT by itself does not mean the user is authorized, because at some point you may revoke that user's token, the token could expire, etc.

Approaches for logout functionality in Spring Boot app (Spring Security + JWT)

I create Spring Boot + Spring Security app with JWT authentication and it works something like this:
client sends username, password to the login endpoint;
server checks if the provided credentials are valid and return a token;
client sends the token with every future request.
It's based on this example and approach for logout functionality which author suggest - just remove JWT token on client-side. I don't think that is good idea, and I'd like to implement it on back-end.
As I understand I need to remove JWT token of user which call logout REST method on server-side, after that when this user will call other REST methods he should get responses with 403 error. I investigated ways how can I add this feature and didn't find any appropriate solution.
Here are details of my implementation (there may be some differences from the example from the link above):
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurity(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.antMatchers(HttpMethod.POST, LOGIN_URL).permitAll()
.antMatchers(HttpMethod.GET, HEALTH_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.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;
}
}
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(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) {
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;
}
}
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl(LOGIN_URL);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User creds = new ObjectMapper().readValue(req.getInputStream(), User.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(((org.springframework.security.core.userdetails.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 SecurityConstants {
public static final String SECRET = "SecretKeyToGenJWTs";
public static final long EXPIRATION_TIME = 864_000_000; // 10 days
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
}

Categories