So lets assume I have 2 model classes - User and Presentation, and they look something like this:
#Entity
public class Presentation {
#Id
private Long id;
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
}
#Entity
public class User implements UserDetails {
#Id
private Long id;
private String username;
private String name;
private String surname;
private String password;
#Enumerated(EnumType.STRING)
private Role role;
}
As you can see I have a unidirectional mapping for user in Presentation class. My endpoint looks something like this:
#RestController
public class PresentationController {
#GetMapping("/{presentationId}")
public PresentationDTO getPresentation(#PathVariable Long presentationId) {
return presentationService.getPresentationById(presentationId);
}
#GetMapping
public List<PresentationDTO> getAllPresentations() {
return presentationService.getAllPresentations();
}
}
Now for my question - how do I change getAllPresentations() to return the presentations that the users with role "user" own, and return all presentations for users with role "admin"? I know I can create a separate endpoint with a different mapping (like /admin/presentations) and add #PreAuthorize(hasRole("admin")), but here is the tricky part.
For the getAllPresentations() endpoint which everyone who is authenticated is supposed to fetch his own presentations, how do I know for which user I have to return his presentations? Maybe I can get the username as a parameter but that might be dangerous cause he can submit any username he wants and get the presentations for that user. I don't know too much about Spring Security and I don't even know the right question to ask google to get an answer so I'm stuck...
Any help will be appreciated, thanks!
You don't have to pass username to your controller method. The currently authenticated user is available through a number of different mechanisms in Spring.The simplest way to retrieve the currently authenticated principal is via a static call to the SecurityContextHolder like this :
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
So you can refactor you method getAllPresentations() in service class to accept username as argument, and then you can load user by passed username and return presentations for that user.
One way to do what you want is to use #PostFilter annotation to filter List<Presentation> that the authenticated user owns or if the authenticated user has a role ADMIN like this:
#GetMapping
#PostFilter("filterObject.user.username == authentication.principal.username or hasRole('ADMIN')")
public List<PresentationDTO> getAllPresentations() {
return presentationService.getAllPresentations();
}
I am using below code to fetch user information from MySQL database using username, but it always returns empty list.
public interface UserRepository extends JpaRepository<User, Integer> {
List<User> findByUserNameIgnoreCase(String userName);
}
The below method is in service class
public String login(LoginDTO loginDTO) {
logger.info("Login request for customer {} with password {}",
loginDTO.getUserName(), loginDTO.getPassword());
List<User> user =
userRepo.findByUserNameIgnoreCase(loginDTO.getUserName());
if(user.isEmpty())
return "Invalid";
else
return "Successful";
}
Kindly help.
Trying findByUsernameIgnoreCase because Spring might treat it as User and Name instead of Username.
So, by using the line below we can extract the username and password:
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
But I would like to get the userId of the logged in user, a property of my custom User class. so I decided to do this:
CustomUserDetails details = new CustomUserDetails();
...
details.setUser(current);
UserDetails myUserDetails = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
BeanUtils.copyProperties(details, myUserDetails);
CustomUserDetails myDetails = details.downCast(myUserDetails);
Integer userId = myDetails.getUser().getId(); //Fetch the custom property in User class
user.setId(userId);
However, I am just getting the user through HQL and putting it inside a temporary User class, which is pointless.
So, what's the best way to get the property userId? It seems I am better off just using a HQL query (get the User through HQL with the username of the logged in user).
I would extend the Spring Security UserDetails class and add whatever appropriate attributes that are of interest your application needs. We typically do this specifically for immutable information which includes userId.
Then when you need the appropriate attribute:
// Get the principal from spring security
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
// verify principal if your custom user details type.
// if so, get the userid and do whatever.
if(principal != null && principal instanceof CustomUserDetails) {
CustomUserDetails userDetails = CustomUserDetails.class.cast(principal);
Long userId = userDetails.getUserId();
/* do whatever you want with user id */
}
Then during authentication, just create your own CustomUserDetails object when your implementation of UserDetailsService returns the UserDetails.
I have a custom Model of Autorization that i want to represent with spring security :
I have the concpt of roles and permissions :
#Entity
public class User
...............
#ManyToMany
#Column
private Set<Role> roles;
#ManyToMany
#Column
private Set<Permission> permissions;
}
In my custom UserdetailsService i have a clean way to load the roles but i on't find any way and any componenent in spring-security related to the permissions :
public class BELUserDetailService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User connectedUser = userRepositoy.findUserByUsername(username);
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (Role r :connectedUser.getRoles()) {
authorities.add(new SimpleGrantedAuthority(r.getRoleAWB().name()));
}
BELUserDetails belUserDetails = new BELUserDetails(connectedUser.getIdUser(), authorities);
.....
....
}
}
My roles are :
ADMIN
NORMAL USER
TRANSACTION USER
My Permissions are :
VALIDATE_TRANSATION
INIT_TRANSACTION
And the functional use case is if you want Validate a Transaction you have to have the ROLE TRANSACTION USER and the permission VALIDATE_TRANSACTION.
hasRole("ROLE_TRANSACTION_USER") and hasPermission("VALIDATE_TRANSACTION")
Another important case is that i want in the future use PermissionEvaluator to put some limits when trying to validate a transaction if the user has the Role "ROLE_TRANSACTION_USER" and the permission VALIDATE_TRANSACTION , he must also have a Amountlimit greater than the amount of the transaction and this functionality is very cool with PermissionEvaluator
That's why i need to implements both Role and permission Concepts
How i will add my permission to the standard flow of spring-security .
Thanks in advance .
By default you have only authorities in Spring Security. Just add all your roles and permissions into authorities collection. Then you can do:
hasRole("ROLE_TRANSACTION_USER") and hasRole("VALIDATE_TRANSACTION")
In most cases mixing the two is not a problem.
You have permissions in Spring Security ACL module, but you need ACL only if you want to have different security rules per domain object.
EDIT. I think the most easy way to do some additional security checks is to use SpEL. Example:
#PreAuthorize("hasRole('ROLE_TRANSACTION_USER')
and hasRole('VALIDATE_TRANSACTION')
and #amountValidatorServiceBean.isAmountValidForCurrentUser(#amount)")
public void doTransaction(Integer amount, ...)
I'm using Spring Security in my Spring MVC app.
JdbcUserDetailsManager is initialized with the following query for authentication:
select username, password, enabled from user where username = ?
And authorities are being loaded here:
select u.username, a.authority from user u join authority a on u.userId = a.userId where username = ?
I would like to make it so that users can login with both username and email. Is there a way to modify these two queries to achieve that ? Or is there an even better solution ?
Unfortunatelly there is no easy way doing this just by changing the queries. The problem is that spring security expects that the users-by-username-query and authorities-by-username-query have a single parameter (username) so if your query contain two parameters like
username = ? or email = ?
the query will fail.
What you can do, is to implement your own UserDetailsService that will perform the query (or queries) to search user by username or email and then use this implementation as authentication-provider in your spring security configuration like
<authentication-manager>
<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager>
<beans:bean id="myUserDetailsService" class="xxx.yyy.UserDetailsServiceImpl">
</beans:bean>
I had the same problem, and after trying with a lot of different queries, with procedures... I found that this works:
public void configAuthentication(AuthenticationManagerBuilder auth)
throws Exception {
// Codificación del hash
PasswordEncoder pe = new BCryptPasswordEncoder();
String userByMailQuery = "SELECT mail, password, enabled FROM user_ WHERE mail = ?;";
String userByUsernameQuery = "SELECT mail, password, enabled FROM user_ WHERE username=?";
String roleByMailQuery = "SELECT mail, authority FROM role WHERE mail =?;";
auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(pe)
.usersByUsernameQuery(userByMailQuery)
.authoritiesByUsernameQuery(roleByMailQuery);
auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(pe)
.usersByUsernameQuery(userByUsernameQuery)
.authoritiesByUsernameQuery(roleByMailQuery);
}
Its just repeat the configuration with the two queries.
If I understood this correctly, then the problem is that you want to lookup username entered by the user in two different DB columns.
Sure, you can do that by customizing UserDetailsService.
public class CustomJdbcDaoImpl extends JdbcDaoImpl {
#Override
protected List<GrantedAuthority> loadUserAuthorities(String username) {
return getJdbcTemplate().query(getAuthoritiesByUsernameQuery(), new String[] {username, username}, new RowMapper<GrantedAuthority>() {
public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
.......
}
});
}
#Override
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(getUsersByUsernameQuery(), new String[] {username, username}, new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
.......
}
});
}
Your bean configuration for this class will look something like this.
<beans:bean id="customUserDetailsService" class="com.xxx.CustomJdbcDaoImpl">
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="usersByUsernameQuery">
<beans:value> YOUR_QUERY_HERE</beans:value>
</beans:property>
<beans:property name="authoritiesByUsernameQuery">
<beans:value> YOUR_QUERY_HERE</beans:value>
</beans:property>
</beans:bean>
Your queries will look something similar to this
select username, password, enabled from user where (username = ? or email = ?)
select u.username, a.authority from user u join authority a on u.userId = a.userId where (username = ? or email = ?)
You can use your UserDetailesService.and config like the below code.
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private MyUserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
The point is that you don't need to return the user with the same username and you can get user-email and return user with the username. The code will be like the code below.
#Service
public class MyUserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
var user = /** here search user via jpa or jdbc by username or email **/;
if(user == null )
throw new UsernameNotFoundExeption();
else return new UserDetail(user); // You can implement your user from UserDerail interface or create one;
}
}
tip* UserDetail is an interface and you can create one or use Spring Default.
You can define custom queries in <jdbc-user-service> tag in users-by-username-query and authorities-by-username-query attributes respectively.
<jdbc-user-service data-source-ref="" users-by-username-query="" authorities-by-username-query=""/>
Update
You can create class which implements org.springframework.security.core.userdetails.UserDetailsService and configure your application to use it as an authentication source. Inside your custom UserDetails service you can execute queries that you need to obtain user from database.
You can config UserDetailesService class like this.
public class UserDetailsServiceImpl implements UserDetailsService{
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.getUserByEmailOrUserName(username); //for fetch user
if(user==null) {
throw new UsernameNotFoundException("User doesn't exists");
}
UserDetailsImpl customUserDetails = new UserDetailsImpl(user);
return customUserDetails;
}
}
Your queries will look something similar to this
select * from user where email = ? or username = ?
UserRepository class for fetch user data
#Repository
public interface UserRepository extends JpaRepository<User, Integer>{
#Query("from user where email = :u or username = :u")
public User getUserByEmailOrUserName(#Param("u") String username);
}
You can also add phone number while doing login.
Here is a workaround I discovered. Basically I'm concatenating username and email address with a delimiter character in between (for example 'jsmith~johnsmith#gmail.com'), and checking to see if the parameter matches the left side of the delimiter or if matches the right side of the delimiter:
select username, password, enabled
from users
where ? in (substring_index(concat(username, '~',email),'~', 1),
substring_index(concat(username, '~',email),'~', -1))
If you are concerned that the delimiter character (such as ~) might exist within the username or email, use a non-standard delimiter character instead (for example, X'9C')