I have spent days on trying to get ROLES working within Spring Security using JWT tokens. Authentication seems to work fine but authorization seems to still not be working.
My current setup is that I have...
an In Memory Database for Users (I have one single user called "lion", and one single role "KING")
an authenticate end point which authenticates the user, and returns the JWT in the response.
My WebCofig looks like this :
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/hello").permitAll()
.antMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
I Basically want to say
anyone can access (/)
anyone can access "hello" and "authenticate"
My web controller looks like this :
#Controller
public class WebController {
#PreAuthorize("permitAll()")
#RequestMapping("/hello")
#ResponseBody
public String hello() {
return "Hello! ALL USERS logged in or not are allowed to see this!";
}
#PreAuthorize("hasRole('KING')")
#RequestMapping("/king")
#ResponseBody
public String king() {
return "Hello King";
}
#PreAuthorize("hasRole('ROLE_KING')")
#RequestMapping("/king2")
#ResponseBody
public String king2() {
return "Hello King2";
}
}
My web token returned via my authentication looks good (i think..). Decoded the body looks like this :
{
"sub": "lion",
"scopes": "KING",
"iat": 1580645802,
"exp": 1580663802
}
... and will allow me to access anywhere where I just need to be authenticated. "anyRequest().authenticated()".
However both king and king2 REST endpoints give me 403 - "Access Denied" even though my user has role KING (I tried both KING and ROLE_KING)
I feel like I am missing a piece of the puzzle.
My JWT Filter seems to look good too. The authentication object (..which is set in the security context handler like so : SecurityContextHolder.getContext().setAuthentication(authentication);) has exactly ONE SimpleGrantedAuthority with role = KING.
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
userDetails.getAuthorities(); //has exactly ONE SimpleGrantedAuthority with value "KING"
SimpleGrantedAuthority s = new SimpleGrantedAuthority("asfd");
s.getAuthority(); //string
UsernamePasswordAuthenticationToken authentication = jwtUtil.getAuthentication(jwt,
SecurityContextHolder.getContext().getAuthentication(), userDetails);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
My UserDetailsService looks like this :
public class UserService implements UserDetailsService {
#Autowired
private UserRepository repository;
#Autowired
private PasswordValidationService passwordValidationService;
#Override
public User loadUserByUsername(String userName) throws UsernameNotFoundException {
UserEntity u = repository.findByUserNameIgnoreCase(userName);
SimpleGrantedAuthority a = new SimpleGrantedAuthority(u.getRole().toString());
ArrayList<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
auths.add(a);
return new User(u.getUserName(), u.getPassword(), auths);
}
}
Everything looks good to me which is why I am confused right now :(
*********** UPDATE ************
If I use hasAuthority instead of hasRole then this works :
#PreAuthorize("hasAuthority('KING')")
#RequestMapping("/king3")
#ResponseBody
public String king3() {
return "Hello King";
}
So how do I get hasRole to work? Am I missing the appending of "ROLE_" somewhere in my code?
Related
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;
}
}
I have created a project with Spring Security and JWT Tokens.
I observed that the path specified in configure method in WebSecurityConfig class does not behave equally for different HTTP methods.
My ConcertRESTController that I am trying to secure looks as following:
#RestController
#RequestMapping("/concerts")
public class ConcertRESTController {
#GetMapping("")
public List<Concert> getAllConcerts() {
// get all concerts logic
}
#PostMapping("")
public ResponseEntity<Concert> addConcert(#RequestBody Concert concert) {
// add concert logic
}
#DeleteMapping("/{id}")
public ResponseEntity<?> deleteConcertById(#PathVariable("id") int id) {
// delete concert logic
}
And WebSecurityConfig:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers(HttpMethod.GET,"/concerts/**").permitAll()
.antMatchers(HttpMethod.POST, "/concerts/**").hasRole("admin")
.antMatchers(HttpMethod.DELETE, "/concerts/**").hasRole("admin")
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore((Filter) authenticationJwtTokenFilter(),
UsernamePasswordAuthenticationFilter.class);
}
However, the only method that works with this pattern is HttpMethod.GET. Other two give me status 403 when trying to call them with admin rights in Postman.
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/concerts"
What is interesting, when I change them to:
.antMatchers(HttpMethod.GET,"/concerts/**").permitAll()
.antMatchers(HttpMethod.POST, "concerts/**").hasRole("admin")
.antMatchers(HttpMethod.DELETE, "concerts/**").hasRole("admin")
All works as expected.
Could anyone explain this behaviour? Thank you in advance!
----------------------------------------------------------------------
Edit - adding code for JwtAuthTokenFilter class
public class JwtAuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtProvider tokenProvider;
#Autowired
private UserDetailsServiceImpl userDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwt(httpServletRequest);
if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
String username = tokenProvider.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Can NOT set user authentication -> Message: {}", e);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private String getJwt(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.replace("Bearer ", "");
}
return null;
}
}
In WebSecurityConfig:
#Bean
public JwtAuthTokenFilter authenticationJwtTokenFilter() {
return new JwtAuthTokenFilter();
}
I think there is no issue with your end points. The problem is with your role.
hasRole automatically inserts "ROLE_" in the parameters.
You need to make sure few things:
1) If your role is stored as "ROLE_admin", then use hasRole("admin").
2) If your role is stored as "admin", then you should use hasAuthority("admin").
What is interesting, when I change them to:
.antMatchers(HttpMethod.GET,"/concerts/**").permitAll()
.antMatchers(HttpMethod.POST, "concerts/**").hasRole("admin")
.antMatchers(HttpMethod.DELETE, "concerts/**").hasRole("admin")
In above case FilterSecurityInterceptor does not consider your end point as Secure object instead it consider it as Public object, so make sure your pattern starts with forward slash. You can see below logs:
2020-06-06 17:22:56 DEBUG AntPathRequestMatcher:176 - Checking match of request : '/concerts'; against 'concerts/**'
2020-06-06 17:22:56 DEBUG FilterSecurityInterceptor:210 - Public object - authentication not attempted
2020-06-06 17:44:23 DEBUG AntPathRequestMatcher:176 - Checking match of request : '/concerts'; against '/concerts/**'
2020-06-06 17:44:23 DEBUG FilterSecurityInterceptor:219 - Secure object: FilterInvocation: URL: /concerts; Attributes: [hasRole('ROLE_Admin')]
I have problem with validating user credentials. When I give correct credentials first time everything goes OK but giving invalid credentials first and then give correct ones I get invalid credentials error. I use Postman Basic
Auth.
My config class:
#Configuration
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserService userService;
#Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST ,"/login").permitAll()
.antMatchers("/admin").hasAuthority("ADMIN")
.anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and()
.logout()
.deleteCookies("remove")
.invalidateHttpSession(true);
http.rememberMe().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userService)
.and().eraseCredentials(true);
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
And my controller class
#PostMapping
public ResponseEntity<?> loginButtonClicked(HttpServletRequest request) {
HttpSession session = request.getSession();
final String authorization = request.getHeader("Authorization");
String[] authorizationData=null;
if (authorization != null && authorization.startsWith("Basic")) {
// Authorization: Basic base64credentials
String base64Credentials = authorization.substring("Basic" .length()).trim();
String credentials = new String(Base64.getDecoder().decode(base64Credentials),
Charset.forName("UTF-8"));
// credentials = username:password
authorizationData = credentials.split(":", 2);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(authorizationData[0], authorizationData[1],Arrays.asList(new SimpleGrantedAuthority("USER")));
User user = userService.findUserEntityByLogin(authorizationData[0]);
if(user != null && user.getFromWhenAcceptLoginAttempts() != null && (user.getFromWhenAcceptLoginAttempts()).isBefore(LocalDateTime.now())){
// Authenticate the user
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
// Create a new session and add the security context.
session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
return new ResponseEntity<>(new LoginResponseObject(200,"ACCESS GRANTED. YOU HAVE BEEN AUTHENTICATED"), HttpStatus.OK);
}else{
session.getId();
SecurityContextHolder.clearContext();
if(session != null) {
session.invalidate();
}
return new ResponseEntity<>(new ErrorObject(403,"TOO MANY LOGIN REQUESTS","YOU HAVE ENTERED TOO MANY WRONG CREDENTIALS. YOUR ACCOUNT HAS BEEN BLOCKED FOR 15 MINUTES.", "/login"), HttpStatus.FORBIDDEN);
}
}else{
session.getId();
SecurityContextHolder.clearContext();
if(session != null) {
session.invalidate();
}
return new ResponseEntity<>(new ErrorObject(401,"INVALID DATA","YOU HAVE ENTERED WRONG USERNAME/PASSWORD CREDENTIALS", "/login"), HttpStatus.UNAUTHORIZED);
}
}
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public ObjectMapper objectMapper(){
return new ObjectMapper();
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
The problem is that the request is stored in cache due to your sessionCreationPolicy.
To avoid this problem, you could add .requestCache().requestCache(new NullRequestCache()) in your http security config to override the default request cache configuration, but be careful because this could create another side effect (it depends on your application).
In case you do not need the session, you can choose another session policy:
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
Another alternative is to relay in Spring's BasicAuthenticationFilter. This filter does all the authentication logic for you. To enable it, you only have to add .httpBasic()in your http security configuration.
You may want to add a custom logic on authentication success/failure. In that case, you only have to create a custom filter (CustomBasicAuthenticationFilter) that extends BasicAuthenticationFilter class and overrides the methods onSuccessfulAuthentication()and onUnsuccessfulAuthentication(). You will not need to add .httpBasic() but you will need to insert your custom filter in the correct place:
.addFilterAfter(new CustomBasicAuthenticationFilter(authenticationManager), LogoutFilter.class).
Any of that 3 solutions will avoid your problem.
Try to write .deleteCookies("JSESSONID") in your SpringSecurityConfig class.
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.
In logout controller I tryed to write a lot of combination of code. Now I have this:
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
SecurityContextHolder.getContext().setAuthentication(null);
auth.setAuthenticated(false);
But after provided code execution token still valid.
What do I wrong? How to revoke token eventually?
The class you're looking for is
DefaultServices, method revokeToken(String tokenValue).
Here an exemple of a controller that revokes token, and here the oauth2 configuration with the DefaultServices bean.
If you need to revoke a token for another user than the current one (E.g. an admin wants to disable a user account), you can use this:
Collection<OAuth2AccessToken> tokens = tokenStore.findTokensByClientIdAndUserName(
"my_oauth_client_id",
user.getUsername());
for (OAuth2AccessToken token : tokens) {
consumerTokenServices.revokeToken(token.getValue());
}
With tokenStore being an org.springframework.security.oauth2.provider.token.TokenStore and consumerTokenServices being a org.springframework.security.oauth2.provider.token.ConsumerTokenServices
the thread is a bit old but for JWTToken users this is not working as the tokens are not stored.
So another option is to use a filter.
1 create a method for admin to lock/unlock a user on your database.
2 use a filter and if the method needs authentication check if the user is active or not
exemple :
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication != null
&& authentication.getName() != null
&& !authentication.getName().equalsIgnoreCase("anonymousUser")) {
UserModel user = userService.getUser(authentication.getName());
if(user != null && !user.isActivated())
throw new SecurityException("SECURITY_USER_DISABLED");
}
chain.doFilter(request, response);
}
On client side just intercept this error and disconnect user
hope this helps someone.
Simple example of token revocation for current authorized user using DefaultTokenServices:
Need Bean for Default token store
#Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
Then you can write your own controller
#RestController
#RequestMapping("/user")
public class UserApi {
#Autowired
private DefaultTokenServices tokenServices;
#Autowired
private TokenStore tokenStore;
#DeleteMapping("/logout")
#ResponseStatus(HttpStatus.NO_CONTENT)
public void revokeToken() {
final OAuth2Authentication auth = (OAuth2Authentication) SecurityContextHolder
.getContext().getAuthentication();
final String token = tokenStore.getAccessToken(auth).getValue();
tokenServices.revokeToken(token);
}
}
Autowire the DefaultTokenServices then use this code:
String authHeader = request.getHeader("Authorization");
String tokenValue = authHeader.replace("bearer", "").trim();
tokenService.revokeToken(tokenValue);
tokenService.setAccessTokenValiditySeconds(1);
tokenService.setRefreshTokenValiditySeconds(1);
Just try the code to revoke the access token.