Create JWT security for two entities - java

I've got two entities as two type of account in my spring app:
Customer:
#Entity
#Table(name = "customer")
public class Customer {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
#Column(name = "customer_id",updatable = false)
private String customerId;
#NotBlank(message = "Nickname may not be blank")
#Size(min = 3, max = 20, message = "Nickname '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "nickname",updatable = false)
private String nickname;
#NotBlank(message = "City may not be blank")
#Size(min = 3, max = 25, message = "City '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "city")
private String city;
#NotBlank(message = "Phone Number may not be blank")
#Pattern(regexp="(^$|[0-9]{9})")
#Column(name = "phone_number",updatable = false)
private String phoneNumber;
#NotBlank(message = "Email may not be blank")
#Email
#Column(name = "mail",updatable = false)
private String mail;
#NotBlank(message = "Password may not be blank")
#Size(min = 5 , max = 30, message = "Password '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "password")
private String password;
// getters setters
}
and Specialist:
#Entity
#Table(name = "specialist")
public class Specialist {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
#Column(name = "specialist_id",updatable = false)
private String specialistId;
#NotBlank(message = "Name may not be blank")
#Size(min = 3, max = 20, message = "Name '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "name",updatable = false)
private String name;
#NotBlank(message = "Surname may not be blank")
#Size(min = 3, max = 20, message = "Name '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "surname",updatable = false)
private String surname;
#NotNull(message = "Province may not be blank")
#Column(name = "province")
private Province province;
#NotBlank(message = "City may not be blank")
#Size(min = 3, max = 25, message = "City '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "city")
private String city;
#NotBlank(message = "Profession may not be blank")
#Size(min = 3, max = 25, message = "Profession '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "profession")
private String profession;
#NotBlank(message = "Phone Number may not be blank")
#Pattern(regexp="(^$|[0-9]{9})")
#Column(name = "phone_number",updatable = false)
private String phoneNumber;
#NotBlank(message = "Email may not be blank")
#Email
#Column(name = "mail",updatable = false)
private String mail;
#NotBlank(message = "Password may not be blank")
#Size(min = 5 , max = 30, message = "Password '${validatedValue}' isn't correct => must be between {min} and {max} characters")
#Column(name = "password")
private String password;
#Column(name = "rate")
private HashMap<String,Double> rateStars;
#Range(min = 0 , max = 5)
#Column(name = "average_rate")
private Double averageRate;
//getters setters
}
As you can see this entities have got differently required fields.
Now I want to create JWT where specialist and customer have got other access to endpoints.
For example specialist has got other header than customer in front end.
And now i ask question to You, how connect two differently entities to JWT security? Because all of tutorials/posts in internet concerns JWT only to one User entity and then separate role.
I tried to solve this but I stand with this how to supersede User entity and change this to my Specialist and Customer entities:
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if(user == null) new UsernameNotFoundException("User not found");
return user;
}
#Transactional
public User loadUserById(Long id){
User user = userRepository.getById(id);
if(user == null) new UsernameNotFoundException("User not found");
return user;
}
#Component
public class UserValidator implements Validator {
#Override
public boolean supports(Class<?> aClass) {
return User.class.equals(aClass);
}
#Override
public void validate(Object object, Errors errors) {
User user = (User) object;
if(user.getPassword().length() < 6){
errors.rejectValue("password","Length","Password must be at least 6 characters");
}
if(!user.getPassword().equals(user.getConfirmPassword())){
errors.rejectValue("confirmPassword","Match","Passwords must match");
}
}
}
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
InvalidLoginResponse loginResponse = new InvalidLoginResponse();
String jsonLoginResponse = new Gson().toJson(loginResponse);
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(401);
httpServletResponse.getWriter().print(jsonLoginResponse);
}
}
public class JwtAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenProvider tokenProvider;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try{
String jwt = getJWTFromRequest(httpServletRequest);
if(StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)){
Long userId = tokenProvider.getUserIdFromJWT(jwt);
User userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,null, Collections.emptyList()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}catch (Exception ex){
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
private String getJWTFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(HEADER_STRING);
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)){
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
#Component
public class JwtTokenProvider {
public String generateToken(Authentication authentication){
User user = (User)authentication.getPrincipal();
Date now = new Date(System.currentTimeMillis());
Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);
String userId = Long.toString(user.getId());
Map<String,Object> claims = new HashMap<>();
claims.put("id",(Long.toString(user.getId())));
claims.put("username", user.getUsername());
claims.put("fullName", user.getFullName());
return Jwts.builder()
.setSubject(userId)
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public boolean validateToken(String token) {
try{
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
}catch (SignatureException ex){
System.out.println("Invalid JWT Signature");
}catch (MalformedJwtException ex){
System.out.println("Invalid JWT Token");
}catch (ExpiredJwtException ex){
System.out.println("Expired JWT token");
}catch (UnsupportedJwtException ex){
System.out.println("Unsupported JWT token");
}catch (IllegalArgumentException ex){
System.out.println("JWT claims string is empty");
}
return false;
}
public Long getUserIdFromJWT(String token){
Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
String id = (String)claims.get("id");
return Long.parseLong(id);
}
}
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint unAuthorizedHandler;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(){
return new JwtAuthenticationFilter();
}
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Override
#Bean(BeanIds.AUTHENTICATION_MANAGER)
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unAuthorizedHandler).and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().frameOptions().sameOrigin() //To enable H2 db
.and()
.authorizeRequests()
.antMatchers(
"/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers(SIGN_UP_URLS).permitAll()
.antMatchers(H2_URL).permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}

