2

A Spring Boot app has a custom UserDetails. Everything works properly when all the app does is authenticate existing users from a database with the User class. However, problems emerge when I try to make changes so that people can create new user accounts through the app GUI. Specifically, changing the custom UserDetailsService to become a UserDetailsManager required changing the User class to explicitly implement UserDetails, which in turn is now causing Spring to be unable to compile a jar when I type java -jar target/modular-0.0.1-SNAPSHOT.jar in the terminal from the root directory of the app.

The core error message is:

    java.lang.IllegalArgumentException: 
    Not an managed type: interface org.springframework.security.core.userdetails.UserDetails

What specific changes need to be made to the code below in order to enable Spring to be able to compile the app with the custom UserDetails and UserDetailsManager?

Here is the code for the core class in the app, which includes the Spring Security configuration:

@SpringBootApplication
@Controller
@EnableJpaRepositories(basePackages = "demo", considerNestedRepositories = true)
public class UiApplication extends WebMvcConfigurerAdapter {

    @Autowired
    private WebLeadRepository myrepo;   

    @Autowired
    private Users users;//duplicate from AuthenticationSecurity internal class below. Remove one?

    // Match everything without a suffix (so not a static resource)
    @RequestMapping(value = "/{[path:[^\\.]*}")
    public String redirect() {
        // Forward to home page so that route is preserved.
        return "forward:/";
    }

    @RequestMapping("/user")
    @ResponseBody
    public Principal user(Principal user) {
        return user;
    }

//lots of other @RequestMapping @ResponseBody url handling methods

    public static void main(String[] args) {
        SpringApplication.run(UiApplication.class, args);
    }

    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Configuration
    protected static class AuthenticationSecurity extends GlobalAuthenticationConfigurerAdapter {

        @Autowired
        private Users users;

        @Override
        public void init(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(users);
        }
    }

    @SuppressWarnings("deprecation")
    @Configuration
    @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
    @EnableWebMvcSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.httpBasic().and().authorizeRequests()
                .antMatchers("/sign-up").permitAll()
                .antMatchers("/index.html", "/", "/login", "/something*") 
                .permitAll().anyRequest().authenticated().and().csrf()
                .csrfTokenRepository(csrfTokenRepository()).and()
                .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
        }

        private Filter csrfHeaderFilter() {
            return new OncePerRequestFilter() {
                @Override
                protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
                        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
                        if (csrf != null) {
                            Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
                            String token = csrf.getToken();
                            if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                                cookie = new Cookie("XSRF-TOKEN", token);
                                cookie.setPath("/");
                                response.addCookie(cookie);
                        }
                    }
                    filterChain.doFilter(request, response);
                }
            };
        }

        private CsrfTokenRepository csrfTokenRepository() {
            HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
            repository.setHeaderName("X-XSRF-TOKEN");
            return repository;
        }
    }

    @Repository//This repository is what Spring cannot seem to create in the stack trace
    interface UserRepository extends CrudRepository<UserDetails, Long> {
        User findByName(String name);
    }

}

Users.java is:

@Service
class Users implements UserDetailsManager {

    private UserRepository repo;

    @Autowired
    public Users(UserRepository repo) {this.repo = repo;}

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = repo.findByName(username);
        if (user == null) {throw new UsernameNotFoundException("Username was not found. ");}
        List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
        if (username.equals("admin")) {auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN");}
        String password = user.getPassword();
        return new org.springframework.security.core.userdetails.User(username, password, auth);
    }

    @Override
    public void createUser(UserDetails user) {// TODO Auto-generated method stub
        repo.save(user);
    }

    @Override
    public void updateUser(UserDetails user) {// TODO Auto-generated method stub
        repo.save(user);
    }

    @Override
    public void deleteUser(String username) {// TODO Auto-generated method stub
        User deluser = (User)this.loadUserByUsername(username);
        repo.delete(deluser);
    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {
        // TODO Auto-generated method stub
    }

    @Override
    public boolean userExists(String username) {
        // TODO Auto-generated method stub
        return false;
    }

}  

And User.java is:

@Entity
class User implements UserDetails{

    @GeneratedValue
    @Id
    private Long iduser;
    private String name;//valid email address only
    private String password;
//lots of other properties that model all the things the User does in the app

    //getters and setters
    public String getName() {return name;}//valid email address
    public void setName(String name) {this.name = name;}//valid email address

    public String getPassword() {return password;}
    public void setPassword(String password) {this.password = password;}

    //LOTS OF OTHER GETTERS AND SETTERS OMITTED HERE, THAT MANAGE MANY CUSTOM PROPERTIES

    // Also, All the following are for implementing UserDetails
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {// TODO Auto-generated method stub
        return null;
    }
    @Override
    public String getUsername() {// TODO Auto-generated method stub
        return null;
    }
    @Override
    public boolean isAccountNonExpired() {// TODO Auto-generated method stub
        return false;
    }
    @Override
    public boolean isAccountNonLocked() {// TODO Auto-generated method stub
        return false;
    }
    @Override
    public boolean isCredentialsNonExpired() {// TODO Auto-generated method stub
        return false;
    }
    @Override
    public boolean isEnabled() {// TODO Auto-generated method stub
        return false;
    }
}

WebLeadRepository.java is:

public interface WebLeadRepository extends JpaRepository<WebLead, Long> {

