How to access user details in Controller using Spring Security? - spring-boot

I need to access currently logged-in user details (id or email address) in Controller. This is how I am trying to do so right now, and this doesn't work.
ApplicationUser is an #Entity in database.
UserDetailsService:
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
private ApplicationUserRepository applicationUserRepository;
public UserDetailsServiceImpl(ApplicationUserRepository applicationUserRepository) {
this.applicationUserRepository = applicationUserRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ApplicationUser applicationUser = applicationUserRepository.findByUsername(username);
if (applicationUser == null) {
throw new UsernameNotFoundException(username);
}
return builtCustomUser(applicationUser);
}
private User builtCustomUser(ApplicationUser applicationUser) {
String username = applicationUser.getUsername();
String password = applicationUser.getPassword();
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
MyUser myUser = new MyUser(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, emptyList());
return myUser;
}
}
Custom User class:
public class MyUser extends User implements UserDetails {
public MyUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
That's how I am trying to access it in Controller:
MyUser mu = (MyUser) authentication.getPrincipal();
And this is error:
java.lang.ClassCastException: class java.lang.String cannot be cast to class MyUser

On this code, actual type of Authentication is UsernamePasswordAuthenticationToken, and return type of getPrincipal() is String, username.
You can set any other Authentication implementation instead of UsernamePasswordAuthenticationToken to SecurityContext, and principal type is free(so you can set MyUser), too.

Related

Spring Security 5.7 - How to return custom UserDetails

I've seen a lot of examples where a user creates a custom UserDetailsService in order to override the loadUserByUsername method and return a custom implementation of a UserDetails object.
This was done previously with sth like this
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
Now with the new version I'm confused on how to do this
I created a Bean and used the JdbcUserDetailsManager, I can configure my custom queries for users and authorities tables
#Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
String usersByUsernameQuery = "select username, password, enabled from tbl_users where username = ?";
String authsByUserQuery = "select username, authority from tbl_authorities where username = ?";
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
return userDetailsManager;
}
but how to return a custom UserDetails object with an extra field, e.g. an email with the new version?
OK after many tries what I did was to remove completely JdbcUserDetailsManager stuff from my custom SecurityConfig class and I created a custom UserDetailsService and custom UserDetails class and it worked.
So security config class had no code regarding the authentication of the users.
I was very confused because I thought that somehow I had to create a #Bean inside the config class, implement the authentication myself and in general that all this authentication code had to be done inside the config class, but it worked with this approach.
#Service
public class MyCustomUserDetailsService implements UserDetailsService {
#Autowired
UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User Not Found with username: " + username);
}
return MyUserDetails.build(user);
}
}
And the details class
public class MyUserDetails implements UserDetails {
private String username;
private String firstName;
private String lastName;
#JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public MyUserDetails(String username, String firstName, String lastName, String password,
Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.password = password;
this.authorities = authorities;
}
public static MyUserDetails build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getAuthority()))
.collect(Collectors.toList());
return new MyUserDetails(
user.getUsername(),
user.getFirstName(),
user.getLastName(),
user.getPassword(),
authorities);
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
#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;
MyUserDetails user = (MyUserDetails) o;
return Objects.equals(username, user.username);
}
}
Also check Spring Security Architecture

How to provide custom UserDetails with additional fields for testing a secured controller method?

Assume I have the following #WebMvcTest and #RestController in a Spring boot applcation (version 2.4.2).
// the test
#Test
#WithUserDetails
public void should_return_ok() throws Exception {
mockMvc.perform(get("/api/products").andExpect(status().isOk());
}
// the controller
#GetMapping(path = "/api/products")
public ResponseEntity<List<Product>> getProducts(#AuthenticationPrincipal CustomUserDetails userDetails) {
List<Product> products = productService.getProductsByUserId(userDetails.getUserId());
return ResponseEntity.ok(products);
}
I also provided a CustomUserDetails class which adds a userId.
#Getter
#Setter
public class CustomUserDetails extends User {
private static final long serialVersionUID = 5540615754152379571L;
private Long userId;
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
I understand that Spring provides the #WithUserDetails annotation to provide an adequate object for testing. And this also allows specifying a custom username, password, etc. However I don't know how I could provide the userId which is necessary so that the controller method can extract it from the CustomUserDetails object.
You can create your own custom UserDetails object in your test class and do the following:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
CustomUserDetails customUserDetails = new CustomUserDetails(...);
mockMvc.perform(get("/api/products").with(user(customUserDetails))).andExpect(status().isOk());
In your implementation of UserDetailsService you should return your instance of UserDetails. For example:
#Override
public UserDetails loadByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Username " + username + " not found");
}
CustomUserDetails customUserDetails = new CustomUserDetails(user);
customUserDetails.setUserId(user.getUserId());
return customUserDetails;
}
public class CustomUserDetails implements UserDetails {
private final Long userId;
private final User user;
...constructors
...getters and setters
}
In your code, you can cast the Authentication object to your CustomUserDetails.
CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication();
Long userId = customUserDetails.getUserId();

Spring session replication with mongodb not working as expected