all of tutorials/posts in internet concerns JWT only to one User entity and then separate role.
You may store users in two tables but they are users of two different roles- Customer and Specialist
how connect two differently entities to JWT security?
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* logic to check if this username belongs to Customer/Specialist
* based on that create a new Authority - ROLE_CUSTOMER or ROLE_SPECIALIST
*/
}
specialist has got other header than customer in front end.
These are Menu if I am not wrong. Based on Authority that you passed using above method, you can show Menu.

Related

Multi-Tenancy whit JWT autentication and validation of user for tenant

I have a project running with JWT authentication, it works, but now I need to implement Multi-Tenancy using the following approach:
Requirements:
A user can have access to one or more tenants
Access permissions are defined by user and tenant
Getting subdomain through #RequestAttribute in requests
Generate the token containing the tenant ID (subdomain).
Validate the tenant on all requests
Implemented:
Created JWT Autentication.
Created TenantInterceptor.
Getting subdomain using #RequestAttribute on requests.
Created existsByUsernameAndSubdomain validation.
I'm having trouble implementing this new feature, can you point me to an implementation example or tutorial that can help me?
I thank you for your help!
Below are my classes or if you prefer clone on GitHub!
My classes
Models:
/** ERole **/
public enum ERole {
ROLE_USER,
ROLE_MODERATOR,
ROLE_ADMIN
}
/** Role **/
#Entity
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "roles")
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Enumerated(EnumType.STRING)
#Column(length = 20)
private ERole name;
}
/** Tenant **/
#Entity
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "tenants",
uniqueConstraints = {
#UniqueConstraint(columnNames = "subdomain", name = "un_subdomain")
})
public class Tenant {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank
#Size(max = 20)
private String subdomain;
#NotBlank
private String name;
}
/** User **/
#Entity
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users",
uniqueConstraints = {
#UniqueConstraint(columnNames = "username", name = "un_username")
})
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank
#Size(max = 20)
private String username;
#NotBlank
#Size(max = 120)
#JsonIgnore
private String password;
// Remove
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "users_roles",
joinColumns = {#JoinColumn(name = "user_id",
foreignKey = #ForeignKey(name = "fk_users_roles_users1"))},
inverseJoinColumns = {#JoinColumn(name = "role_id",
foreignKey = #ForeignKey(name = "fk_users_roles_roles1"))})
private Set<Role> roles = new HashSet<>();
// Include
#EqualsAndHashCode.Exclude
#OneToMany(mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY)
#JsonManagedReference
private List<UserTenant> tenants = new ArrayList<>();
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
/** UserTenant **/
#Entity
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users_tenants",
uniqueConstraints = {
#UniqueConstraint(columnNames = "user_id", name = "un_user_id"),
#UniqueConstraint(columnNames = "tenant_id", name = "un_tenant_id")
})
public class UserTenant {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id",
nullable = false,
foreignKey = #ForeignKey(
name = "fk_users_tenants_user1"))
#JsonBackReference
private User user;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "tenant_id",
nullable = false,
foreignKey = #ForeignKey(
name = "fk_users_tenants_tenant1"))
#JsonBackReference
private Tenant tenant;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "users_tenants_roles",
joinColumns = {#JoinColumn(name = "user_tenant_id",
foreignKey = #ForeignKey(name = "fk_users_tenants_user_tenant1"))},
inverseJoinColumns = {#JoinColumn(name = "role_id",
foreignKey = #ForeignKey(name = "fk_users_tenants_roles1"))})
private Set<Role> roles = new HashSet<>();
}
Payloads:
/** LoginRequest **/
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class LoginRequest {
#NotBlank
private String username;
#NotBlank
private String password;
}
/** SignupRequest **/
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class SignupRequest {
#NotBlank
#Size(max = 20)
private String username;
#NotBlank
#Size(max = 40)
private String password;
private Set<String> role;
}
/** JwtResponse **/
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class JwtResponse {
private Long id;
private String username;
private List<String> roles;
private String tokenType = "Bearer";
private String accessToken;
public JwtResponse(String accessToken, Long id, String username,
List<String> roles) {
this.id = id;
this.username = username;
this.roles = roles;
this.accessToken = accessToken;
}
}
/** MessageResponse **/
#Data
#Builder
#NoArgsConstructor
public class MessageResponse {
private String message;
public MessageResponse(String message) {
this.message = message;
}
}
Repositories:
/** RoleRepository **/
#Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(ERole name);
}
/** UserRepository **/
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByUsername(String username);
}
/** UserTenantRepository **/
#Repository
public interface UserTenantRepository extends JpaRepository<UserTenant, Long> {
#Query("SELECT ut FROM UserTenant ut WHERE ut.user.username = :username AND ut.tenant.subdomain = :subdomain ")
Optional<UserTenant> findByUserAndSubdomain(String username, String subdomain);
#Query("SELECT " +
"CASE WHEN COUNT(ut) > 0 THEN true ELSE false END " +
"FROM UserTenant ut " +
"WHERE ut.user.username = :username " +
"AND ut.tenant.subdomain = :subdomain ")
Boolean existsByUsernameAndSubdomain(String subdomain, String username);
}
Services:
/** AuthService **/
#Service
#RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
private final PasswordEncoder encoder;
private final RoleRepository roleRepository;
public JwtResponse authenticateUser(String subdomain, LoginRequest loginRequest) {
System.out.println(subdomain);
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
System.out.println(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
return new JwtResponse(jwt,
userDetails.getId(),
userDetails.getUsername(),
roles);
}
#Transactional
public MessageResponse registerUser(SignupRequest signUpRequest) {
// Create new user's account
User user = new User(
signUpRequest.getUsername(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRole();
Set<Role> roles = new HashSet<>();
if (strRoles == null) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
switch (role) {
case "admin":
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
break;
case "mod":
Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(modRole);
break;
default:
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return new MessageResponse("User registered successfully!");
}
}
/** UserDetailsImpl **/
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private final Long id;
private final String username;
#JsonIgnore
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getPassword(),
authorities);
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Long getId() {
return id;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return username;
}
#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 user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}
}
/** UserDetailsServiceImpl **/
#Service
#RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
private final UserTenantRepository userTenantRepository;
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(user);
}
}
Controller
/** AuthController **/
#RestController
#RequestMapping("/auth")
#RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
private final UserRepository userRepository;
private final UserTenantRepository userTenantRepository;
#PostMapping("/signin")
public ResponseEntity<?> authenticateUser(
#RequestAttribute String subdomain,
#Valid #RequestBody LoginRequest loginRequest
) {
if (!userTenantRepository.existsByUsernameAndSubdomain(subdomain, loginRequest.getUsername())) {
return ResponseEntity
.badRequest()
.body(new MessageResponse("Unauthorized: This username and tenant is not authorized!"));
}
return ResponseEntity.ok(authService.authenticateUser(subdomain, loginRequest));
}
#PostMapping("/signup")
public ResponseEntity<?> registerUser(#Valid #RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity
.badRequest()
.body(new MessageResponse("Error: Username is already taken!"));
}
return ResponseEntity.ok(authService.registerUser(signUpRequest));
}
}
JWT:
/** AuthEntryPointJwt **/
#Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: incorrect username or password");
}
}
/** AuthTokenFilter **/
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private UserDetailsServiceImpl userDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response
, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
String serverName = request.getServerName();
String subdomain = serverName.substring(0, serverName.indexOf("."));
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
System.out.println(userDetails);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
e.printStackTrace();
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
/** JwtUtils **/
#Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
#Value("${example.app.jwtSecret}")
private String jwtSecret;
#Value("${example.app.jwtExpirationMs}")
private int jwtExpirationMs;
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder()
.setSubject((userPrincipal.getUsername()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}
Utils:
/** TenantInterceptor **/
public class TenantInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String serverName = request.getServerName();
String tenantId = serverName.substring(0, serverName.indexOf("."));
request.setAttribute("subdomain", tenantId);
return true;
}
}
/** WebSecurityConfig **/
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig implements WebMvcConfigurer {
final
UserDetailsServiceImpl userDetailsService;
private final AuthEntryPointJwt unauthorizedHandler;
public WebSecurityConfig(UserDetailsServiceImpl userDetailsService, AuthEntryPointJwt unauthorizedHandler) {
this.userDetailsService = userDetailsService;
this.unauthorizedHandler = unauthorizedHandler;
}
#Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(
"/auth/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/configuration/**",
"/swagger-resources/**",
"/webjars/**",
"/api-docs/**").permitAll()
.antMatchers("/api/**").authenticated()
.anyRequest().authenticated();
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantInterceptor());
}
}
I was able to solve the problem by modifying the loadUserByUsername method in the UserDetailsServiceImpl.
See the implementation details on GitHub!
#Service
#RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserTenantRepository userTenantRepository;
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Getting subdomain from request attributes
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))
.getRequest();
String serverName = request.getServerName();
String subdomain = serverName.substring(0, serverName.indexOf("."));
UserTenant userTenant = userTenantRepository.findByUserAndSubdomain(username, subdomain)
.orElseThrow(() -> new UsernameNotFoundException(
"UserTenant Not Found with username: " + username + " and " + subdomain));
// Getting Rules from the UserTenant
List<GrantedAuthority> authorities = userTenant.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(
userTenant.getUser().getId(),
userTenant.getUser().getUsername(),
userTenant.getUser().getPassword(),
authorities
);
}
}
Inserts in Database
INSERT INTO roles(id, name)
VALUES (1 ,'ROLE_USER'),
(2, 'ROLE_MODERATOR'),
(3, 'ROLE_ADMIN');
INSERT INTO tenants (id, name, subdomain)
VALUES (1, 'Tenant 1', 'tenant1'),
(2, 'Tenant 2', 'tenant2');
# user, password
# user1, user1
# user2, user2
INSERT INTO users (id, username, password)
VALUES (1, 'user1', '$2a$10$wFMJLxdXKGRa8lJO6k2DAOnW9HstAPoHecXUNkDyYNeaNnZJAz.hy'),
(2, 'user2', '$2a$10$Z9/wLkmf5IwfjJqIQU6X.OBFg3TCBUyk3bdfgkGjU0.HI5kVibZxG');
INSERT INTO users_tenants (id, tenant_id, user_id)
VALUES (1, 1, 1),
(2, 2, 2);
INSERT INTO users_tenants_roles (user_tenant_id, role_id)
VALUES (1, 2),
(1, 3),
(2, 1);
INSERT INTO items (id, tenant_id, name)
VALUES (1, 1, 'Product 1 in Tenant 1'),
(2, 1, 'Product 2 in Tenant 1'),
(3, 2, 'Product 1 in Tenant 2'),
(4, 2, 'Product 2 in Tenant 2');
Validations in Postman
Created token variable in Postman:
Set token value in Postman variable:
Added Authorization variable in requests headers:
Validating if the domain exists in sign in:
Validating user access permission on tenant:
Authorized login:
Getting items of tenant1:
Trying to get tenant2 list of items without being logged in tenant2 and having access permissions:
Logging in with user2 in tenant2:
Getting items list tenant2
Trying to get tenant1 list of items without being logged in tenant1 and having access permissions:

