Spring security - Limiting access to my update profile page - java

I am using spring security in my application and ran into a problem. My application has an Update Profile page. I have added preAuthorized() with request mapping as
#PreAuthorize("isAuthenticated()")
#RequestMapping (value="/user/{uid}/profile/update", method = GET)
public String updateProfileView(#ModelAttribute("form") UserProfileForm form, #PathVariable ("uid") Integer userId, Model model){
It works fine, and unauthenticated user can not access this page.
But the issue is that every Authenticated User can access this page.
For example : User A logged in into application, he/she will be able to update every one's profile.
My CustomUserDetailService class is
#Service
#Transactional
public class CustomUserDetailsService implements UserDetailsService {
#Resource
UserService userService;
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
com.analyst.future.domain.User user = userService.getUser(email);
SimpleGrantedAuthority auth = new SimpleGrantedAuthority("ROLE_USER");
Collection<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();
authorities.add(auth);
User userDeatails = new User(user.getEmail(), user.getPassword(), authorities);
return userDeatails;
}
}
I don't think i can restrict it with roles as every authenticated user will have same roles.
Is there any way i can restrict Authenticated user to access only self update profile page.

I am no Spring Security expert, but try reading up on using Expression-Based Access - Link here
There is one tiny little line that matches what you want -
For example, if you wanted a particular method to only allow access to a user whose username matched that of the contact, you could write
#PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
I think in your case it would be something like
#PreAuthorize("email == authentication.email")
This is method level though, so maybe not what you are looking for? Good news is that there is a way to use the logged in user and match it against the request user. :)

Since all previous answers talk about matching username (which is included in the principal object) but you need to match the userID, this needs a little more work. We first need to return a custom User object that extends UserDetails. Here is how you can do that:
Step 1: Make your 'User' model implement UserDetails
#Entity
public class UserAccount implements UserDetails {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonProperty("id")
private int userId;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
private String email;
private String password;
// other fields and methods
}
You will also have to override some methods from UserDetails, which is easy to figure out.
Step 2: Make you User Service implement UserDetailsService
#Component
public class UserAccountService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) {
// todo
}
}
Step 3: Return YOUR user model from the loadUserByUsername method
#Override
public UserDetails loadUserByUsername(String username) {
Optional<UserAccount> userAccount = userAccountDao.findByEmail(username);
if (!userAccount.isPresent()) {
throw new MyException("User account not found for the given Username.", HttpStatus.NOT_FOUND);
}
return userAccount.get();
}
Step 4: Add the #PreAuthorize expression to your resource
#PutMapping(value = "/{userId}")
#PreAuthorize("#userId == authentication.principal.userId or hasAuthority('ADMIN')")
public UserAccount updateUserAccount(#PathVariable("userId") int userId,
#RequestBody UserAccount userAccount) {
return userAccountService.updateUserAccount(userId, userAccount);
}
Important things to notice above are:
We can check if the userId is equal above, only because our custom UserAccount model has a userId. If we had returned a raw UserDetail (like org.springframework.security.core.userdetails.User) object instead, it will not have any 'id' property to match with, so above will fail.
Above #PreAuthorize annotation checks for 2 conditions. It allows the request if the user is the owner of the Account or if the user has ADMIN authority.

Related

Redis caches the principal object issue

While using the redis, I'm not able to change the user's property since Principal object is cached.
I have a service which is for changing the user's property. To do that, I've created a CustomUserDetails class and implemented the UserDetails interface. CustomUserDetails class have 2 fields; propertyid, currentPropertyid(transient). See the implementation:
public class CustomUserDetails implements UserDetails {
private Long propertyid;
private Long currentPropertyid;
public Long getPropertyid() {
if(getCurrentPropertyid() != null){
return getCurrentPropertyid();
}
return propertyid;
}
public void setPropertyid(Long propertyid) {
this.propertyid = propertyid;
}
#Transient
public Long getCurrentPropertyid() {
return currentPropertyid;
}
public void setCurrentPropertyid(Long propertyid) {
this.currentPropertyid = propertyid;
}
}
Service implementation:
#PutMapping(value="change/property")
public void changeProperty(Long propertyid) {
CustomUserDetails user = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal())
user.setCurrentPropertyid(propertyid);
}
So, basically service's aim is to change propertyid to see the other property's data.
This is working fine, but when I enable the redis, it is not working. Some how redis caches the Principal object. I didn't add here my redis implementation, because, in my redis implementation, I don't have any implementation that caches the UserDetails object. In any case, I've comment out the RedisTemplates that I implemented to project, but redis is not disabled, again same problem.
Any idea to handle this problem?
I figure out that redis caches principal object by OAuth2AccessToken. So that, I needed to override the OAuth2AccessToken. Here is my updated service method;
#Autowired
#Qualifier("customRedisTokenStore")
CustomRedisTokenStore mCustomRedisTokenStore;
#PutMapping(value="change/property")
public void changeProperty(Long propertyid) {
CustomUserDetails user = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal())
user.setCurrentPropertyid(propertyid);
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();
OAuth2AccessToken accessToken = mCustomRedisTokenStore.getAccessToken(oAuth2Authentication);
mCustomRedisTokenStore.storeAccessToken(accessToken, oAuth2Authentication);
}

