I'm building a login and register application with java and spring boot version 3.1.1, I implemented the entities:
@Entity
@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
@Table(name = "users")
public class User {
@Id
@Column(name = "user_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String userName;
private String email;
private String password;
private LocalDate registrationDate;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
private Boolean locked = false;
private Boolean enabled = true;
public User(String firstName, String lastName, String userName, String email, String password) {
this.firstName = firstName;
this.lastName = lastName;
this.userName = userName;
this.email = email;
this.password = password;
this.registrationDate = LocalDate.now();
}
}
@Entity
@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
public class Role {
@Id
@Column(name = "role_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private UserRole roleName;
}
public enum UserRole {
WAITER,
KITCHEN,
CASHIER,
ADMIN;
}
@Getter
@AllArgsConstructor
public class AppUserDetails implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
user.getRoles().forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role.getRoleName().name()));
});
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !user.getLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.getEnabled();
}
}
@Service
@AllArgsConstructor
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new AppUserDetails(userRepository.findByUserName(username).orElseThrow(() ->
new UsernameNotFoundException(String.format("User %s not found", username))));
}
}
And the JWT filter, configurations and so on:
@Service
public class JwtService {
@Value("${api.security.token.secret}")
private String secretKey;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateTokens(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date((System.currentTimeMillis() + 1000 * 60 * 24)))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims (String token) {
return Jwts.parserBuilder().setSigningKey(getSignInKey()).build().parseClaimsJws(token).getBody();
}
private Key getSignInKey() {
byte [] keysBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keysBytes);
}
}
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserService userDetailsService;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userName;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userName = jwtService.extractUsername(jwt);
if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userName);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfigurations {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
authorize -> authorize
.requestMatchers(HttpMethod.POST, "/api/v*/auth/register").hasAnyRole("ADMIN", "CASHIER")
.requestMatchers(HttpMethod.POST, "/api/v*/auth/authenticate").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Everytime I try to authenticate the token is generated with success:
@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/api/v1/auth")
public class AuthenticationController {
private final AuthenticationService authenticationService;
@PostMapping("/register")
@PreAuthorize("hasAnyRole('ADMIN', 'CASHIER')")
public ResponseEntity<AuthenticationResponse> register(@RequestBody RegistrationRequest request) {
return new ResponseEntity<>(authenticationService.register(request), HttpStatus.CREATED);
}
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
return new ResponseEntity<>(authenticationService.authenticate(request), HttpStatus.OK);
}
}
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final JwtService jwtService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final RolesRepository rolesRepository;
private final AuthenticationManager authenticationManager;
public AuthenticationResponse register(RegistrationRequest request) {
User user = new User(
request.getFirstName(),
request.getLastName(),
request.getUserName(),
request.getEmail(),
passwordEncoder.encode(request.getPassword())
);
request.getRoles().forEach(role -> user.getRoles().add(rolesRepository.findByRoleName(role).orElseThrow(
() -> new NoSuchElementException(String.format("Role %s was not found at database", role.name()))
)));
userRepository.save(user);
return AuthenticationResponse.builder().token("Testando assim").build();
}
// TODO add some errors to return message to user
// TODO add a constraint into username field
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
User user = userRepository.findByUserName(request.getUsername()).orElseThrow(
() -> new NoSuchElementException(String.format("Username %s not found", request.getUsername())));
var jwtToken = jwtService.generateTokens(new AppUserDetails(user));
return AuthenticationResponse.builder().token(jwtToken).build();
}
}
I registered a User directly at database with the admin role, but when I try to register another user at the system using the admin user generated token at the authenticate method, I receive the 403 message forbidden with not even acessing the register method, there is something I missed?
I can make the repository public and post the link if will be better.
Added the requests. Login to get the token:
Setting the bearer token at the request header for register:
Request body with the message:
Properties printscreen:
Spring return for the method:
Return without @PreAuthorize("hasAnyRole('ADMIN', 'CASHIER')") and with .permitAll():
After a long search and documentation reading I finally find out what was my issue. I mapped the entities and the roles just by the prefixes 'ADMIN', 'CASHIER', ... But when using the function hasAnyRoles
, the role should have the prefix role in their names like 'ROLE_ADMIN', 'ROLE_CASHIER' and not just the name. But there already is a method to use without the prefixes 'ROLE', the hasAuthority
method works with just the names!
So the solution was change the way I was mapping the chain filter method:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfigurations {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
authorize -> authorize
.requestMatchers(HttpMethod.POST, "/api/v*/auth/register").hasAnyAuthority("ADMIN", "CASHIER")
.requestMatchers(HttpMethod.POST, "/api/v*/auth/authenticate").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
In this example I used the correct method for the approach. And worked fine!