Spring Security configuration with role-based access returns a 403 error

I am developing a small application with Spring Security configuration and JWT token-based authorization and encountered a problem - when using hasRole() or hasPermission() methods to differentiate access to requests, even if the authorized user has the roles that meet the restrictions, the request still returns a 403 code.
Implementing of UserDetails and UserDetailsService
#RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final User user;
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles();
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getUsername();
}
#Override
public boolean isAccountNonExpired() {
return user.getIsEnable();
}
#Override
public boolean isAccountNonLocked() {
return user.getIsEnable();
}
#Override
public boolean isCredentialsNonExpired() {
return user.getIsEnable();
}
#Override
public boolean isEnabled() {
return user.getIsEnable();
}
}
#Service
#RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String exceptionMsg = String.format("User with username '%s' not found", username);
return new UserDetailsImpl(userRepository.findUserByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(exceptionMsg)));
}
}
User entity and Roles
#Entity(name = "users")
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Setter
#EqualsAndHashCode(of = {"id", "username"})
public class User {
#Transient
private final String MAIL_REGEX =
"^\\w+([\\.-]?\\w+)*#\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$";
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(unique = true, nullable = false)
#NotBlank(message = "Username cannot be empty")
#NotNull(message = "Username cannot be empty")
#Size(min = 1, max = 90)
private String username;
#Column(nullable = false)
#NotBlank(message = "Password cannot be empty")
#NotNull(message = "Password cannot be empty")
#Size(min = 8, max = 100)
private String password;
#Column(unique = true, nullable = false)
#Email(message = "Invalid email address", regexp = MAIL_REGEX)
#NotBlank(message = "Email cannot be empty")
private String email;
#ElementCollection(fetch = EAGER)
private Set<Role> roles = Set.of(Role.USER, Role.ADMIN, Role.SUPER_ADMIN);
#Column(nullable = false)
private Boolean isEnable = true;
}
public enum Role implements GrantedAuthority {
USER, ADMIN, SUPER_ADMIN;
#Override
public String getAuthority() {
return name();
}
}
Security configuration
#Configuration
public class SecurityConfig {
#Value("${jwt.public.key}")
RSAPublicKey publicKey;
#Value("${jwt.private.key}")
RSAPrivateKey privateKey;
#Bean
PasswordEncoder encoder() {
return new BCryptPasswordEncoder(10);
}
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeHttpRequests().requestMatchers("/login", "/register").permitAll();
http.authorizeHttpRequests().requestMatchers("/hello").hasAuthority("USER");
http.httpBasic(Customizer.withDefaults());
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
http.sessionManagement(session ->
session.sessionCreationPolicy(STATELESS));
http.exceptionHandling()
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler());
return http.build();
}
#Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
#Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwkSource);
}
}
If I try to query the /hello path, it returns a forbidden error, despite the fact that the authorized user has all the possible roles. Can you tell me what could be the problem? A long time ago I did everything exactly as it is now and everything worked without errors.
Tried to add ROLE_ prefix, make roles without enum class with clear string format, applied both hasRole() and hasAuthority() methods