Custom authorities with LDAP authentication

I’ve found few Spring XML Configuration examples for logging in with LDAP and configuring the authorities of the logged in user with the help of a custom method and not through LDAP.
Unfortunately, I could not find any Spring Boot example with annotations.
In our case, there is a central LDAP repository in which the usernames and passwords of the users are stored, but the groups of the users are not stored there.
I appreciate any example or reference.
Thank you in advance.
Below you can find our final implementation at the project.
Basic flow is:
a)Check the user id and roles during the authentication. If user is not defined or does not have the roles for the application, do not authenticate the user.
b)if user pass the database check, continue with ldap authentication.
c)Merge the roles coming from database with ldap to be used during the application.
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter implements InitializingBean {
...
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(this.ldapAndDatabaseAuthenticationProvider());
}
#Bean(name="ldapAuthenticationProvider")
public AuthenticationProvider ldapAndDatabaseAuthenticationProvider(){
LdapUserDetailsMapper userDetailsMapper = new LdapUserDetailsMapper();
userDetailsMapper.setRoleAttributes(new String[]{"groupMembership"});
LdapAndDatabaseAuthenticationProvider provider =
new LdapAndDatabaseAuthenticationProvider(this.ldapAuthenticator(), this.ldapAuthoritiesPopulator());
provider.setUserDetailsContextMapper(userDetailsMapper);
return provider;
}
#Bean( name = "ldapAuthoritiesPopulator" )
public LdapAndDatabaseAuthoritiesPopulator ldapAuthoritiesPopulator(){
return new LdapAndDatabaseAuthoritiesPopulator(this.contextSource(), "");
}
#Bean( name = "ldapAuthenticator" )
public LdapAuthenticator ldapAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator( this.contextSource() );
authenticator.setUserDnPatterns(new String[]{"cn={0},ou=prod,o=COMP"});
return authenticator;
}
#Bean( name = "contextSource" )
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource contextSource =
new DefaultSpringSecurityContextSource( ldapUrl );
return contextSource;
}
Here is how additional roles populator (LdapAndDatabaseAuthoritiesPopulator ) implemented.
public class LdapAndDatabaseAuthoritiesPopulator extends DefaultLdapAuthoritiesPopulator{
public LdapAndDatabaseAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) {
super(contextSource, groupSearchBase);
}
protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user,
String username) {
Set<GrantedAuthority> mappedAuthorities = new HashSet<GrantedAuthority>();
/* Add additional roles from other sources for this user*/
/* below add is just an example of how to add a role */
mappedAuthorities.add(
new GrantedAuthority() {
private static final long serialVersionUID = 3618700057662135367L;
#Override
public String getAuthority() {
return "ROLE_MYAPP_USER"; //this is just a temporary role we are adding as example. get the roles from database.
}
#Override
public String toString(){
return this.getAuthority();
}
});
for (GrantedAuthority granted : mappedAuthorities) {
log.debug("Authority : {}", granted.getAuthority().toString());
}
return mappedAuthorities;
}
}
Below is how the Custom Ldap authentication provider (LdapAndDatabaseAuthenticationProvider) implemented to check if user has required roles defined in the database to access the application. If user is not in the database or roles are missing, authentication will throw account DisabledException.
franDays also suggested to use "Custom Authentication Provider". I would like to give him a credit.
public class LdapAndDatabaseAuthenticationProvider extends LdapAuthenticationProvider{
public LdapAndDatabaseAuthenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authoritiesPopulator) {
super(authenticator, authoritiesPopulator);
}
#Override
protected DirContextOperations doAuthentication(
UsernamePasswordAuthenticationToken authentication) {
log.debug("Checking if user <{}> is defined at database to use this application.", authentication.getName());
// Here is the part we need to check in the database if user has required role to log into the application.
// After check if user has the role, do nothing, otherwise throw exception like below example.
boolean canUserAuthenticate = isActiveUserExist(authentication.getName());
log.debug("canUserAuthenticate: {}", canUserAuthenticate);
if (!canUserAuthenticate)
throw new DisabledException("User does not have access to Application!");
return super.doAuthentication(authentication);
}
private boolean isActiveUserExist(String userId) {
// Do your logic here are return boolean value...
}
You can implement your own AuthenticationProvider. The authenticate method would query an LdapTemplate and upon successful attempt then query the groups from wherever they are stored. It could look like below:
public class CustomAuthenticationProvider implements AuthenticationProvider {
private LdapTemplate ldapTemplate;
private UserRepository userRepository;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
boolean success = ldapTemplate.authenticate(...);
if (!success) {
throw new BadCredentialsException("Wrong username or password");
}
User user = userRepository.findByUsername(username);
if (user == null) {
throw new BadCredentialsException("Username not known by the application");
}
return new CustomAuthentication(username, user.getRoles());
}
}
I omitted the initialization of the LdapTemplate because it depends on the specifics of your case. Same thing for the Authentication object you return for which you would need to implement a class and allow a way to build an instance by passing the username and password.
If you need guidance on how to register your auth provider using java config, this post might help: Custom Authentication provider with Spring Security and Java Config

