Spring Security UserDetails and username - spring

In my current implementation I have a User entity that implements org.springframework.security.core.userdetails.UserDetails interface.
#Entity
#Table(name = "users")
public class User extends BaseEntity implements UserDetails {
private static final long serialVersionUID = 8884184875433252086L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String username;
private String password;
....
During the OAuth2 authorization I manually create a new User object, populate its fields and store in my database.
According to the UserDetails contract - UserDetails.getUsername() method can't return null but I have no values retrieved from Social Networks that can be used as username.
What value in this case should be returned in the User.getUsername() method ?
Is it okay to return something like this ?
#Override
public String getUsername() {
return String.valueOf(id);
}

If you need to save this entity before you have a valid value for the name then I think that's a problem with the design. Having said that User.getUsername() is mainly used for display purposes, so I doubt it matters what the actual value is, as long as it can be matched to something in an authentication.

Related

Spring boot UserDetailsService Multi-User with extra fields

I have a spring boot project that has 3 types of users (Admin, Expert, Customer) and the application is for Experts that register on site for giving services like fixing computers to Customers that are asking help in site.
I have an inheritance of different kind of User types as following.
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "USER_TYPE", discriminatorType = DiscriminatorType.INTEGER)
public abstract class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private Set<String> roles = new HashSet<>();
// getter & setter...
}
#Entity
#DiscriminatorValue("1")
public class Admin extends User {
}
#Entity
#DiscriminatorValue("2")
public class Expert extends User {
private Byte[] expertPhoto;
private String password;
// some other fields & getter & setter...
}
#Entity
#DiscriminatorValue("3")
public class Customer extends User {
private Long credit;
private Set<CustomerOrder> orders = new HashSet<>();
// some other fields & getter & setter...
}
I want to use spring boot security and implement UserDetailsService, my problem is that how to design when I have different User types (Expert, Customer, etc.)?
I want users to be able to have different roles (admin, expert, customer) with one username.
How should I design my system to solve these requirements?
Your role modal seems a bit off. It is better to have a single type of User and fill it with list of a new Role entity. The new User entity will look like the following:
#Table(name = "user")
#Entity
public class User {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "username", unique = true, nullable = false)
private String username;
#Column(name = "password", nullable = false)
private String password;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(
name = "user_role",
joinColumns = {#JoinColumn(name = "user_id")},
inverseJoinColumns = {#JoinColumn(name = "role_id")},
)
private Set<Role> roles;
// getters and setters & other fields user can have
}
And the Role entity will look like this:
#Entity
#Table(name = "role")
public class Role {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "role_name", unique = true, nullable = false)
private String roleName;
#ManyToMany(mappedBy = "roles")
private Set<User> users;
}
Then, you need to implement org.springframework.security.core.userdetails.User interface to use as a concrete implementation of spring security class Useron your UserDetailsService. Notice that this class is also called User and is different than the User class on your system.
public class MyUserDetail extends User {
private String otherFieldsLikePhoto; // you can add different fields like this to keep extra information
public MyUserDetail(String username, String password, Collection<? extends GrantedAuthority> authorities, String otherFieldsLikePhoto) {
super(username, password, authorities);
this.otherFieldsLikePhoto = otherFieldsLikePhoto;
}
}
Then, you can create your UserDetailsService by implementing org.springframework.security.core.userdetails.UserDetailsService of spring security.
What you will achieve UserDetailsService is to load the user in the MyUserDetail format we just created. It will be something like this:
public class MyUserDetailsService implements UserDetailsService {
private final UserReadService userReadService; // put your service to get user from db
public MyUserDetailsService(UserReadService UserReadService) {
this.userReadService = UserReadService;
}
#Override
public UserDetails loadUserByUsername(String username) {
User user = userReadService.getByUsername(username); // get user from db
String otherFieldsLikePhoto = getUserPhotoOrAnythingElse(user); // get your extra fields however you want
return new MyUserDetail(
user.getUsername(),
user.getPassword(),
getAuthoritySetOfUser(user), // notice how we embed roles to UserDetail
otherFieldsLikePhoto
);
}
// this function is not necessary but useful to calculate authority set calculation on helper
private Set<SimpleGrantedAuthority> getAuthoritySetOfUser(User user) {
Set<Role> userRoles = user.getRoles(); // get roles of user like ADMIN, EXPERT etc.
Set<SimpleGrantedAuthority> authorities = roles.stream()
.map(rolex -> new SimpleGrantedAuthority(rolex.getRoleName()))
.collect(Collectors.toSet());
return authorities;
}
}

