I'm trying to integrate the updated Spring Security in my project, instead of using the deprecated extending WebSecurityConfigurerAdapter. I've set up a good system in which the user gets authenticated (User implements UserDetails - I am using Hibernate) and a token gets generated. I get a 200 on this login and receive a token. This authetication part works fine.
Now the problem is that my users have roles (like ADMIN, USER, ...) These roles are added to the generated token. My controllers get the #PreAuthorize annotation. The request cannot pass these annotation and get a forbidden. When I don't use the #PreAuthorize, the requests get validated with the token.
#Configuration
#EnableWebSecurity
#EnableMethodSecurity
public class SecurityConfig {
private RSAKey rsaKey;
private final DefaultUserDetailsService defaultUserDetailsService;
public SecurityConfig(DefaultUserDetailsService defaultUserDetailsService) {
this.defaultUserDetailsService = defaultUserDetailsService;
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.userDetailsService(defaultUserDetailsService)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.headers(headers -> headers
.frameOptions().sameOrigin()
)
.httpBasic(withDefaults())
.build();
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
#Bean
JwtDecoder jwtDecoder() throws JOSEException {
return NimbusJwtDecoder.withPublicKey(rsaKey.toRSAPublicKey()).build();
}
#Bean
JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwks) {
return new NimbusJwtEncoder(jwks);
}
#Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:4200"));
configuration.setAllowedMethods(List.of("GET","POST","DELETE"));
configuration.setAllowedHeaders(List.of("Authorization","Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",configuration);
return source;
}
}
#Component
public class KeyGeneratorUtils {
private KeyGeneratorUtils() {}
static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
public class Jwks {
private Jwks() {}
public static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
}
#Service
public class DefaultTokenService implements TokenService {
private final JwtEncoder encoder;
public DefaultTokenService(JwtEncoder encoder) {
this.encoder = encoder;
}
#Override
public String generateToken(Authentication authentication) {
Instant now = Instant.now();
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
System.out.println("scope: " + scope);
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(1, ChronoUnit.HOURS))
.subject(authentication.getName())
.claim("scope", scope)
.build();
return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
public class UserDetailsImpl implements UserDetails{
private static final long serialVersionUID = 1L;
private final Long id;
private final String username;
private final String riziv;
private final boolean verified;
#JsonIgnore
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String riziv, String password,
Collection<? extends GrantedAuthority> authorities, boolean verified) {
this.id = id;
this.username = username;
this.riziv = riziv;
this.password = password;
this.authorities = authorities;
this.verified = verified;
}
public static UserDetailsImpl build(AuthUser authUser) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(authUser.getRol().toString()));
return new UserDetailsImpl(
authUser.getId(),
authUser.getUsername(),
authUser.getRiziv(),
authUser.getPassword(),
authorities, authUser.isVerified());
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Long getId() {
return id;
}
public boolean isVerified() {
return verified;
}
public String getRiziv() {
return riziv;
}
#Override
public String getUsername() {
return username;
}
#Override
public String getPassword() {
return password;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
#Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl klant = (UserDetailsImpl) o;
return Objects.equals(id, klant.id);
}
}
#Service
public class DefaultUserDetailsService implements UserDetailsService {
private final AuthUserService authUserService;
public DefaultUserDetailsService(AuthUserService authUserService) {
this.authUserService = authUserService;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AuthUser authUser = authUserService.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(authUser);
}
}
#PreAuthorize("hasAnyRole('USER', 'ADMIN')")
I am making a configuration mistake somewhere, but I cannot seem to find it. Spring docs are very very hard to figure out, but I have been reading them relentlessly. There is also not a lot of clear information on these topics yet. I can find youtube videos tutorials and some related topics, but they only explain small parts, never a full setup.
I have added below my securityConfig, KeyGenerator, Jwks and tokengenerate service. I also just added the Userdetailsimpl and service. I build my userdetailsImpl out of a user with a static build method. It might seem a strange construction but it works, it is because I did the security last and didn't think of it before. Also I added an example of my #Preauthorize.
I am very close and this could be a good example for other users trying to implement this, because I seem not te able to find an example somewhere.Does anyone have experience with setting the Spring Boot 3 security up and can they tell me how I am misconfiguring? Why is my role not getting 'read' by the #PreAuthorize?
Okay so here's the thing, since you're implementing resource server, the class
org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
is the one responsible for converting your scopes inside jwt token to granted authorities.
Now, this class prepends all the authorities with SCOPE_ prefix.
Since you're using
hasAnyRole('ADMIN', 'USER',...)
this method internally invokes
hasAnyAuthorityName(defaultRolePrefix, roleName)
method with the defaultRolePrefix as ROLE_ and the roleName as your passed value(s).
Internal Implementation:
#Override
public final boolean hasAnyRole(String... roles) {
return hasAnyAuthorityName(this.defaultRolePrefix, roles);
}
#Override
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
On the other hand, the hasAnyAuthority method makes a call to the same method but with null passed as defaultRolePrefix.
Now since you're using
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
in security config, it is using the default AuthenticationConverter for your jwt token which is
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
which further invokes
org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
As per the implementation in JwtGrantedAuthoritiesConverter, all the scopes in your jwt token are prefixed by SCOPE_ as I mentioned earlier.
Now assuming your granted authorities return ADMIN as one of the roles. Once you add it to your scope in jwt, the default converter will return SCOPE_ADMIN as an Authority and similarly if you return ROLE_ADMIN in the scope, it will be converted to SCOPE_ROLE_ADMIN by default.
The JwtAuthenticationConverter class returns an instance of
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
So, it can be fixed in following ways:
Either, use hasAnyAuthority to check the authorities by appending SCOPE_ to the role names you have set in scope.
If your role name is ADMIN or ROLE_ADMIN you should use
#PreAuthorize("hasAnyAuthority('SCOPE_ADMIN')")
#PreAuthorize("hasAnyAuthority('SCOPE_ROLE_ADMIN')")
and so on.
If you want to use hasAnyRole check then you must use
#PreAuthorize("hasAnyAuthority('ROLE_SCOPE_ADMIN')")
#PreAuthorize("hasAnyAuthority('ROLE_SCOPE_ROLE_ADMIN')")
for ADMIN and ROLE_ADMIN values respectively.
Or, Implement a custom Authority Converter and pass it to the oauth2ResourceServer in security config as follows,
Example Custom Converter
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.Collections;
#Component
public class JwtCustomAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
#Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Collection<String> splitScopes = Arrays.asList(jwt.getClaim("scope").split(" "));
for (String authority : splitScopes) {
grantedAuthorities.add(new SimpleGrantedAuthority(authority));
}
return grantedAuthorities;
}
}
Then update your spring security config as:
#Bean
JwtCustomAuthoritiesConverter jwtCustomAuthoritiesConverter;
#Bean
JwtAuthenticationConverter jwtAuthenticationConverter(){
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtCustomAuthoritiesConverter);
return jwtAuthenticationConverter;
}
...
http.oauth2ResourceServer((oauth2) ->
oauth2.jwt((jwt) -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
...
With the second option you can use your role names the way you want to in jwt token and the
hasAnyRole('ADMIN')
check should get ROLE_ADMIN for ADMIN scope instead of ROLE_SCOPE_ADMIN which is the case now.
Related
I'm creating an api for use in a pharmacy. When implementing the security, jwt token and filter for using security, an error occurs when validating requests and the token is simply not considered valid. It has an expiration time of 2 hours, the user is allowed to login, I do this via postman, I take the token that appears, I copy and paste it in the authorization validation, I make a protected request in postman and I simply take a forbidden403 . I'm using the post method to make this request, and the baerer Token to send it. In the code, as you can see, it has all the correct configuration for my filter to come before spring's filter and also has the configuration to demonstrate how far the code is running, with a sistem.out with the message("calling filter") What indicates that the code runs until the validation of the token and after the problem. Note: I'm using mysql, springboot 3.0 and java 17. The entire structure of the tables is already created and working, but I can't make any request besides the login after facing this validation error. Follow the code below:
User Entity: #Table(name = "users") #Entity(name = "User") #Getter #NoArgsConstructor #AllArgsConstructor
#EqualsAndHashCode(of ="id") public class User implements UserDetails{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String login;
private String senha;
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
#Override
public String getPassword() {
return senha;
}
#Override
public String getUsername() {
return login;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
Authentication Controller:
#RestController #RequestMapping("/login") public class AutenticacaoController {
#Autowired
private AuthenticationManager manager;
#Autowired
private TokenService tokenService;
#PostMapping
public ResponseEntity<DadosTokenJWT> efetuarLogin(#RequestBody #Valid DadosAutenticacao dados) {
var authenticationToken = new UsernamePasswordAuthenticationToken(dados.login(), dados.senha());
var authentication = manager.authenticate(authenticationToken);
var tokenJWT = tokenService.gerarToken((Usuario) authentication.getPrincipal());
return ResponseEntity.ok(new DadosTokenJWT(tokenJWT));
}
}
SecutiryConfigurations:
#Configuration #EnableWebSecurity public class SecutiryConfigurations {
#Autowired
private SecutiryFilter securityFilter;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
return http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeHttpRequests().requestMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
.and().addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
return configuration.getAuthenticationManager();
}
#Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
}
Token Service:
#Service public class TokenService {
#Value("${api.security.token.secret}")
private String secret;
public String gerarToken(Usuario usuario) {
try {
var algoritmo = Algorithm.HMAC256(secret);
return JWT.create().withIssuer("API remedios_api").withSubject(usuario.getLogin())
.withExpiresAt(dataExpiracao()).sign(algoritmo);
} catch (JWTCreationException exception) {
throw new RuntimeException("Erro ao gerar Token JWT", exception);
}
}
public String getSubject(String tokenJWT) {
try {
var algoritmo = Algorithm.HMAC256(secret);
return JWT.require(algoritmo).withIssuer("API remedios_api").build().verify(tokenJWT).getSubject();
} catch (JWTVerificationException exception) {
throw new RuntimeException("Token inválido ou expirado");
}
}
private Instant dataExpiracao() {
return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
}
}
SecurityFilter:
#Component public class SecutiryFilter extends OncePerRequestFilter{
#Autowired
private TokenService tokenService;
#Autowired
private UsuarioRepository repository;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
var tokenJWT = recuperarToken(request);
System.out.println("Chamando Filter");
if(tokenJWT != null) {
var subject = tokenService.getSubject(tokenJWT);
var usuario = repository.findByLogin(subject);
var authentication = new UsernamePasswordAuthenticationToken(usuario, null, usuario.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("Logado na requisição");
}
filterChain.doFilter(request, response);
}
private String recuperarToken(HttpServletRequest request) {
var authorizationHeader = request.getHeader("Authorization");
if( authorizationHeader != null) {
return authorizationHeader.replace("Bearer", "");
}
return null;
}
I login via postman, get the token code and paste it in the authorization to then use that token in some other request that is also protected via spring security.
I hope the request is accepted and released after validating the token in the request header.
I have a UserController that receives a UserDTO and creates/updates the user in the DB. The problem I'm getting is that I also have a login, and when I insert the username and password on the login form, I always get the 'Wrong Password.' exception, despite the credentials being correctly inserted.
One thing I suspect is that BCrypt is to blame, since due to the fact that it generates random salt while encoding, maybe, just maybe, the cipher text ends up being different and stuff, which is weird, since I assume that it should work. I want to know how can I fix this problem of the hashing being different & not being able to validate the userCredentials
I have tried for example encoding the received password and using the matches method via my autowired passwordEncoder, and I'm using my own authProvider.
Here's the code, let me know if you need anything else.
CustomAuthProvider.java
#Service
public class CustomAuthProvider implements AuthenticationProvider {
private final UserServiceImpl userServiceImpl;
private final BCryptPasswordEncoder passwordEncoder;
#Autowired
public CustomAuthProvider(UserServiceImpl userServiceImpl, BCryptPasswordEncoder passwordEncoder) {
this.userServiceImpl = userServiceImpl;
this.passwordEncoder = passwordEncoder;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails userDetails = userServiceImpl.loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDetails.getPassword())) { //The problem is here evidently.
throw new BadCredentialsException("Wrong password.");
}
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Also, here's the loadUserByUsername method:
UserServiceImpl.java
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDTO user = this.getUserByUsername(username);
User anUser = convertToUser(user);
ModelMapper modelMapper = new ModelMapper();
return modelMapper.map(anUser,UserPrincipal.class);
}
}
And here is the save method I use to save and update users, as well as the LoginController;
#Override
public void save(UserDTO user) {
User aUser = this.convertToUser(user);
aUser.setPassword(passwordEncoder.encode(aUser.getPassword()));
this.userRepository.save(aUser); }
LoginController.java:
#RestController
public class LoginController{
private final CustomAuthProvider providerManager;
#Autowired
public LoginController(CustomAuthProvider providerManager) {
this.providerManager = providerManager;
}
#GetMapping("/login")
public String login() {
return "login";
}
#PostMapping("/login")
public String login(#RequestParam("username") #NotBlank String username,
#RequestParam("password") #NotBlank String password, Model model) {
if(username == null || password == null) { //This is probably not necessary
model.addAttribute("error", "Invalid credentials");
return "login";
}
try {
Authentication auth = providerManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
SecurityContextHolder.getContext().setAuthentication(auth);
return "redirect:/notes";
} catch (AuthenticationException e) {
model.addAttribute("error", "Invalid credentials");
return "login";
}
}
}
UserPrincipal.java
#Data
public class UserPrincipal implements Serializable , UserDetails {
int id;
private String username;
private String password;
private Date accountCreationDate = new Date();
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
#Override
public boolean isAccountNonExpired() {
return false;
}
#Override
public boolean isAccountNonLocked() {
return false;
}
#Override
public boolean isCredentialsNonExpired() {
return false;
}
#Override
public boolean isEnabled() {
return false;
}
}
UserDTO.java
#Data
public class UserDTO implements Serializable {
int id;
private String username;
private String password;
private List<Note> notes = new ArrayList<>();
}
I read several issues related to this topic, like
Spring Boot PasswordEncoder.matches always false
Spring Security - BcryptPasswordEncoder
Inconsistent hash with Spring Boot BCryptPasswordEncoder matches() method
How can bcrypt have built-in salts?
Decode the Bcrypt encoded password in Spring Security to deactivate user account
but none of those helped me solve my issue and there was no real solution to the problem since most of them don't even have an accepted answer.
EDIT: Found out that the 'matches' method only works if I insert the hashed password, not the raw password.
Found out my mistake:
The setPassword method in the User class was re-hashing the hashed password which was already being hashed on the save method, thus the modelMapper.map() method used that setPassword method, therefore the passwords never matched and the password I got from the user class never matched the actual password I could see on my database.
I 'm coding a back-end/APIRest, part of an personal app.
I'm trying to set an authentication by JWT, with roles : ADMIN and USER. because i want to filter the following endpoint only for role ADMIN : /users/all
But when i send my http request from Insomnia App, with localhost:7777/users/all i have an error 403 forbidden. I don't know why.
Just bellow, this is a generated JWT by my sended request, you can check it on jwt.io :
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJrZW50MSIsInJvbGVzIjoiQURNSU4iLCJleHAiOjE2NjExNTUxNDUyOTUsImlhdCI6MTY2MDA3NTE0NTI5NX0.RGJzJVkM6bB0g6YlK6FFMzbjjTZ8qPqGf9pfHMeKHfyDV_OM9lF1w8SDzys3SC-iNdBMgXMjVP972URcISwJ_A
/CONCERNED ENDPOINT CONTROLLER/
#RestController
#RequestMapping("/users")
public class AppUserController {
#RolesAllowed("ADMIN")
#GetMapping("/all")
public ResponseEntity<List<AppUserListDto>> getAllAppUsers() {
logger.info("GET /users/all");
return new ResponseEntity<List<AppUserListDto>>(appUserServiceImpl.getAllUsers(), HttpStatus.OK);
}
}
/SECURITY CONFIGURATION/
#EnableWebSecurity
#Configuration
#EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class SecurityConfiguration {
#Autowired
RestAuthenticationEntryPoint restAuthenticationEntryPoint;
#Autowired
JwtFilter jwtFilter;
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable().exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/users/add").permitAll()
.antMatchers("/users/all").hasRole("ADMIN")
.anyRequest().authenticated();
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ADMIN > USER";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
#Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
}
/MY USER DETAILS SERVICE/
#Service
public class MyUserDetailService implements UserDetailsService {
#Autowired
AppUserRepository appUserRepository;
#Autowired
RoleRepository roleRepository;
#Override
public UserDetails loadUserByUsername(String appUsername) throws UsernameNotFoundException {
AppUserEntity appUser = appUserRepository.findByAppUsername(appUsername);
if (appUser == null) {
return new org.springframework.security.core.userdetails.User(
" ", " ", true, true, true, true,
getAuthorities(Arrays.asList(
roleRepository.findByRoleName("USER"))));
}
return new org.springframework.security.core.userdetails.User(
appUser.getAppUsername(), appUser.getPassword(), true, true, true,
true, getAuthorities(appUser.getRoles()));
}
private Collection<? extends GrantedAuthority> getAuthorities(
Collection<Role> roles) {
return getGrantedAuthorities(getPrivileges(roles));
}
private List<String> getPrivileges(Collection<Role> roles) {
List<String> privileges = new ArrayList<>();
List<Privilege> collection = new ArrayList<>();
for (Role role : roles) {
privileges.add(role.getRoleName());
collection.addAll(role.getPrivileges());
}
for (Privilege item : collection) {
privileges.add(item.getName());
}
return privileges;
}
private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String privilege : privileges) {
authorities.add(new SimpleGrantedAuthority(privilege));
}
return authorities;
}
}
/JWT UTILS/
#Component
public class JwtUtils {
#Autowired
AppUserRepository appUserRepository;
long JWT_VALIDITY = 5 * 60 * 60 * 60;
#Value("${jwt.secret}")
String secret;
private final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
public String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles",authentication.getAuthorities().stream().map(role -> role.getAuthority()).findFirst().orElseThrow(NoSuchElementException::new));
claims.put("iat",new Date(System.currentTimeMillis()));
claims.put("exp", new Date(System.currentTimeMillis() + JWT_VALIDITY * 1000));
claims.put("sub", authentication.getName());
Map<String, Object> headerJwt = new HashMap<>();
headerJwt.put("alg", "HS512");
headerJwt.put("typ", "JWT");
//TODO Later : Header, claims, jwt... will be Base64urlEncoded
return Jwts.builder()
.setHeader(headerJwt)
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
AppUserEntity appUser = appUserRepository.findByAppUsername(claims.getSubject());
logger.info("THIS IS THE SUBJECT FROM CLAIMS : {}", claims.getSubject());
Collection<? extends GrantedAuthority> authorities = getAuthorities(appUser.getRoles());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
private Collection<? extends GrantedAuthority> getAuthorities(
Collection<Role> roles) {
return getGrantedAuthorities(getPrivileges(roles));
}
private List<String> getPrivileges(Collection<Role> roles) {
List<String> privileges = new ArrayList<>();
List<Privilege> collection = new ArrayList<>();
for (Role role : roles) {
privileges.add(role.getRoleName());
collection.addAll(role.getPrivileges());
}
for (Privilege item : collection) {
privileges.add(item.getName());
}
return privileges;
}
private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String privilege : privileges) {
authorities.add(new SimpleGrantedAuthority(privilege));
}
return authorities;
}
}
/JWT CONTROLLER/
#RestController
public class JwtController {
#Autowired
JwtUtils jwtUtils;
#Autowired
AuthenticationManagerBuilder authenticationManagerBuilder;
#PostMapping("/login")
public ResponseEntity<?> createAuthToken(#RequestBody JwtRequest jwtRequest) {
Authentication authentication = logUser(jwtRequest.getAppUsername(), jwtRequest.getPassword());
String jwt = jwtUtils.generateToken(authentication);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(AUTHORIZATION_HEADER, "Bearer " + jwt);
Object principal = authentication.getPrincipal();
return new ResponseEntity<>(new JwtResponse(((User) principal).getUsername()), httpHeaders, HttpStatus.OK);
}
public Authentication logUser(String appUsername, String password) {
Authentication authentication = authenticationManagerBuilder.getObject()
.authenticate(new UsernamePasswordAuthenticationToken(appUsername, password));
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
}
/JWT FILTER/
#Component
public class JwtFilter extends OncePerRequestFilter {
#Autowired
JwtUtils jwtUtils;
public static final String AUTHORIZATION_HEADER = "Authorization";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String jwt = resolveToken(request);
if (StringUtils.hasText(jwt)) {
Authentication authentication = jwtUtils.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Your decoded token looks like this :
{
"sub": "kent1",
"roles": "ADMIN",
"exp": 1661155145295,
"iat": 1660075145295
}
As far as I can see, the problem is in the user roles. Your roles should be in this format:
ROLE_ADMIN, or ROLE_CUSTOMER, etc.. (notice prefix ROLE_)
With #RolesAllowed("ADMIN")annotation being set, this part :
.antMatchers("/users/all").hasRole("ADMIN")
is not needed.
BTW, in your JwtUtils class I noticed this line of code:
claims.put("roles",authentication.getAuthorities());
With this line of code, you are setting this
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
list as a value for your "roles" key in map.
Since this list will contain whole GrantedAuthority objects, that's not what Spring expects.
Assuming that your role table looks like this:
+---------+------------+
| id_role | role_name |
+---------+------------+
| 1 | ROLE_ADMIN |
| 2 | ROLE_USER |
+---------+------------+
You can do something like this :
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
List<String> roles = new ArrayList<>();
for(GrantedAuthority authority: authorities){
if(authority.getAuthority().startsWith("ROLE")){
roles.add(authority.getAuthority());
}
}
claims.put("roles",roles);
and that will resolve problem that I just described.
I have the following two classes which provide the JWT authentication mechanisem.
CustomDelegatingAuthenticationProvider
#Singleton
#Replaces(value = DelegatingAuthenticationProvider.class)
public class CustomDelegatingAuthenticationProvider extends DelegatingAuthenticationProvider {
/**
* #param userFetcher Fetches users from persistence
* #param passwordEncoder Collaborator which checks if a raw password matches an encoded password
* #param authoritiesFetcher Fetches authorities for a particular user
*/
public CustomDelegatingAuthenticationProvider(UserFetcher userFetcher, PasswordEncoder passwordEncoder, AuthoritiesFetcher authoritiesFetcher) {
super(userFetcher, passwordEncoder, authoritiesFetcher);
}
#Override
protected Publisher<AuthenticationResponse> createSuccessfulAuthenticationResponse(AuthenticationRequest authenticationRequest, UserState userState) {
if (userState instanceof UserMember) {
UserMember user = (UserMember) userState;
return Flowable
.fromPublisher(authoritiesFetcher.findAuthoritiesByUsername(user.getUsername()))
.map(authorities -> new HDSUser(user.getUsername(), authorities, user.getId()));
}
return super.createSuccessfulAuthenticationResponse(authenticationRequest, userState);
}
}
CustomJWTClaimsSetGenerator
#Singleton
#Replaces(value = JWTClaimsSetGenerator.class)
public class CustomJWTClaimsSetGenerator extends JWTClaimsSetGenerator {
CustomJWTClaimsSetGenerator(TokenConfiguration tokenConfiguration, #Nullable JwtIdGenerator jwtIdGenerator, #Nullable ClaimsAudienceProvider claimsAudienceProvider) {
super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider);
}
protected void populateWithUserDetails(JWTClaimsSet.Builder builder, UserDetails userDetails) {
super.populateWithUserDetails(builder, userDetails);
if (userDetails instanceof HDSUser) {
builder.claim("userId", ((HDSUser) userDetails).getId());
}
}
}
The default response to the client looks like this:
My question. How can I extend the class to return all user attributes? Besides username I want to have the user id.
UPDATE
HDS user class which gathers the DB id
#CompileStatic
public class HDSUser extends UserDetails {
private long id;
public HDSUser(String username, Collection<String> roles, long id) {
super(username, roles);
this.id = id;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}
To extend the returned data you need to extend (implement custom) TokenRenderer as well as a custom version of the AccessRefreshToken.
Just as a simple example see the following code snipped which will extend the default access token payload with userId field.
First, create a custom AccessRefreshToken class with additional fields which are required.
#Introspected
#Getter
#Setter
public class CustomAccessRefreshToken extends BearerAccessRefreshToken {
// the new field which will be in the response
private String userId;
public CustomAccessRefreshToken(String username,
Collection<String> roles,
Integer expiresIn,
String accessToken,
String refreshToken,
String tokenType
) {
super(username, roles, expiresIn, accessToken, refreshToken, tokenType);
}
}
Next, we will need a TokenRenderer which will be used by the underlying subsystem to generate our custom token.
#Singleton
#Replaces(value = BearerTokenRenderer.class)
public class CustomTokenRenderer implements TokenRenderer {
private static final String BEARER_TOKEN_TYPE = HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER;
#Override
public AccessRefreshToken render(Integer expiresIn, String accessToken, #Nullable String refreshToken) {
return new AccessRefreshToken(accessToken, refreshToken, BEARER_TOKEN_TYPE, expiresIn);
}
#Override
public AccessRefreshToken render(Authentication authentication, Integer expiresIn, String accessToken, #Nullable String refreshToken) {
CustomAccessRefreshToken token = new CustomAccessRefreshToken(authentication.getName(), authentication.getRoles(), expiresIn, accessToken, refreshToken, BEARER_TOKEN_TYPE);
// here just take the user data from Authentication object or access any other service
token.setUserId("Some user id");
return token;
}
}
That's it )) Just implement render() method the way you want and add as many custom fields as needed.
The response from the given example will look like
{
"username": "sherlock",
"userId": "Some user id",
"access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTYzNjk5MTgzMSwicm9sZXMiOltdLCJpc3MiOiJtaWNyb25hdXRndWlkZSIsImV4cCI6MTYzNjk5NTQzMSwiaWF0IjoxNjM2OTkxODMxfQ.Cat1CTsUZkCj-OHGafiefNm1snPsALoaNw9y2xwF5Pw",
"token_type": "Bearer",
"expires_in": 3600
}
If you are on the older version of the Micronaut v1.x the TokenRenderer will look like this.
#Singleton
#Replaces(value = BearerTokenRenderer.class)
public class CustomTokenRenderer implements TokenRenderer {
private static final String BEARER_TOKEN_TYPE = HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER;
public AccessRefreshToken render(Integer expiresIn, String accessToken, String refreshToken) {
return new AccessRefreshToken(accessToken, refreshToken, BEARER_TOKEN_TYPE, expiresIn);
}
public AccessRefreshToken render(UserDetails userDetails, Integer expiresIn, String accessToken, String refreshToken) {
CustomAccessRefreshToken token = new CustomAccessRefreshToken(userDetails.getUsername(), userDetails.getRoles(), expiresIn, accessToken, refreshToken, BEARER_TOKEN_TYPE);
token.setUserId("Some user id, Some user id");
return token;
}
}
Using Spring Boot 1.3.1, I am having problems with #AuthenticationPrincipal.
This is my controller:
#RestController
#RequestMapping("/api/user")
public class UserController {
#RequestMapping("/")
public UserDto user(#AuthenticationPrincipal(errorOnInvalidType = true) User user) {
return UserDto.fromUser(user);
}
}
This is my custom User class:
#Entity()
#Table(name = "app_user")
public class User extends AbstractEntity<UserId> implements Serializable {
// ------------------------------ FIELDS ------------------------------
#NotNull
#Column(unique = true)
#Pattern(regexp = "[a-zA-Z_\\-\\.0-9]+")
#Size(min = 1, max = 20)
private String username;
private String password;
#Column(unique = true)
#Email
private String emailAddress;
#Enumerated(EnumType.STRING)
#NotNull
private UserRole role;
}
I also created a class to confirm to the UserDetails interface of Spring Security:
public class CustomUserDetails extends User implements UserDetails {
public CustomUserDetails(User user) {
super(user);
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Sets.newHashSet(new SimpleGrantedAuthority("ROLE_" + getRole().name()));
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
Then in my UserDetailsService:
#Override
public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.company.app.domain.account.User user = userRepository.findByUsername(username);
if (user != null) {
return new CustomUserDetails(user);
} else {
throw new UsernameNotFoundException(format("User %s does not exist!", username));
}
}
I have an integration test that works perfectly. However, running the application itself (from IntelliJ IDEA), does not work. I get an exception:
"CustomUserDetails{id=UserId{id=401868de-99ff-4bae-bcb6-225e3062ed33}} is not assignable to class com.company.app.domain.account.User"
But this is not true, since CustomUserDetails is a subclass of my custom User class.
Checking with the debugger, I see that this code in AuthenticationPrincipalArgumentResolver fails:
if (principal != null
&& !parameter.getParameterType().isAssignableFrom(principal.getClass())) {
The classloader of parameter.getParameterType() is an instance of RestartClassloader, while principal.getClass() has a classloader that is an instance of Launcher$AppClassLoader.
Is this a bug in Spring Boot or Spring Security or am I doing something wrong?
UPDATE:
I can confirm that disabling devtools of Spring Boot makes it work:
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(MyApplication.class, args);
}
To help anyone else who hits the same problem, this is a limitation in Spring Boot DevTools and Spring Security OAuth. It's being tracked in this issue: https://github.com/spring-projects/spring-boot/issues/5071