8

This is a Spring Security question.

In my application, I have a User entity as a domain object. Users will be registered and will be logging in with credentials stored in the database. My User domain object contains implementation to support Spring UserDetails object.

The challenge is that I need an ability to log into the application even before the first user is created. In other words, I need to log in as 'admin' to create the 'admin' user.

To make sure my Spring setup is working, I'm currently returning the hardcoded admin user from SpringSecurityUserDetailsServiceImpl.loadUserByUsername(String userName).

public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException {

    User user=null;
    try {
        if("admin".equalsIgnoreCase(userName)) {
            user=new User();
            user.setUserName("ADMIN");
            user.setPassword("adsf"); // assume there's a hash of a true password here
            user.setStatus(UserStatus.ACTIVE);
            user.setAccessLevel(UserAccessLevel.ADMINISTRATOR);
        } else {
            //user = userDAO.getUserByUserName(userName);

        }

    } catch(Throwable t) {
        throw new UsernameNotFoundException("Unable to locate User with user name \"" + userName + "\".", t);
    }

    return user;
}

This works, so now, I'm looking for the right way to do it. One would be to define this default admin user credentials in a properties file and read that properties file within loadUserByUsername(String userName) to construct the admn user object. However, I'm hoping there is a way to do this within the Spring Security xml configuration. I tried security:user name="admin" password="admin" authorities="ADMINISTRATOR" but that apparently does not work when you have security:authentication-provider user-service-ref="customUserDetailsService"

My spring-security.xml

<security:http auto-config="true" use-expressions="true" access-denied-page="/denied">
<security:intercept-url pattern="/login.html" access="permitAll"/>
<security:intercept-url pattern="/style/**" access="permitAll"/>
<security:intercept-url pattern="/user**" access="hasRole('ADMINISTRATOR')"/>
<security:intercept-url pattern="/**" access="hasRole('AUTHOR')"/>

<security:form-login    login-page="/login.html"
                        login-processing-url="/j_spring_security_check" 
                        authentication-failure-url="/login.html?failedAttempt=true"
                        default-target-url="/home.html"/>

<security:logout        invalidate-session="true"
                        logout-success-url="/login"
                        logout-url="/logout"/>
                        </security:http>

<security:authentication-manager>
    <security:authentication-provider user-service-ref="customUserDetailsService">       
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

<bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>


<bean id="customUserDetailsService" class="com.modelsite.services.impl.SpringSecurityUserDetailsServiceImpl"/>

So the question is: how do I define a default admin user that is able to log in and do stuff. Please note, I do not want to handle this with sql imports at set up times.

skaffman
  • 398,947
  • 96
  • 818
  • 769
jacekn
  • 1,521
  • 5
  • 29
  • 50

4 Answers4

4

You can have multiple authentication providers:

  • Use the first like you already did.
  • Add a second with fixed name, password and role for the admin.

(The order of both authentication providers is important; the second is only taken into account if the authentication is not found in the first.)

<security:authentication-manager>
    <security:authentication-provider user-service-ref="customUserDetailsService">       
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
    <security:authentication-provider>
        <security:user-service>
            <security:user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
        </security:user-service>
   </security:authentication-provider>
</security:authentication-manager>

@see also: Can I have multiple security contexts with spring security?

Community
  • 1
  • 1
Ralph
  • 118,862
  • 56
  • 287
  • 383
  • Thanks, Ralph. It does work and it solves my issue. The '' tag didn't work for me but with security namespace '' it did the trick. – jacekn Jan 06 '12 at 18:35
  • Unless you are able to remove the second authentication-provider after creating a user through your application, this is a very bad idea as you will then always have an admin:admin user. – aweigold Jan 06 '12 at 19:49
  • @aweigold you are right, this way the admin has always access -- So one should use a more secure password. -- Anyway this probelm occures with all soultions that use a fixed user name and do not remove them. – Ralph Jan 06 '12 at 19:56
  • Ultimately, you stated you do not want to use SQL import scripts at set up time... but it may be a good idea for you to reconsider. Liquibase is a good option if you are concerned about the database being platform agnostic, or the script running everytime you start up. – aweigold Jan 06 '12 at 20:02
  • @aweigold: much to heavy weight. If jacekn realy wants to tackle the problem: then use a custom authentication provider to replace the second from my config. This custom authentication provider provides the initial admin authentication as long as there is no user in the db created. (If it founds a user in the db, even the admin with the correct password will not get authenticated by that authentication provider) – Ralph Jan 06 '12 at 20:09
  • @Ralph: that can work as well... IMHO however, unless you are using ddl (which is awful to deal with when refactoring), it makes sense to bring in db creation scripts anyways. – aweigold Jan 06 '12 at 20:18
  • @aweigold I agree with that statement – Ralph Jan 06 '12 at 20:19
  • @aweigold: If my application was a commercial undertaking, then I might have enough reasons to execute setup programs, import sql or build some smarts into the application itself. But my application is a working example of Spring 3 that is helping me learn this framework. That's why I don't want to spend time on sql imports and such, as those are not teaching me anything new. Ralph provided a quick answer which does the trick for me. – jacekn Jan 08 '12 at 00:04
  • And frankly, if there is no other setup that an application requires at install time, then this solution looks pretty good to me. The concern you have about security risk can be resolved by a quick redeployment with an updated security file that no longer contains the default admin. I'm sure you'll agree that this would be far easier and quicker than maintaining some sql scripts. But again, every application is different and we don't want to get into a debate on this. :) – jacekn Jan 08 '12 at 00:13
1

Personally, for the admin account I won't go with the basic Spring Security user service, mainly because it lacks the flexibility of a DB-based user management approach. Indeed, you probably don't want to have your admin credentials established once for all, since they can be guessed or stolen or simply forgotten.

