Spring Security Ouath2 : Extended UserDetails not returned by the Principal object - spring

Last week I started on extending the UserDetails class to be able to support a custom field. The special thing about this field is that it gets filled with a value that depends an a request parameter. I managed to implement this correctly (so the question does not focus on that).
Now the thing is that after a successfull login the UserDetails object gets filled correctly (I was able to see this using a AuthenticationSuccessHandler) and client recieves a JWT token from the OAuth2 provider. The client then tries to fetch more details on the user by visiting the "/uaa/user" endpoint. This is set to return the Principal object. But after checking the contents of the Principal object I was supprised that the UserDetails object was missing. The method getPrincipal() only returned the username instead of the UserDetails object.
According to this question this is the result of a failed login. The AuthenticationToken (in this case a UsernamePasswordAuthenticationToken) gets rejected by the AuthenticationManager. I have no idea why it should do such a thing. The authentication with the default UserDetails object seems to work just fine. Can someone help me solve this problem?
Some details on the implemented classes (as mentioned above). Some code has been left out here for reasons.
CustomUserDetails
public class CustomUserDetails extends User {
private final Integer custom;
public CustomUserDetails (...default params..., Integer custom) {
super(...default params...);
this.custom = custom;
}
}
CustomUserDetailsService
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Override
public CustomUserDetails loadUserByUsername(String username) throw UsernameNotFoundException {
return new CustomUserDetails(...default params..., 12345);
}
}
Configuration
#Autowired
private CustomUserDetails userDetails;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetails);
}
User Endpoint
#RequestMapping(value = "/user", method = RequestMethod.GET)
#ResponseBody
public Principal getDetails(Principal user) {
return user;
}
The Principal object returned here should have the UserDetails object inside of it and should return this to the client. But instead of that it only returns a String with the username when you call getPrincipal();
In the end I want the JSON returned by the User endpoint (which returns the Principle object) to contain the custom field I added to the UserDetails.
Thanks in advance.

Generally, you need the annotation #AuthenticationPrincipal, but I will suggest you to build your own annotation, something like this:
/**
* Our own {#link AuthenticationPrincipal} annotation as suggested by
* http://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#mvc-authentication-principal
*
*/
#Target({ElementType.PARAMETER, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#AuthenticationPrincipal
public #interface CurrentUser {}
Then you can have this Principal in this way:
#RequestMapping(..)
public Principal test(#CurrentUser Principal principal){..}
BUT, IMHO you should have your own Impl of Principal, or rather extends the existing impl. something like this:
public MyPrincipal extends org.springframework.security.core.userdetails.User {..}
In this case you can return values whatever you want to.

You can use this method, in any case to get extended user details object, in controller or anywhere you need. There can be cons in this method, but its effective.
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
MyUserDetails myUser = (MyUserDetails) auth.getPrincipal();
public class MyUserDetails implements
org.springframework.security.core.userdetails.UserDetails {
private User user; //This is the user domain.
private String firstName;
private String lastName;
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
authList.add(new SimpleGrantedAuthority(user.getRole().getName()));
return authList;
}
public String getPassword() {
return user.getPassword();
}
public String getUsername() {
return user.getEmail();
}
public boolean isAccountNonExpired() {
return ((user.getAccountState() == AccountState.InActive) || (user.getAccountState() == AccountState.Blocked) ? false : true);
}
public boolean isAccountNonLocked() {
return (user.getAccountState() == AccountState.Locked) ? false : true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return ((user.getAccountState() == AccountState.Active)
|| (user.getAccountState() == AccountState.PasswordReset)
|| (user.getAccountState() == AccountState.UnVerified) ? true
: false);
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
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;
}
}

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

Basic Auth Spring security with enum Roles and Permissions always return 401