I am replicating session using mongodb
below is the configuration I am using
#Configuration
#EnableMongoHttpSession
public class MongoSessionReplication {
#Bean
public AbstractMongoSessionConverter mongoSessionConverter() {
List<Module> securityModules = SecurityJackson2Modules.getModules(getClass().getClassLoader());
return new JacksonMongoSessionConverter(securityModules);
}
#Bean
public MongoTemplate mongoTemplate(#Qualifier("replicaSet") Datastore replicaSet){
MongoTemplate mongoTemplate = new MongoTemplate(replicaSet.getMongo(),replicaSet.getDB().getName());
return mongoTemplate;
}
}
Now everything is working fine except the Principal object that spring security creates after loggin in.
I have custom implementation of UserDetails
public class PortalUser extends User {
private String primaryEmailId;
private String redirectUrl;
public PortalUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
public PortalUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, true, true, true, true, authorities);
}
public String getPrimaryEmailId() {
return primaryEmailId;
}
public void setPrimaryEmailId(String primaryEmailId) {
this.primaryEmailId = primaryEmailId;
}
public String getRedirectUrl() {
return redirectUrl;
}
public void setRedirectUrl(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
}
Below is UserDetailsService
#Service
public class PortalUserDetailService implements UserDetailsService {
#Autowired
private SSOServiceAPI ssoServiceAPI;
#Autowired
private UserProfileService userProfileService;
#Override
public UserDetails loadUserByUsername(String hexId) throws UsernameNotFoundException {
UserProfile userProfile = userProfileService.getUserProfileByUserId(hexId);
List<GrantedAuthority> grantedAuthority = new ArrayList<GrantedAuthority>();
if(userProfile!=null) {
grantedAuthority.add(new SimpleGrantedAuthority(userProfile.getSsmRoles().name()));
} else {
grantedAuthority.add(new SimpleGrantedAuthority("USER"));
}
SSOUsers ssoUser = ssoServiceAPI.findSSOUser(hexId, false);
PortalUser portalUser = new PortalUser(hexId, hexId, true, true, true, true, grantedAuthority);
portalUser.setPrimaryEmailId(ssoUser.getPrimaryUserId());
return portalUser;
}
}
Controller
public String getAllProducts(#RequestParam(value = "callback", required = true) String callback, Principal principal, HttpServletRequest request) {
String hexId = principal.getName();
String primaryEmailId = ((PortalUser) ((UsernamePasswordAuthenticationToken) principal).getPrincipal()).getPrimaryEmailId(); //----->> this line fails
}
Above highlighted typecasting failed as it returns instance of UserDetails instead of my custom PortalUser. But this isn't a case when I disable spring-session replication..
You need to implement Spring's Security UserDetails, not User.
update MyUser to the below:
public class SecUserDetails implements UserDetails {
private User user;
public SecUserDetails(User user) {
this.user = user;
}
......
......
......
}

How to add additional details to Spring Security userdetails

I want to add additional information to userdetails like user's Ip address. Is there any way to achieve this? I tried to create a new CustomSpringUser class but the problem is how can i get this information from Authentication object. Is there any other way to store additional information for authenticated user?
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
My custom user class;
public class CustomSpringUser extends org.springframework.security.core.userdetails.User {
public String ip;
public CustomSpringUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomSpringUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, String ip) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.ip= ip;
}
}
Edit: I found that we can add additional information for Authentication but I couldn't found how to do that.
http://docs.spring.io/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/core/Authentication.html#getDetails()
#Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
try {
AppUser appUser = new AppUser();
appUser.setUsername(userName);
AppUser domainUser = genericDao.getByTemplate(appUser).get(0);
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
List<String> roles = new ArrayList<String>();
roles.add(domainUser.getRole().getName());
return new CustomSpringUser(
domainUser.getUsername(),
domainUser.getPassword().toLowerCase(),
enabled,
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
getGrantedAuthorities(roles),
***domainUser.getAccount().getIdentificationId())*** ;
} catch (Exception e) {
genericLogger.saveLog(Logger.Status.ERROR, "Couldn't login", e);
throw new RuntimeException(e);
}
}
To get the UserDetails from the Authentication object/instance use the getPrincipal() method. The getDetails() method is to be used to get additional information about the user (which in general will be an instance of WebAuthenticationDetails).
Links
Authentication javadoc
Authentication.getDetails() javadoc
Authentication.getPrincipal() javadoc

UserDetailsService config for properly getting user

I create this topic from my previous one Get authenticated user entity Spring MVC where I asked question about properly getting authenticated user entity. I adviced that Principal object (for example, on my view <sec:authentication property="principal.customFieldName" />) can has access to my custom fields if my UserDetailsService configuration is right. Does my UserDetailsService configured properly to accomplish this functionality?
#Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = Logger.getLogger(UserDetailsServiceImpl.class);
#Autowired
#Qualifier("hibernateUserDao")
private UserDAO userDAO;
#Override
#Transactional(readOnly = true)
public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException {
UserDetails user = userDAO.findByLogin(userName);
if (user == null) {
logger.error("User was not found! Input login: " + userName);
}
return buildUserFormUserEntity(user);
}
#Transactional(readOnly = true)
private org.springframework.security.core.userdetails.User buildUserFormUserEntity(UserDetails userDetails) {
boolean enableStatus = userDetails.isEnabled();
String userName = userDetails.getLogin();
String password = userDetails.getPassword();
boolean enabled = enableStatus;
boolean accountNonExpired = enableStatus;
boolean credentialsNonExpired = enableStatus;
boolean accountNonLocked = enableStatus;
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(userDetails.getRole()));
User springSecurityUser = new User(userName, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
return springSecurityUser;
}
public UserDAO getUserDAO() {
return userDAO;
}
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
}
I think you need some additional steps to be able succesfully use
<sec:authentication property="principal.customFieldName" />
on some page:
Add your custom user object that implements org.springframework.security.core.userdetails.UserDetails interface. The simpliest way to do it is to extend existing org.springframework.security.core.userdetails.User class: class CutomUser extends User
Add your customFieldName property to CutomUser class.
Use CutomUser as a return type in your UserDetailsServiceImpl.loadUserByUsername(...) method. Do not forget to fill customFieldName at this moment.

Resources