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.
Related
I have a problem with cycle injection and don't know how to fix it :(
For my SecurityConfig I need JWTAuthenticationFilter.
For my JWTAuthenticationFilter I need AuthenticationManager.
For my AuthenticationManager I need SecurityConfig.
Can you help me with resolving this problem ? Thanks !
#Configuration
#RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository userRepository;
#Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
#Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder auth ) throws Exception {
return auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder()).and().build();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfig {
private final JWTVerifierFilter jwtVerifierFilter;
private final AuthenticationProvider authenticationProvider;
private final JWTAuthenticationFilter jwtAuthenticationFilter;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/auth/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerifierFilter, JWTAuthenticationFilter.class);
return http.build();
}
}
#RequiredArgsConstructor
#Component
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final TokenRepository tokenRepository;
private final JwtService jwtService;
private final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
JwtAuthenticationRequest authModel = OBJECT_MAPPER.readValue(request.getInputStream(), JwtAuthenticationRequest.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(authModel.getUsername(), authModel.getPassword());
return authenticationManager.authenticate(authentication);
} catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
String token = jwtService.generateToken((UserEntity) authResult.getPrincipal());
TokenEntity tokenEntity = TokenEntity.builder()
.id(UUID.randomUUID().toString())
.authenticationToken(token)
.username(authResult.getName())
.build();
tokenEntity = tokenRepository.save(tokenEntity);
response.addHeader(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", tokenEntity.getId()));
ConnValidationResponse respModel = ConnValidationResponse.builder().isAuthenticated(true).build();
response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(OBJECT_MAPPER.writeValueAsBytes(respModel));
}
}
I tried to replace
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
by
#Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
but for HttpSecurity I need SecurityConfig too.
I also tried add #Lazy for AuthenticationConfiguration config but it doesn't work
I'm trying to build a REST API with Spring Boot and Spring Security but I have run into an issue where any request to the /login endpoint is blocked because of CORS and I can't figure out how to allow it.
Here's what I get when I inspect the network tab of my developper tools whenever I send a login request in my frontend application:
Here's my SecurityConfig.java file (I've omitted imports for brevity):
#EnableWebSecurity(debug = true)
#RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AuthenticationUserDetailService authenticationUserDetailService;
private final PasswordEncoder passwordEncoder;
#Value("${jwt.secret}")
private String jwtSecret;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors();
http.authorizeRequests().antMatchers("/login").permitAll();
http.authorizeRequests().antMatchers("/").permitAll();
http.authorizeRequests().antMatchers("/api/teachers/**").hasAuthority("TEACHER");
http.authorizeRequests().antMatchers("/api/students/**").hasAuthority("STUDENT");
http.authorizeRequests().antMatchers("/api/**").hasAuthority("ADMIN");
http.authorizeRequests().anyRequest().authenticated();
//http.authorizeRequests().anyRequest().permitAll();
http.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtSecret));
http.addFilter(new JwtAuthorizationFilter(authenticationManager(), jwtSecret));
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authenticationUserDetailService).passwordEncoder(passwordEncoder);
}
}
Here's my JwtAuthenticationFilter.java file:
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final String jwtSecret;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, String jwtSecret) {
this.authenticationManager = authenticationManager;
this.jwtSecret = jwtSecret;
setFilterProcessesUrl("/login");
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginRequest credentials = new ObjectMapper()
.readValue(request.getInputStream(), LoginRequest.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 {
User user = (User) authResult.getPrincipal();
String token = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, jwtSecret.getBytes())
.claim("role", authResult.getAuthorities().iterator().next().getAuthority())
.setSubject(user.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 24 * 3600 * 1000))
.compact();
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"token\":\"" + token + "\"}");
}
}
I have tried every solution I have found online and I'm out of ideas. How can I authorize CORS from any source for my /login endpoint ?
Thanks a lot!
You have to provide a Bean of type CorsConfigurationSource and tell Spring Security to use that configuration source in the CorsFilter, and place it before the authentication and authorization filters.
First, define the CorsConfigurationSource Bean:
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("http://localhost:3000");
source.registerCorsConfiguration("/**", config);
return source;
}
Then, tell Spring Security to configure CORS with defaults:
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.cors(Customizer.withDefaults());
...
}
I am using Spring Security and using Username Password Authentication Filter. I want to know if it is possible to show the authentication endpoint in Swagger. This endpoint is generated automatically by the filter (as far I understood).
I really want to make this endpoint appear in Swagger-ui, otherwise I will need to login in Postman and then use Swagger with the Jwt Token, which is little strange.
This is the UsernamePasswordAuthenticationFilter:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) {
try {
LoginDTO userEntity = new ObjectMapper().readValue(req.getInputStream(), LoginDTO.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userEntity.getEmail(),
userEntity.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException {
Token token = new Token(JwtUtils.createAccessToken(ZonedDateTime.now().plusMinutes(10), TenantContext.getCurrentUserUniqueIdentifier(), TenantContext.getCurrentTenantId()),
JwtUtils.createRefreshToken(ZonedDateTime.now().plusMonths(3)));
PrintWriter out = res.getWriter();
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
out.print(new ObjectMapper().writeValueAsString(token));
out.flush();
}
}
This is my class extending from WebSecurityAdapter. As you can see, I set the url as /v1/login.
#EnableWebSecurity
#AllArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserEntityDetailsService userEntityDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/v1/account").permitAll()
.antMatchers(HttpMethod.GET, "/v1/healthcheck").permitAll()
.antMatchers("/v1/recoverpassword/**").permitAll()
.antMatchers("/swagger-ui/**", "/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/**", "/swagger-ui.html", "/webjars/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(getAuthenticationFilter())
.addFilter(new AuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userEntityDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
public AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
filter.setFilterProcessesUrl("/v1/login");
return filter;
}
}
This is my SpringFox config:
#Configuration
#EnableSwagger2
public class SpringFoxConfig {
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Projeto List")
.description("All endpoints of Projeto List Api")
.build();
}
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("br.com.projetolist.resource"))
.paths(PathSelectors.ant("/**"))
.build()
.securitySchemes(Arrays.asList(apiKey()));
}
/**
* SwaggerUI information
*/
#Bean
UiConfiguration uiConfig() {
return UiConfigurationBuilder.builder()
.deepLinking(true)
.displayOperationId(false)
.defaultModelsExpandDepth(1)
.defaultModelExpandDepth(1)
.defaultModelRendering(ModelRendering.MODEL)
.displayRequestDuration(false)
.docExpansion(DocExpansion.NONE)
.filter(false)
.maxDisplayedTags(null)
.operationsSorter(OperationsSorter.ALPHA)
.showExtensions(false)
.tagsSorter(TagsSorter.ALPHA)
.validatorUrl(null)
.build();
}
private ApiKey apiKey() {
return new ApiKey("jwtToken", "Authorization", "header");
}
}
You can just add a similar method to one of your controllers. In your case, it will be the POST method with URL "/v1/login".
#PostMapping("/v1/login")
#ResponseStatus(OK)
fun login(#RequestBody userCreds: UserCredentials) {}
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 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";
}