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:
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
I have a spring rest web application where I am doing authentication with a single game token. I would like to add Google (or GitHub, Facebook, Telegram) account binding (oauth2) so that I can log in with oauth2, because it takes a long time to generate a token every time, how can this be implemented?
My code https://github.com/iNiceHero/VueAuthSpring
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true)
#EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsServiceImpl userDetailsService;
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/", "/api/auth/**", "/js/**", "/error**", "/login", "/api/user/**").permitAll()
.mvcMatchers("/api/test/**").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/login")
.and().csrf().disable();
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
http.sessionManagement()
.sessionFixation().migrateSession();
}
}
UserDetailsServiceImpl.java
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
UserRepository userRepository;
#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);
}
}
UserDetailsImpl.java
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
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(),
authorities);
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
#Override
public String getPassword() {
return null;
}
public Long getId() {
return id;
}
#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);
}
}
AuthController.java authenticateUser
if (!userRepository.existsByUsername(username)) {
User user;
if (userRepository.existsById(id)) {
user = userRepository.findById(id).orElseThrow();
user.setUsername(username);
} else {
user = new User(username, id);
}
Set<String> strRoles = loginRequest.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.setEnabled(true);
user.setRoles(roles);
userRepository.save(user);
}
User user = userRepository.findById(id).orElseThrow();
UserDetailsImpl userDetails1 = UserDetailsImpl.build(user);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails1, null, userDetails1.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> rolesUser = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
return ResponseEntity.ok(new UserResponse(
userDetails.getId(),
userDetails.getUsername(),
rolesUser));
Basically my json post is looks like this:
{username: "2kzhuanyonghao2", password: "qqq542417349", confirmPassword: "qqq542417349", firstname: "peiran", lastname: "liu", …}
confirmPassword: "qqq542417349"
email: "liupeiran9324#gmail.cokm"
firstname: "peiran"
lastname: "liu"
password: "qqq542417349"
phone: "234324234322"
roles: Array(2)
0: "Manager"
1: "Admin"
length: 2
__proto__: Array(0)
username: "2kzhuanyonghao2"
__proto__: Object
Well, as you can see the roles is a Array(2) which contain "Manager" and "Admin"
Now I wish my Java spring backend can get this roles into a Set of String, then I have a
signUpRequest class :
package com.crmbackend.payLoad.request;
import java.util.Set;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class SignupRequest {
#NotBlank
#Size(min = 1, max = 128)
private String username;
#NotBlank
#Size(min = 5, max = 128)
private String password;
#NotBlank
#Size(max = 128)
#Email
private String email;
#NotBlank
#Size(max = 45)
private String firstname;
#NotBlank
#Size(max = 45)
private String lastname;
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
#NotBlank
#Size(max = 64)
private String phone;
private Set<String> roles;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Set<String> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
}
and when I use this class, I'm doing this:
Set<String> roleSetString = signUpRequest.getRoles();
Every attribute is fine, except this roles attribute, I keep get Null value for this.
Here is my controller where call the signUpRequest:
#PostMapping("/signup")
public ResponseEntity<?> regietserUser(#Valid #RequestBody SignupRequest signUpRequest) {
if (userRepo.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest().body(new ReturnMessageResponse("Error: Username is already exist!"));
}
if (userRepo.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest().body(new ReturnMessageResponse("Error: Email is already used"));
}
User user = new User(signUpRequest.getUsername(), signUpRequest.getFirstname(), signUpRequest.getLastname(),
encoder.encode(signUpRequest.getPassword()), signUpRequest.getEmail(), signUpRequest.getPhone());
Set<String> roleSetString = signUpRequest.getRoles();
Set<Role> roles = new HashSet<>();
//if there are no roll specific
System.out.println(roleSetString);
if (roleSetString == null) {
Role userRole = roleRepo.findByName("User")
.orElseThrow(() -> new RuntimeException("Error: Role user is missing/not found"));
roles.add(userRole);
} else {
roleSetString.forEach(role -> {
switch (role) {
case "Admin":
Role adminRole = roleRepo.findByName("Admin")
.orElseThrow(() -> new RuntimeException("Error: Role Admin is not found."));
roles.add(adminRole);
break;
case "Manager":
Role managerRole = roleRepo.findByName("Manager")
.orElseThrow(() -> new RuntimeException("Error: Role Manager is not found."));
roles.add(managerRole);
break;
case "User":
Role userRole = roleRepo.findByName("User")
.orElseThrow(() -> new RuntimeException("Error: Role User is not found."));
roles.add(userRole);
break;
}
});
}
user.setRoles(roles);
userRepo.save(user);
return ResponseEntity.ok(new ReturnMessageResponse("User registered successfully!!"));
}
But as I pring out the roleSetString, it still return null,which means the getRoles() can not return me the role value, which I pass through json Array.
Any suggestions guys?
How/Where are you mapping each JSON field to class field? The problem is likely there.
Also there doesn't seem to be any annotations on your roles field in your class (unlike other fields), could this effect it?
I am using Spring boot-I have 3 classes User,Role and UserRole.I have pesisted both role object and user object but i get error that role object is not persisted.The mappings- between User and UserRole is OneToMany ,between Role and UserRole OneToMany.In the UserServiceImpl class i have persisted Role object roleRepository.save(userRole.getRole());
Error is-
Caused by: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.bookstore.domain.security.UserRole.role -> com.bookstore.domain.security.Role
#Entity
public class User implements UserDetails,Serializable {
private static final long serialVersionUID=157954L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "Id",nullable = false,updatable = false)
private Long id;
private String userName;
private String password;
private String firstName;
private String lastName;
private String email;
private String phone;
private boolean enabled;
#OneToMany(mappedBy = "user",cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JsonIgnore
private Set<UserRole> userRoles=new HashSet<UserRole>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Set<UserRole> getUserRoles() {
return userRoles;
}
public void setUserRoles(Set<UserRole> userRoles) {
this.userRoles = userRoles;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities=new HashSet<GrantedAuthority>();
userRoles.forEach(userRole->{
authorities.add(new Authority(userRole.getRole().getRoleName()));
});
return authorities;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return enabled;
}
#Override
public String getUsername() {
return userName;
}
}
#Entity
public class Role implements Serializable{
private static final long serialVersionUID=68678L;
#Id
private Long roleId;
private String roleName;
#OneToMany(mappedBy = "role",cascade = CascadeType.ALL,fetch = FetchType.LAZY)
#JsonIgnore
private Set<UserRole> userRoles=new HashSet<UserRole>();
public Role() {
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public Set<UserRole> getUserRoles() {
return userRoles;
}
public void setUserRoles(Set<UserRole> userRoles) {
this.userRoles = userRoles;
}
}
#Entity
public class UserRole implements Serializable {
private static final long serialVersionUID=456874L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long userRoleId;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "roleId")
private Role role;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "userId")
private User user;
public UserRole(User user,Role role) {
this.role = role;
this.user = user;
}
public UserRole() {
super();
}
public Long getUserRoleId() {
return userRoleId;
}
public void setUserRoleId(Long userRoleId) {
this.userRoleId = userRoleId;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
#Service
public class UserServiceImpl implements UserService {
private static final Logger LOG=LoggerFactory.getLogger(UserServiceImpl.class);
#Autowired
UserRepository userRepository;
#Autowired
RoleRepository roleRepository;
#Transactional
#Override
public User CreateUser(User user, Set<UserRole> userRoles) {
User localUser=userRepository.findByUserName(user.getUserName());
if(localUser!=null) {
LOG.warn("Username {} already exists",user.getUserName());
}
else {
for(UserRole userRole:userRoles) {
roleRepository.save(userRole.getRole());
LOG.error("inside for {}",userRole.getRole().getRoleName());
}
user.getUserRoles().addAll(userRoles);
localUser=userRepository.save(user);
}
return localUser;
}
}
#SpringBootApplication
public class BookStoreApplication implements CommandLineRunner {
#Autowired
UserService userService;
public static void main(String[] args) {
SpringApplication.run(BookStoreApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
User user1=new User();
user1.setUserName("test");
user1.setPassword(SecurityUtility.passwordEncoder().encode("test"));
user1.setEmail("test#test.com");
user1.setEnabled(true);
user1.setFirstName("testFirstName");
user1.setLastName("testLastName");
user1.setPhone("123456789");
Role role1=new Role();
role1.setRoleId((long)1);
role1.setRoleName("ROLE_USER");
UserRole userRole1=new
UserRole(user1,role1);
Set<UserRole> userRoles1=new HashSet<UserRole>();
userRoles1.add(userRole1);
userService.CreateUser(user1, userRoles1);
User user2=new User();
user2.setUserName("admin");
user2.setPassword(SecurityUtility.passwordEncoder().encode("admin"));
user2.setEmail("admin#admin.com");
user2.setEnabled(true);
user2.setFirstName("adminFirstName");
user2.setLastName("adminLastName");
user2.setPhone("223456789");
Role role2=new Role();
role2.setRoleId((long) 2);
role2.setRoleName("ROLE_ADMIN");
UserRole userRole2=new UserRole(user2,role2);
Set<UserRole> userRoles2=new HashSet<UserRole>();
userRoles2.add(userRole2);
userService.CreateUser(user2, userRoles2);
}
}
Couple of issues here.
The first (and the question) issue and the reason why you are getting a "Transient state error" is because you are trying to save an entity with entities attached to it that are NOT yet managed by hibernate.
Have a read of: Entity Lifecycle Model in Hibernate
Caused by: org.hibernate.TransientPropertyValueException:
object references an unsaved transient instance -
save the transient instance before flushing :
com.bookstore.domain.security.UserRole.role
->
com.bookstore.domain.security.Role
So you are somewhere trying to save a UserRole with a Role that is not yet managed.
When you call new on an entity, it is in a Transient state. Hibernate doesn't know how to handle it. It doesn't have a database ID and is not part of the context for hibernate to manage (make the relevent queries etc).
To make an entity managed you need to save it via the repo.
I.e. roleRepo.save(role)
You will then notice it then has an Id and is now managed by hibernate.
#Service
public class UserServiceImpl implements UserService {
#Transactional
#Override
public User CreateUser(User user, Set<UserRole> userRoles) {
User localUser = userRepository.findByUserName(user.getUserName());
if (localUser != null) {
LOG.warn("Username {} already exists", user.getUserName());
} else {
// For each transient UserRole passed in, save the Role.
// Role is now managed.
for (UserRole userRole : userRoles) {
roleRepository.save(userRole.getRole());
LOG.error("inside for {}", userRole.getRole().getRoleName());
}
user.getUserRoles().addAll(userRoles);
localUser = userRepository.save(user);
}
return localUser;
}
}
This service above doesn't maybe do what you expect.
You are getting the Roles and saving them.
You then don't replace the Role in UserRole with the managed one back from the repo.
Maybe this would work?
for(UserRole userRole:userRoles) {
//The role is now managed.
Role managedRole = roleRepository.save(userRole.getRole());
//Replace the transient role in the UserRole with a managed Role.
userRole.setRole(managedRole);
}
So when it goes on to save the User:
user.getUserRoles().addAll(userRoles);
localUser = userRepository.save(user);
The UserRoles (which are still transient) have a managed Role at least.
The Cascade.ALL should do what you expect (I am unsure mind!) and save the transient UserRoles because Cascade.ALL will save the children UserRoles.
https://www.baeldung.com/jpa-cascade-types
=============================
The second issue, not causing the problem in question, but you may want to go have a think about:
At the moment you have:
User 1 : M UserRole
UserRole M : 1 Role
1 User has many UserRoles.
1 Role has many UserRoles.
The modelling here just smells off.
Usually you'd have some Role entities/database entries that can be related to a user via ManyToMany relationship.
User signs up, is given the "USER" role in their Set<Role> userRoles rather than creating a new Role for each user with "USER" as a field.
So a user has a relationship to a role via a "join table" UserRole.
Spring can already create a join table for you. You do not really need the UserRole entity in your code as it stands as it just holds a FK_User and FK_Role.
Basically, you want:
User M:M Role
I.e. a user can have many roles.
Simply use the #ManyToMany annotation for a Many:Many relationship between user and roles.
To add a role you search the database for
Role findByRoleName
And add that managed entity to the user's roles and then persist the user.
ManyToMany Baeldung