Child table is not mapping in OneToMany relationship in JPA

I am trying to establish One to many relationship between User and Role.
One user can have many role.
Here is the code for User class
#Entity
public class User {
#Id
private int id;
private String name;
private String password;
private String email;
private String phoneNo;
#OneToMany(
targetEntity = Role.class,
mappedBy = "user",
cascade = CascadeType.ALL,
fetch = FetchType.LAZY
)
private Set<Role> roles;
// Getters, setters and Constructor
Code for the Role class
#Entity
public class Role {
#Id
#GeneratedValue
private int roleId;
private String role;
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
// Getters, setters and Constructor
POST request on Postman is
{
"id":101,
"name": "rahul",
"password": "456",
"email": "rahul#gmail.com",
"phoneNo": "1234561234",
"role": [{
"role":"USER"
}]
}
Code on Configuration part
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http)throws Exception
{
http.csrf().disable();
}
#Bean
public BCryptPasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
}
Code On Controller part
#RestController
public class AdminController {
#Autowired
UserRepository userRepo;
#Autowired
BCryptPasswordEncoder encryptPassword;
#PostMapping("/admin/add")
public String addUserByAdmin(#RequestBody User user)
{
String pass = user.getPassword();
String encrypt = encryptPassword.encode(pass);
user.setPassword(encrypt);
userRepo.save(user);
return "User added Successfully";
}
}
Role table connection to database through Jpa
public interface RoleRepository extends JpaRepository<Role, Integer> {
}
User table connection to database through Jpa
public interface UserRepository extends JpaRepository<User,Integer>{
}
Here problem is User table is mapped properly but Role table is not getting mapped.
roleId role user_id
NULL NULL NULL
Where I am wrong ? Could anyone help me ?
On your controller method below, have you tried debugging the incoming request User object?
Anyways, I have below points here:
First, looking into your request body, the field for your roles is named role while your User object has a field roles thus, I'm pretty sure it is null during your processing since it will not be deserialized there due to mismatch field names. Try changing your request body to something like this:
{
"id":101,
"name": "rahul",
"password": "456",
"email": "rahul#gmail.com",
"phoneNo": "1234561234",
"roles": [{
"role":"USER"
}]
}
Second, if you check your database, the roles will be persisted however your foreign key user_id is null. This is expected. The cascade you did on the User object will only means that (since you use CascadeType.ALL) once you save the User object the save operation will also be cascaded to the Role object however, JPA still needs to know the relationship thus you have to set the user for each role object. Hence, you can update your controller method to something below:
#PostMapping("/admin/add")
public String addUserByAdmin(#RequestBody User user)
{
String pass = user.getPassword();
String encrypt = encryptPassword.encode(pass);
user.setPassword(encrypt);
// JPA needs to know this relationship...
user.getRoles().forEach(role -> role.setUser(user));
userRepo.save(user);
return "User added Successfully";
}
Now you can try and see that your expected behavior should now be happening.
Additional recommendations:
Why are we passing ID field on the user request? You can just remove that from your request body and use below to auto-generate your IDs to avoid Unique index or primary key violation exceptions on all of your entities:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
You can also remove the targetEntity = Role.class on the mapping as it is only used for generics and for your case clearly you are not using generics for Set. Update your User object for roles mapping:
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Role> roles;
Lastly, it is better if you can wrap your incoming payload to a DTO since you would not want to expose your entity/model to your API but I am thinking this is just for your test environment.
You need to flush the changes to the database when using save(), try this instead:
userRepo.saveAndFlush(user);

ClassCastException - Spring Security With User

I'm currently making an E-Commerce application and I'm trying to add "Add To Cart" functionality. I haven't yet worked with httprequests, so everyone has to login before accessing the website.
When a user logs in, goes onto the home page and tries to add a product to cart, it will send a post request to add that product.
#PostMapping("/cart/{id}")
public String addProduct(#PathVariable Long id, #ModelAttribute("cartItem") CartItems cartItem, Model model) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = ((User) principal);
Optional<Product> product = productService.findById(id);
cartItem.setProduct(product.get());
cartItem.setPrice(cartItem.getQuantity() * cartItem.getProduct().getPrice());
user.getCart().getCartItems().add(cartItem);
return "redirect:/cart";
}
User Class:
#Entity
#EntityListeners(AuditingEntityListener.class)
#Table(name = "users")
public class User implements Serializable, UserDetails
{
private static final long serialVersionUID = -8903772563656397127L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Long id;
#Column(name = "username")
private String username;
#Column(name = "email")
private String email;
#Column(name = "password")
private String password;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
#OneToOne(mappedBy = "user")
private Cart cart;
#Column(name = "address")
private String address;
USER DETAILS:
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority("UNDEFINED"));
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
The problem is casting the User class to Object principal from user details. I am getting the following error:
java.lang.ClassCastException: class org.springframework.security.core.userdetails.User cannot be cast to class com.nickjojo.ecomapp.entity.User (org.springframework.security.core.userdetails.User and com.nickjojo.ecomapp.entity.User are in unnamed module of loader 'app')
There's a couple of things I would improve here:
Your User class already implements UserDetails, so no need to convert it to a org.springframework.User in your UserDetailsService.loadUserByUsername. Simply return it.
Instead of going through the SecurityContext, you can then simply go with the #AuthenticationPrincipal annotation like so (note, it will be null if the user is not logged in):
#PostMapping("/cart/{id}")
public String addProduct(... #AuthenticationPrincipal user) {
// ...
}
I found the answer. I was casting User to the principle object, but instead I casted UserDetails to the object (they can be casted together), then got the object's username as specified as one of the fields when returning a UserDetails object in the UserDetailsService class. I then used that username to find the user through my service.
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = ((UserDetails) principal).getUsername();
User user = userService.findByUsername(username);

why I can't use string as id

I am trying to create a user model with a CrudRepository:
#Entity
public class User {
#Id
#GeneratedValue
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
public interface UserRepository extends CrudRepository<User, String> {
}
However I got an 500 error every time I call findOne():
#Controller
public class UserController {
#Autowired
private UserRepository users;
#Override
#RequestMapping(value="/register", method=RequestMethod.POST)
public #ResponseBody User register(#RequestBody User userToRegister) {
String username = userToRegister.getUsername();
User user = users.findOne(id);
if (user != null) {
return null;
}
User registeredUser = users.save(userToRegister);
return registeredUser;
}
}
However if I just switch to an long type id instead of username itself then everything works. I think it's common to use string as id. So how to make this work?
I use the embedded hsql database. I didn't wrote any sql code.
The problem is that String username; is annotated with both #Id and #GeneratedValue. #Id means that is should be a primary key, fine it can be a String. But #GeneratedValue means that you want the system to automatically generate a new key when you create a new record. That's easy when the primary key is integer, all databases have a notion of sequence (even if the syntax is not always the same). But if you want String automatically generated keys, you will have do define your own custom generator.
Or if you have no reason for the #GeneratedValue annotation, simply remove it as suggested by Bohuslav Burghardt
Use column annotation like below by putting nullable False.
#Id
#GeneratedValue
#Column(name = "username", nullable = false)
private String username;

How to add properties to the relationship in Spring data neo4j when we use createRelationshipBetween

For example I want to make relationship between User A and User B and they have RelationshipEntity named MakeFriend, I am used code below, but I am also want to set in relation entity some property values like role = 10.........
userRepository.createRelationshipBetween(startUser, endUser, MakeFriend.class, RelTypes.FRIEND.name());
#RelationshipEntity
public class MakeFriend {
#GraphId
private Long id;
private String role;
#StartNode
private UserEntity startUser;
#EndNode
private UserEntity endUser
#NodeEntity
public class UserEntity implements Serializable {
private static final long serialVersionUID = 1L;
public static final String FRIEND = "FRIEND";
public static final String JOYNED = "JOYNED";
#GraphId
private Long id;
#Indexed(unique = true)
private Long userId;
private String email;
You could could add the following to your UserEntity class:
#RelatedToVia(type = RelTypes.FRIEND, direction = Direction.BOTH)
private MakeFriend friend;
friend.setRole("yourRole");
Another way to do it, when you're using the advanced mapping mode, is using one of the NodeBacked.relateTo() methods. Then add the property to the returned Relationship.
And a third way, it to use the Neo4jTemplate.createRelationshipBetween() method and provide your properties (e.g. role) as the final argument.

Resources