    List<WebLead> findByLastname(String lastName);
    List<WebLead> findBySessionid(String sid);
    WebLead findByIdlead(Long idl);
}

The complete stack trace is too long for this posting, but I have uploaded the complete stack trace to a file sharing site, which you can view by clicking on this link. In addition, the root cause in the stack trace is summarized below as follows:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'uiApplication.UserRepository': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: interface org.springframework.security.core.userdetails.UserDetails
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1578)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1192)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1116)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1014)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:813)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
    ... 38 more
Caused by: java.lang.IllegalArgumentException: Not an managed type: interface org.springframework.security.core.userdetails.UserDetails
    at org.hibernate.jpa.internal.metamodel.MetamodelImpl.managedType(MetamodelImpl.java:219)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:68)
    at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getEntityInformation(JpaEntityInformationSupport.java:67)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:152)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:99)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:81)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:185)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:251)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:237)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:92)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1637)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574)
CodeMed
  • 9,527
  • 70
  • 212
  • 364
  • 2
    Your repository needs a `User` not a `UserDetails` in the type declaration. Change `CrudRepository` to `CrudRepository`. – M. Deinum Jan 11 '16 at 04:58
  • In your custom implements of the `UserDetailsManager` you need to cast the `UserDetails` to the `User` object. – M. Deinum Jan 11 '16 at 06:52
  • @M.Deinum Your comments provided the answer. If you would like to write them up as an answer, I would be happy to mark it as accepted and +1. Specifically, the solution involved casting objects in the various methods back and forth between `User` and `UserDetails` in order for the CrudRepository to work with `User` instances with the datasource, while also allowing Spring Security to work with `UserDetails` objects. – CodeMed Jan 11 '16 at 22:16

4 Answers4

1

I took the approach of having two classes: a User entity and a custom user details class. I wrote a helper method to generate a CustomUserDetails from a (Entity) User. This way, there is separation / distinction between your Spring object and your entity object.

Edit: code

Custom user details:

public class CustomUser extends User implements UserDetails, CredentialsContainer {

User Entity:

@Entity
@Table(name="users")
public class User {

This is all wired up by a custom user details service:

public class CustomUserDetailsService extends JdbcUserDetailsManager implements UserDetailsService {    

In loadByUserName (one of the methods you must implement in CustomUserDetailsService, I create a CustomUserDetails from the User entity:

CustomUser customUser = user.getUserDetails();
Brian Kates
  • 480
  • 4
  • 14
  • Can you please show working code? In its present form, your answer is not yet helpful. – CodeMed Jan 11 '16 at 04:11
  • Thank you and +1 for adding code and elaborating. I opted to take the steps shown in M.Dienam's accepted answer. – CodeMed Jan 12 '16 at 07:00
1

See this answer, which explains the error "Not a managed type" that is output in your stacktrace. Oliver Gierke, a contributor to Spring Data, notes that there can be a package problem associated with JPA entities, however that would only be your problem if your Spring Boot application is having trouble discovering all the classes in your app, so it may not answer your question.

Another possibility is this answer, again by Oliver Gierke, who notes that you need a concrete implementation of your JPA repository in some situations. You haven't posted your WebLeadRepository, so I can't be sure.

If neither of these answers helps, then I'm stumped too.

Community
  • 1
  • 1
ben3000
  • 4,629
  • 6
  • 24
  • 43
  • Goodness me! I'm happy to remove this answer, if necessary. The similarity between a plain Spring-managed application using JPA and your Spring Boot application using JPA is clear, and both applications are generating in the same exception. I wasn't aware that you'd looked at that question. – ben3000 Jan 11 '16 at 03:53
  • This did not resolve the problem. Note that M.Deinum's comments below the OP above are what actually solved the problem. The +1 here is just to thank this user for his time. – CodeMed Jan 11 '16 at 23:01
1

First you need to fix the mapping for your repository, the object you are trying to persist is a User not a UserDetails. Spring Data JPA needs this information so that it can scan the entity and create the necessary bindings.

@Repository
interface UserRepository extends CrudRepository<User, Long> {
    User findByName(String name);
}

Now you will have a compilation issue in your custom UserDetailsManager class Users. As all the methods take a UserDetails object and not your User object which is the one expected by your UserRepository. To fix this simply cast the argument to User before passing it to the UserRepository.

@Override
public void createUser(UserDetails user) {// TODO Auto-generated method stub
    repo.save((User) user);
}

@Override
public void updateUser(UserDetails user) {// TODO Auto-generated method stub
    repo.save((User) user);
}
M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • I re-framed by other question based on continuing research. My hope is to make it easier for people to read. Are you willing to comment on it? http://stackoverflow.com/questions/34932581/how-do-i-force-spring-security-to-update-xsrf-token-cookie – CodeMed Jan 21 '16 at 19:32
0

You may need the @MappedSuperClass annotation on the UserDetails interface otherwise hibernate wouldn't know how to persist anything implementing that interface.

james_s_tayler
  • 1,823
  • 1
  • 15
  • 20