i am new to Spring Security, i just have a User with enum Role and enum permissions, i wanted to have a basic auth using postman and to test it , but i always get 401 status code.
I am not sure what is the problem exactly because no errors i receive or no exeption occured but all i know is that i can not log in with basic auth using postman perhaps my configuration is not perfect or UserDetails and UserDetailsServices are not like they should be or maybe capturing the authorities in UserDetails is not working at all.
or maybe my password is not encoded in database and that's why the authentication can not pass.
My ApplicationSecurityConfig:
`#Configuration
#EnableWebSecurity
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {
//private final PasswordEncoder passwordEncoder;
private final ApplicationUserDetailsService applicationUserDetailsService;
#Autowired
public ApplicationSecurityConfig(PasswordEncoder passwordEncoder,
ApplicationUserDetailsService applicationUserDetailsService) {
// this.passwordEncoder = passwordEncoder;
this.applicationUserDetailsService = applicationUserDetailsService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/","/index","/css/*","/js/*") .permitAll()
//MEMBER
.antMatchers("/api/**").hasAnyRole(
ApplicationUserRole.SUPER_ADMIN.name(),
ApplicationUserRole.ADMIN.name(),
ApplicationUserRole.MEMBER.name()
)
.antMatchers(HttpMethod.GET,"/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_READ.name(),
ApplicationUserPermissions.ADMIN_READ.name(),
ApplicationUserPermissions.MEMBER_READ.name()
)
//ADMIN
.antMatchers("/admin/api/**").hasAnyRole(ApplicationUserRole.ADMIN.name(),ApplicationUserRole.SUPER_ADMIN.name())
.antMatchers(HttpMethod.POST,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PUT,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PATCH,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.DELETE,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
//SUPER_ADMIN
.antMatchers("/super/admin/api/**").hasAnyRole(
ApplicationUserRole.SUPER_ADMIN.name()
)
.antMatchers(HttpMethod.POST,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PUT,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PATCH,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.DELETE,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(applicationUserDetailsService);
}`
ApplicationUserRole:
package com.github.workTimeMangementGithub.security;
import com.google.common.collect.Sets; import
org.springframework.security.core.GrantedAuthority; import
org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Set; import java.util.stream.Collectors;
public enum ApplicationUserRole {
SUPER_ADMIN(Sets.newHashSet(ApplicationUserPermissions.SUPER_ADMIN_READ,ApplicationUserPermissions.SUPER_ADMIN_WRITE)),
ADMIN(Sets.newHashSet(ApplicationUserPermissions.ADMIN_READ,ApplicationUserPermissions.ADMIN_WRITE)),
MEMBER(Sets.newHashSet(ApplicationUserPermissions.MEMBER_READ,ApplicationUserPermissions.MEMBER_WRITE));
private final Set<ApplicationUserPermissions> permissions;
ApplicationUserRole(Set<ApplicationUserPermissions> permissions) {
this.permissions = permissions;
}
public Set<ApplicationUserPermissions> getPermissions() {
return permissions;
}
public Set<GrantedAuthority> getGrantedAuthorities() {
Set<GrantedAuthority> permissions = getPermissions().stream().map(permission-> new
SimpleGrantedAuthority(permission.getPermission())).collect(Collectors.toSet());
permissions.add(new SimpleGrantedAuthority("ROLE_"+this.name()));
return permissions;
}
}
Here i have implemented User Role for Role Based Auth and i connect them with their permissions
My ApplicationUserPermissions
public enum ApplicationUserPermissions {
SUPER_ADMIN_WRITE("super_admin:write"),
SUPER_ADMIN_READ("super_admin:read"),
ADMIN_WRITE("admin:write"),
ADMIN_READ("admin:read"),
MEMBER_WRITE("member:write"),
MEMBER_READ("member:read");
private final String permission;
ApplicationUserPermissions(String permission) {
this.permission = permission;
}
public String getPermission() {
return permission;
}
}
Here i Created the permissions for every User Role to determine all permissions and privileges for each role.
My ApplicationUserDetailsService
import java.util.Optional;
#Service
#Slf4j
public class ApplicationUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
#Autowired
public ApplicationUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> user = Optional.of(userRepository.findUserByUsername(username));
if(user.get() != null){
UserDTO userDto = UserMapper.toDTO(user.get());
log.info("User Found "+ userDto.getUsername());
}else {
log.warn("User NOT Found ");
}
user.orElseThrow(() -> new UsernameNotFoundException("Not found: " + username));
return new ApplicationUserDetails(user.get());
}
}
Here i have implemented ApplicationUserDetailsService and called the method loadUserByUsername with handling UserNotFoundException in case the user is not found.
My ApplicationUserDetails:
#Slf4j
public class ApplicationUserDetails implements UserDetails {
private List<? extends GrantedAuthority> grantedAuthorities;
private String username;
private String password;
private boolean isAccountNonExpired;
private boolean isAccountNonLocked;
private boolean isCredentialsNonExpired;
private boolean isEnabled;
public ApplicationUserDetails(List<? extends GrantedAuthority> grantedAuthorities, String username, String password, boolean isAccountNonExpired, boolean isAccountNonLocked, boolean isCredentialsNonExpired, boolean isEnabled) {
this.grantedAuthorities = grantedAuthorities;
this.username = username;
this.password = password;
this.isAccountNonExpired = isAccountNonExpired;
this.isAccountNonLocked = isAccountNonLocked;
this.isCredentialsNonExpired = isCredentialsNonExpired;
this.isEnabled = isEnabled;
}
public ApplicationUserDetails(User user) {
List<? extends GrantedAuthority> authorities = new ArrayList<>(ApplicationUserRole.ADMIN.getGrantedAuthorities());
this.grantedAuthorities = authorities;
log.warn("authorities "+authorities);
this.username = user.getUsername();
this.password = user.getPassword();
this.isAccountNonExpired = true;
this.isAccountNonLocked = true;
this.isCredentialsNonExpired = true;
this.isEnabled = true;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return username;
}
#Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
#Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
#Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
#Override
public boolean isEnabled() {
return isEnabled;
}
Here i have implemented ApplicationUserDetails and override some methods.
My problem is that i can not authenticate using basic auth via Postman.
Here a screen capture of the users of the database:
I am trying to find out what is wrong with my code , i follow many tutorials but no full example of working with enum Roles and permissions with JPA authentication , i spend a lot of time and i still don't know what is wrong exactly with my code.
The logger Slf4j is no showing the authenticated user in console and i don't know why.
Postman:
Spring Boot log Captures:
Any help will be so appreciated.

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();

How can I get user's logged Id from SecurityContextHolder using Spring Social?

So, how can I get the user's id from a current logged user from any social providers?
Well I know I can build a custom SocialUser, the same I do for User, but in the case there is no getter on SocialUserDetails and the method I got just accepts userDetails, instead of a normal "Person" entity.
public class SocialUsersDetailServiceImpl implements SocialUserDetailsService {
private UserDetailsService service;
public SocialUsersDetailServiceImpl(UserDetailsService service) {
this.service = service;
}
#Override
public CSocialUserDetails loadUserByUserId(String username) throws UsernameNotFoundException, DataAccessException {
UserDetails userDetails = (UserDetails) service.loadUserByUsername(username);
return new CustomSocialUser(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities(), userDetails.getUserIdentifier()??);
}
}
But there is no ".getUserIdentifier()" method on UserDetails, there is some workaround for this?
The way I do for User:
#Service
public class UserDetailsServiceImpl implements CUserDetailsService {
#Resource
private PersonRepository respository;
#Override
public CUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Person p = repository.findByUsername(username);
return new CUser(p.getUsername(), p.getPassword(), p.grantedAuthorities(), p.getIdPerson());
}
}
and the CUser:
public class CUser extends User{
private Number identifier;
public CUser(String username, String password, Collection<? extends GrantedAuthority> authorities, Number identifier) {
super(username, password, authorities);
this.identifier = identifier;
}
public Number getUserIdentifier() {
return identifier;
}
}