How does JWT get the User role assigned to a user to navigate to the appropriate Dashboard?

I can get a JWT response in postman displaying the Token. But it's not coming with the User details( Role) assigned to the user. Here is the backend codes :
User Class
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String firstName ;
private String lastName;
private String username;
private String password;
private String Gender;
private String phoneNumber;
private String email;
private String branch;
private Calendar createdDate;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(
name = "users_roles",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id")
)
private Set<UserRole> userRole = new HashSet<>();
#Enumerated(EnumType.STRING)
private UserStatus status;
User Controller
public class UserController {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
UserAccountService userAccountService;
#PostMapping(value="/validate")
public ResponseEntity<?> createAuthenticationToken(#RequestBody MyUserDetails authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails userDetails = userAccountService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
This is the React front-end Authentication class
import "assets/css/style.css";
import axios from "axios";
const baseURL = "http://localhost:8080/validate";
export default class Auth extends React.Component {
handleSubmit = e => {
e.preventDefault();
const data = {
username: this.username,
password: this.password
}
axios.post(baseURL,data)
.then(res =>{
localStorage.setItem('token', res.data.jwt)
this.props.history.push('/admin/index')
})
.catch(err => {
console.log(err)
})
}
This is the Admin.Js class in react where it should navigate to if the User is of UserRole type Admin.
const Admin = (props) => {
const mainContent = React.useRef(null);
const location = useLocation();
React.useEffect(() => {
const config = {
headers:{
Authorization: 'Bearer ' + localStorage.getItem('token')
}
}
axios.get('http://localhost:8080/list/User/',config).then(
res => {
console.log(res.data)
},
err => {
console.log(err)
}
)
document.documentElement.scrollTop = 0;
document.scrollingElement.scrollTop = 0;
mainContent.current.scrollTop = 0;
}, [location]);
const getRoutes = (routes) => {
return routes.map((prop, key) => {
if (prop.layout === "/admin") {
return (
<Route
path={prop.layout + prop.path}
component={prop.component}
key={key}
/>
);
} else {
return null;
}
});
};

How to validate data in service layer in spring boot

Controller:
#RestController
#RequestMapping(path = "api/v1/users")
public class UserController {
#Autowired
UserService userService;
#PostMapping("/register")
public ResponseEntity<Map<String, String>> registerUser(#RequestBody Map<String, Object> userMap){
Map<String, String> map = new HashMap<>();
try {
String first_name = (String) userMap.get("first_name");
String last_name = (String) userMap.get("last_name");
Users user = userService.registerUser(first_name, last_name);
map.put("message:","User registered successfully");
}catch(Exception e) {
map.put("message:",e.getMessage());
return new ResponseEntity<>(map, HttpStatus.UNAUTHORIZED);
}
return new ResponseEntity<>(map, HttpStatus.OK);
}
}
Service Layer:
#Service
#Transactional
public class UserServicesImpl implements UserService{
#Autowired
UserRepository userRepository;
#Override
public Users registerUser(String first_name, String last_name) throws EtAuthException {
String username = first_name+99;
Integer userId = userRepository.create(first_namea, last_name);
return userRepository.findById(userId);
}
}
Repository:
#Repository
public class UserRepositoryImpl implements UserRepository {
private final UserRepositoryBasic userRepositoryBasic;
public UserRepositoryImpl(UserRepositoryBasic userRepositoryBasic) {
this.userRepositoryBasic = userRepositoryBasic;
}
#Override
public Integer create(String first_name, String last_name) throws EtAuthException {
try{
Users insertData = userRepositoryBasic.save(new Users(first_name, last_name));
return insertData.getId();
}catch (Exception e){
throw new EtAuthException(e.getMessage());
}
}
}
Model/Entity:
#Entity
#Table(name="users")
public class Users {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", updatable = false, nullable = false)
private Integer id;
#NotBlank(message="First name can not be empty")
#Column(nullable = false, length = 40)
private String first_name;
#NotBlank(message="Last name can not be empty")
#Column(nullable = false, length = 40)
private String last_name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getFirst_name() {
return first_name;
}
public void setFirst_name(String first_name) {
this.first_name = first_name;
}
public String getLast_name() {
return last_name;
}
public void setLast_name(String last_name) {
this.last_name = last_name;
}
public Users(String first_name, String last_name) {
this.first_name = first_name;
this.last_name = last_name;
}
public Users() {
}
}
The response I am getting is:
I want to do the validation at Service Layer but unable to do so. Validation works for my code implementation but the problem is that the validation message is shown with the package and class names which I do not want to show. I am trying to get only the validation error message if validation fail.
I tried adding #Valid annotation but unable to get the response I am looking for.
I am looking for a response below if validation fails.
"message": ["First name can not be empty","Last name can not be empty"]
Can anyone help me out with this problem. Thank you in advance.
Add a global exception handler to parse your error and return error message in the format you need.
#ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleConstraintViolation(
ConstraintViolationException ex, WebRequest request) {
List<String> errors = new ArrayList<String>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getRootBeanClass().getName() + " " +
violation.getPropertyPath() + ": " + violation.getMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
Source
You must to change your userMap object for your validated User class in your controller:
public ResponseEntity<Users> registerUser(#RequestBody #Valid Users){
Users createdUser = userService.registerUser(user.firstName, user.lastName);
return ResponseEntity.ok().body(createdUser).build();
}
This should returns the error messages for not valid fields only in a JSON structure.
A good practice is the use of DTOs instead Entities clases for Controllers requests and responses, you can read more about that here:
https://www.amitph.com/spring-entity-to-dto/
In this way you can choose those fields from your Entities that you want to show.

Failed to read HTTP message. Required request body is missing

I have faced with a problem that I can't get my object on server side.
I am getting this error:
128870 [http-apr-8080-exec-1] WARN org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.webserverconfig.user.entity.User com.webserverconfig.user.controller.UserController.login(com.webserverconfig.user.entity.User)
I am trying to send object on server side using GET request. I want to use this object just to verify fields on it. (I am doing simple user login method and i want to check userName and userPassword).
Here is my code on server side:
Request:
#RequestMapping(value = "/login", method = RequestMethod.GET)
#ResponseStatus(value = HttpStatus.ACCEPTED)
public User login(#RequestBody User user) {
userValidator.validateUserLogin(user);
securityService.autoLogin(user.getUserName(), user.getUserPassword());
return user;
}
Entity user:
#Entity
#Table(name = "Users")
public class User {
public User() {
}
#Id
#GeneratedValue(generator = "increment")
#GenericGenerator(name = "increment", strategy = "increment")
#Column(name = "Id", unique = true, nullable = false)
private long id;
#Column(name = "userName", nullable = false)
private String userName;
#Column(name = "userPassword", nullable = false)
private String userPassword;
#Transient
private String confirmPassword;
public long getId() {
return id;
}
public String getUserName() {
return userName;
}
public String getUserPassword() {
return userPassword;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setId(long id) {
this.id = id;
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
}
Client side code:
private class LoginUserTask extends AsyncTask<Void, Void, User> {
#Override
protected User doInBackground(Void... voids) {
restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
User user = new User(userName, userPassword);
return restTemplate.getForObject(URL.getUserLogin(), User.class, user);
}
#Override
protected void onPostExecute(User user) {
responseEntity = restTemplate.getForEntity(URL.getUserLogin(), String.class);
HttpStatus responseStatus = responseEntity.getStatusCode();
if(responseStatus.equals(HttpStatus.ACCEPTED)){
view.makeToast("User login completed " + user.getUserName());
} else {
view.makeToast("User login or password is not correct");
}
}
}
Am I missing something? Can anybody help with this please ?
You have set a #RequestBody annotation with a User object as an input parameter.
In this case, you have to use POST method along with a User object in the body of the request.

Categories