Conversely, both password modification and recovery mechanisms should be put in place for all accounts, including the admin one (provided you use a trusted email account for password recovery, but this is a reasonable assumption).

Getting concrete, my approach is the following: I use an AuthenticationManager where I inject a CustomUserDetailService

    <authentication-manager alias="authenticationManager">
        <authentication-provider user-service-ref="customUserDetailsService" >
            <password-encoder ref="passwordEncoder" />
        </authentication-provider>
    </authentication-manager>

    <b:bean id="passwordEncoder"
    class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

which is the following

    @Service
    public class CustomUserDetailsService implements UserDetailsService{

        @Autowired
        @Qualifier("userDaoImpl")
        private UserDao userDaoImpl;

        @Override
        @Transactional
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
        {
            User user = userDaoImpl.loadByUsername(username);
            if (user != null)
                return user;
            else
                throw new UsernameNotFoundException(username + " not found.");
        }
    }

this works for all users, not only the admin.

Now it comes the problem of having the admin account full functional when the application starts. This is accomplished by using an initialization bean to be executed at startup, detailed in the following

    @Component
    public class Initializer {

        @Autowired
        private HibernateTransactionManager transactionManager;
        @Autowired
        @Qualifier("userDaoImpl")
        private UserDao userDao;
        @Autowired
        private CredentialsManager credentialsManager;

        private String resetPassword = "makeItHardToGuess";
        private String adminUsername = "admin";


        @PostConstruct
        private void init()
        {
            //since we are executing on startup, we need to use a TransactionTemplate directly as Spring may haven't setup transction capabilities yet
            TransactionTemplate trxTemplate = new TransactionTemplate(transactionManager);
            trxTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    buildAdmin();
                }
            });
        }

        private void buildAdmin()
        {       
            //here I try to retrieve the Admin from my persistence layer
            ProfiledUser admin = userDao.loadByUsername(adminUsername);

            try
            {
                //If the application is started for the first time (e.g., the admin is not in the DB)
                if(admin==null)
                {   
                    //create a user for the admin                   
                    admin = new ProfiledUser();
                    //and fill her attributes accordingly
                    admin.setUsername(adminUsername);
                    admin.setPassword(credentialsManager.encodePassword(resetPassword));
                    admin.setAccountNonExpired(true);
                    admin.setAccountNonLocked(true);
                    admin.setCredentialsNonExpired(true);
                    admin.setEnabled(true);
                    admin.setEulaAccepted(true);

                    Authority authority = new Authority();
                    authority.setAuthority("ROLE_ADMIN");
                    admin.getAuthorities().add(authority);
                }
                //if the application has previously been started (e.g., the admin is already present in the DB)
                else
                {
                    //reset admin's attributes
                    admin.setPassword(credentialsManager.encodePassword(resetPassword));
                    admin.getAuthorities().clear();
                    Authority authority = new Authority();
                    authority.setAuthority("ROLE_ADMIN");
                    admin.getAuthorities().add(authority);
                    admin.setAccountNonExpired(true);
                    admin.setAccountNonLocked(true);
                    admin.setCredentialsNonExpired(true);
                    admin.setEnabled(true);
                }                   
                userDao.save(admin);
            }
            catch (Exception e)
            {
                e.printStackTrace();
                System.out.println("Errors occurred during initialization. System verification is required.");
            }
        }
    }

please note that the @PostConstruct annotation does not guarantee that spring has its transaction services available, that's why I had to manage the transaction my own. Please refer to this for more details.

Community
  • 1
  • 1
MaVVamaldo
  • 2,505
  • 7
  • 28
  • 50
0

the answer by MaVVamaldo is cool (gave my +1 vote already) apart from the Initializer class. That class is great to initialise the database but it should avoid hard-coding the admin credentials which is unsafe as the source code can be easily retrieved (and it's what the original question asked to avoid in the first place).

A better solution IMHO would be to load the hashed credentials from a .properties file (to which you restrict the access via chmod or similar). for this to work you need to have the following in your security-context.xml

    <authentication-manager>
        <authentication-provider>
            <password-encoder hash="sha">
                <salt-source user-property="username"/>    
            </password-encoder>
            <user-service properties="classpath:/users.properties" />
        </authentication-provider>
    </authentication-manager>

where the .properties file looks like this:

bob=4f393f2314f75650ee50844d8e4f016ab5b3468f,ROLE_ADMIN,enabled

the salt is the username so you calculate it over the string password{username} as explained.

Community
  • 1
  • 1
iammyr
  • 271
  • 5
  • 13
0

The challenge is that I need an ability to log into the application even before the first user is created. In other words, I need to log in as 'admin' to create the 'admin' user.

The way I deal with this problem is to put some smarts into my custom UserDetailsService class and/or its DAO class. When it detects that it has been started with empty user details tables (or something), it initializes them with some user details entries that it reads from a configuration file. This allows you to:

  • load the initial admin account into your production system's user details store
  • load a bunch of test accounts into your test system's user details store for automated unit and system testing.

If that's too much work, just create some SQL statements to insert the relevant rows for the admin command and run them using your database's interactive SQL shell.


Embedding the admin account into your source code is a bad idea because:

  • anyone who can see your sourcecode can see the password (unless you use a hash),
  • it means that you need to modify and recompile the code to change the password, and
  • it means that you'll use the same password in testing and production (unless you add that distinction to your code as well).

These all raise security issues.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • Thanks for your contribution, Stephen. Embedded admin account is only shown here to prove that Spring Security setup is working for me. I don't want people to think I'm struggling with establishing a session. The question clearly states that harcoded account has to go. The choice is between 'smarts' in the UserDetailsService or Spring Security configuration. My preference is for the latter, hence the question. – jacekn Jan 06 '12 at 05:11