How to implement AuditorAware with Spring Data JPA and Spring Security?

We use Hibernate/JPA, Spring, Spring Data and Spring Security in our application. I have a standard User entity which is mapped using JPA. Further, I have a UserRepository
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByUsername(String username);
}
which follows the Spring Data convention for naming query methods. I have an entity
#Entity
public class Foo extends AbstractAuditable<User, Long> {
private String name;
}
I want to use Spring Data auditing support. (As descripe here.) Hence I created a AuditorService as follows:
#Service
public class AuditorService implements AuditorAware<User> {
private UserRepository userRepository;
#Override
public User getCurrentAuditor() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
List<User> users = userRepository.findByUsername(username);
if (users.size() > 0) {
return users.get(0);
} else {
throw new IllegalArgumentException();
}
}
#Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
When I create a method
#Transactional
public void createFoo() {
Foo bar = new Foo();
fooRepository.save(foo);
}
Where everything is correctly wired and FooRepository is a Spring Data CrudRepository. Then a StackOverflowError is thrown since the the call to findByUsername seems to trigger hibernate to flush the data to the database which triggers AuditingEntityListener who calls AuditorService#getCurrentAuditor which again triggers a flush and so on.
How to avoid this recursion? Is there a "canonical way" to load the User entity? Or is there a way to prevent Hibernate/JPA from flushing?
I got the same issue and what I did was just change the propagation on the findByUsername(username) method to Propagation.REQUIRES_NEW, I suspected that was a problem with the transactions, so I changed to use a new transaction and that worked well for me. I hope this can help.
#Repository
public interface UserRepository extends JpaRepository<User, String> {
#Transactional(propagation = Propagation.REQUIRES_NEW)
List<User> findByUsername(String username);
}
The solution is not to fetch the User record in the AuditorAware implementation. This triggers the described loop, since a select query triggers a flush (this is the case since Hibernate/JPA wants to write the data to the database to commit the transaction before executing the select), which triggers a call to AuditorAware#getCurrentAuditor.
The solution is to store the User record in the UserDetails provided to Spring Security. Hence I created my own implementation:
public class UserAwareUserDetails implements UserDetails {
private final User user;
private final Collection<? extends GrantedAuthority> grantedAuthorities;
public UserAwareUserDetails(User user) {
this(user, new ArrayList<GrantedAuthority>());
}
public UserAwareUserDetails(User user, Collection<? extends GrantedAuthority> grantedAuthorities) {
this.user = user;
this.grantedAuthorities = grantedAuthorities;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
#Override
public String getPassword() {
return user.getSaltedPassword();
}
#Override
public String getUsername() {
return user.getUsername();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
public User getUser() {
return user;
}
}
Further, I changed my UserDetailsService to load the User and create UserAwareUserDetails. Now it is possible to access the User instance through the SercurityContextHolder:
#Override
public User getCurrentAuditor() {
return ((UserAwareUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();
}
It looks like you use a User entity for two different things:
authentication
audit
I think it will be better to prepare a special AuditableUser for audit purpose (it will have identical username field as original User).
Consider following case: you want to delete some User from database. If all your audit objects are linked to User then they will a) loose author b) may be deleted by cascade too (depends on how the link is implemented). Not sure that you want it.
So by using special AuditableUser you will have:
no recursion
ability to delete some User from the system and preserve all audit info about it
To be honest, You do not actually require one another entity.
For example, I had similar problem and I resolved it in following way:
public class SpringSecurityAuditorAware implements AuditorAware<SUser>, ApplicationListener<ContextRefreshedEvent> {
private static final Logger LOGGER = getLogger(SpringSecurityAuditorAware.class);
#Autowired
SUserRepository repository;
private SUser systemUser;
#Override
public SUser getCurrentAuditor() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SUser principal;
if (authentication == null || !authentication.isAuthenticated()) {
principal = systemUser;
} else {
principal = (SUser) authentication.getPrincipal();
}
LOGGER.info(String.format("Current auditor is >>> %s", principal));
return principal;
}
#Override
public void onApplicationEvent(final ContextRefreshedEvent event) {
if (this.systemUser == null) {
LOGGER.info("%s >>> loading system user");
systemUser = this.repository.findOne(QSUser.sUser.credentials.login.eq("SYSTEM"));
}
}
}
Where SUser is both the class which I use for auditing as well as for the security.
I had maybe different use case than Yours and my approach will be deleted after, but it can be resolved like this.

Resources