Spring Security Acquire Roles from a Web Service

Looking to use Spring MVC + Spring Security with annotations. Spring Security has great features for obtaining role information from an XML file or a database. However, our role information is accessible from a custom-built SOAP web service. Any ideas how I can get the role information from the Web Service?
Ideally, I'd like to override hasRoles(), etc and modify it so it would then call the SOAP WS and return back the user's roles. How would I do that?
Or any other ideas?
If you need custom attributes while using spring-security you have to implement your own UserDetailsService interface which contains the following method:
public UserDetails loadUserByUsername(final String email)
So after retrieving user from datasource add calls to your roles web-service... Something like that:
public class UserDetailsExtendedService implements UserDetailsService {
#Autowired
private UsersDAO usersDao;
private UserDetails prepare(com.data.User user) {
boolean enabled = user.getState().equals(UserState.Active);
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (UserRole r: /*CALL TO WEB-SERVICE*/) {
authorities.add(new GrantedAuthorityImpl(r.getName()));
}
return new UserDetails(user.getId(), user.getEmail(), user.getPassword(), user.getNickname(), user.getPosition(), user.getAvatar(), user.getCommunicationLanguage().getCode(),
user.getNotificationChannel(), user.getPartnerId(), enabled, enabled, enabled, enabled, authorities);
}
#Override
#Transactional(readOnly = true)
public UserDetails loadUserByUsername(final String email)
throws UsernameNotFoundException, DataAccessException, HibernateException {
com.data.User user = usersDao.getByEmail(email);
if (user == null)
throw new UsernameNotFoundException(email);
return prepare(user);
}
}

How to get password from Spring Security?

I'm using a Apache CXF with WebClient and I want to send my credentials to the server
WebClient client = WebClient.create("http://localhost:8084", "username", "password", null)
The problem is that I can't get the password value.
The following method is not working
SecurityContextHolder.getContext().getAuthentication().getCredentials()
Lets say that you been login and you need to get company name that you been retrieve from database. First you will need to object that implement UserDetails that will save in UserDetailsService and add any variable that you need (company, etc).
public class CustomUserDetails implements UserDetails{
private String password;
private String username;
private String companyName;
}
than at typecast with your custom Userdetails implementation.
CustomUserDetails customDetails (CustomUserDetails)SecurityContextHolder.getContext().getAuthentication().getCredentials();
getCredentials will return Object of what you saved to the credential. Whatever it type you can always return it to the class you been save.

Access User Data in Spring, Hibernate

I did this tutorial: http://www.mkyong.com/spring-security/spring-security-hibernate-annotation-example/
And it works perfectly. But I don't understand something. I add a new column to my database. It is named FullName. I completed the User class with the setter and getter method. But for example, how can I access this data in the admin page? How can I print out the actual session user's name. Like: Hi , your full name is .
Edit: So how can I acces the logged user's other details, like fullname, address, sex and other stuff like that, not just the username and password?
Edit2: Thanks, it is more clear now. My only question. He converts his own User class to the org.springframework.security.core.userdetails.User. And this class hasn't got a contructor with fullname. So Do I need to create an own UserDetails class override the orginial. Or what have I need to do?
Edit3: Sorry, but i don't understand this. Which service method? Can you show this for me with this example. Thank you very much.
You have add the code in the controller class MainController which is given in the example mentioned in the mkyong site.
#RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security + Hibernate Example");
model.addObject("message", "This page is for ROLE_ADMIN only!");
model.setViewName("admin");
String fullName = ...; // Add the required logic to get the full name.
model.addObject("FullName", fullName); // Then add the fullName to your model object
return model;
}
Then finally edit the admin.jsp file to display the Full Name.
Update:
The required code is also available in mkyong link. Here is the snippet taken from the same page:
UserDaoImpl.java class has all the code that is needed for your functionality.
List<User> users = new ArrayList<User>();
users = sessionFactory.getCurrentSession()
.createQuery("from User where username=?")
.setParameter(0, username)
.list();
if (users.size() > 0) {
return users.get(0);
} else {
return null;
}
When you get the User object from hibernate it loads all the properties of user, as you have added new field - FullName then this field gets loaded in User object by hibernate if you have proper mapping in hbm files.
This logic is called from the service class MyUserDetailsService using method loadUserByUsername. So take help of this method from service class to get the User details by providing the username input parameter.
Code from service class:
#Transactional(readOnly=true)
#Override
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
com.mkyong.users.model.User user = userDao.findByUserName(username);
List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole());
return buildUserForAuthentication(user, authorities);
}
// Converts com.mkyong.users.model.User user to
// org.springframework.security.core.userdetails.User
private User buildUserForAuthentication(com.mkyong.users.model.User user, List<GrantedAuthority> authorities) {
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, authorities);
}
As you can see here from above code, a new User object is created at line:
new User(user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, authorities);
jut update this line to include the Full name and also add the required constructor in User class.

Categories

Resources