Exploring Google and OpenID login with Spring Security and Spring Roo

18 Jan

Overview

OpenID is an open framework for decentralized user authentication, allowing users to manage their online identity through a single trusted provider. Websites supporting OpenID login allow users to login with the same credentials as this OpenID Provider. In this tutorial we will explore Spring Security’s OpenID support, and use Spring Roo to build a simple application allowing users to login using their Google or other OpenID account.

In order to be able to test OpenID login, you will simply need your Google account. Google’s implementation is however slightly different to many of the other providers, so in order to test more generic OpenID login, you may also want to sign up for your own OpenID from one of the available providers.

Just want to run it?

Download and unzip the project, run mvn tomcat:run from a terminal, and open up a browser to http://localhost:8080/openidlogin

Dependencies

Apache Maven 2.2.1 (download)
Spring Roo 1.1.0.RELEASE (download)
Spring Security OpenID 3.0.5.RELEASE (will be downloaded by Maven)
openid4java 0.9.5 (will be downloaded by Maven)

Setting up the Roo project

We will setup the project using Spring Roo’s scripting functionality. Included in the attached project is the script file script.roo. The commands contained in this file are exactly the same as those you would type into the Roo shell. To run the script, open up a Roo shell and type script –file script.roo

The script:

  • creates a Roo project with a structure following the standard Maven conventions
  • adds the additional dependencies required for OpenID login (Spring Security OpenID, openid4java) to the Maven project object model (POM)
  • installs a JPA persistence provider
  • adds the domain model objects (UserRole, EmployeeStatus and Employee)
  • creates the Spring MVC web frontend
  • configures Spring Security
  • and adds two java classes which we will later expand (InsertTestData and OpenIdUserDetailsService).
# Project
project --topLevelPackage za.co.bsg.rnd.trf.openidlogin

# Persistence
persistence setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY --databaseName openidlogin --hostName localhost

# Dependencies
dependency add --groupId org.springframework.security --artifactId spring-security-openid --version 3.0.5.RELEASE
dependency add --groupId org.openid4java --artifactId openid4java --version 0.9.5

# Domain model
enum type --class ~.domain.UserRole
enum constant --name ROLE_ADMIN
enum constant --name ROLE_USER

enum type --class ~.domain.EmployeeStatus
enum constant --name ACTIVE
enum constant --name DORMANT
enum constant --name RESIGNED
enum constant --name TERMINATED

entity --class ~.domain.Employee --table employee --identifierColumn employee_id
field string --fieldName username --column username --notNull
field string --fieldName password --column password --notNull
field enum --type ~.domain.UserRole --fieldName userRole --notNull --enumType STRING
field string --fieldName openIdIdentifier --column openid_identifier
field string --fieldName firstName --column first_name
field string --fieldName lastName --column last_name
field string --fieldName emailAddress --column email_address
field enum --type ~.domain.EmployeeStatus --fieldName status --notNull --enumType STRING

# We will also need a finder
finder add --finderName findEmployeesByOpenIdIdentifier --class ~.domain.Employee

# Scaffold the web frontend
controller all --package ~.web

# Spring Security
security setup

# Other classes
class --class ~.InsertTestData
class --class ~.OpenIdUserDetailsService

 

Testing the Roo generated code

At this stage we are able to test the Roo generated code. To do so, run mvn tomcat:run in a terminal and open up a browser to http://localhost:8080/openidlogin

In addition to configuring Spring security in applicationContext-security.xml, the Roo command security setup has also added an in-memory user-service with two example users. We may login with one of these users by browsing to http://localhost:8080/openidlogin/login (username “admin”, with a password of “admin”).

Test data (optional)

This InsertTestData class would normally be added to the test package and used to provide data for integration tests. As this application uses an in-memory database, it is convenient to use the same method to populate the database with basic data on each run.

@Component
@Configurable
public class InsertTestData implements
                    ApplicationListener {

    public static final String PASSWORD = "password";

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        init();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void init() {
        if (!Employee.findAllEmployees().isEmpty()) {
            // don't do anything if there is data in the db
            return;
        }

        Employee employeeAdminActive = new Employee();
        employeeAdminActive.setUsername("user1");
        employeeAdminActive.setPassword(PASSWORD);
        employeeAdminActive.setUserRole(UserRole.ROLE_ADMIN);
        employeeAdminActive.setStatus(EmployeeStatus.ACTIVE);
        employeeAdminActive.persist();

        Employee employee2 = new Employee();
        employee2.setUsername("user2");
        employee2.setPassword(PASSWORD);
        employee2.setUserRole(UserRole.ROLE_USER);
        employee2.setFirstName("Peter");
        employee2.setLastName("Jones");
        employee2.setStatus(EmployeeStatus.ACTIVE);
        employee2.persist();

        Employee employee3 = new Employee();
        employee3.setUsername("user3");
        employee3.setPassword(PASSWORD);
        employee3.setUserRole(UserRole.ROLE_USER);
        employee3.setFirstName("Christina");
        employee3.setLastName("Applegate");
        employee3.setStatus(EmployeeStatus.RESIGNED);
        employee3.persist();
    }
}

 

The status of user with username user3 has been set to RESIGNED. We will use this user to test that disabled users are not able to login, and receive an error message when attempting to do so:

Your login attempt was not successful, try again. Reason: User is disabled .

Securing the application

At this point the application does not require a user to login in order to access any of its contents. For this example, we will assume that a user must login in order to access any pages other than the home and login pages. To achieve this, we may update the HTTP security configurations intercept-url elements in applicationContext-security.xml:

<intercept-url pattern="/**" access="isAuthenticated()" />

 

The home and login pages still need to be accessible to all users though, so we add a further two intercept-url elements before the “catch-all” property above:

<intercept-url pattern="/" access="permitAll" />
<intercept-url pattern="/login/**" access="permitAll" />

 

JDBC based UserDetailsService

As previously mentioned, be default Roo adds an in-memory user-service, which is useful for testing purposes, however in reality user details are usually stored in a database. To achieve this, we may configure a JDBC based UserDetailsService by replacing the in-memory UserDetailsService (<user-service>). Convention over configuration assumes the database is structured as show below.

default-schema-diagram

In this case, however, the employee table will hold the user details required, and we may simplify the schema by adding the user’s single authority (role) directly to the user (employee) table. This means that we also need to override the default queries that JdbcDaoImpl (the JDBC-based implementation of the UserDetailsService interface) uses.

<jdbc-user-service
    data-source-ref="dataSource"
    users-by-username-query=
                        "SELECT
                            U.username,
                            U.password,
                            U.status = 'ACTIVE'
                         FROM
                            employee U
                         WHERE
                            U.username=?"
    authorities-by-username-query=
                        "SELECT
                            U.username,
                            U.user_role as authority
                         FROM
                            employee U
                         WHERE
                            U.username=?" />

 

For simplicity we will remove the <password-encoder> element and use pain text passwords.

Running the application, we can now login using the credentials of one of the persisted users created in InsertTestData (e.g. user1, password)

Employees as Spring Security Users

As employees are the users in this application, we may wish to have the Employee class implement the Spring Security UserDetails interface. To do so, we must implement a few getter methods in Employee.java

@RooJavaBean
@RooToString
@RooEntity(
        identifierColumn = "employee_id",
        table = "employee",
        finders = { "findEmployeesByOpenIdIdentifier" })
public class Employee implements UserDetails {

. . .

@Override
public String getUsername() {
    return this.username;
}

@Override
public String getPassword() {
    return this.password;
}

@Override
public boolean isAccountNonExpired() {
    return true;
}

@Override
public boolean isAccountNonLocked() {
    return true;
}

@Override
public boolean isCredentialsNonExpired() {
    return true;
}

@Override
public boolean isEnabled() {
    return this.status == EmployeeStatus.ACTIVE;
}

@Override
public Collection getAuthorities() {
    Collection grantedAuthorities = new HashSet();
    grantedAuthorities.add(
            new GrantedAuthorityImpl(this.userRole.name()));
    return grantedAuthorities;
}

. . .

The getUsername() and getPassword() methods will conflict with those implemented in Employee_Roo_JavaBean.aj. If running, Roo will automatically correct this. If not, you may simply remove the methods from Employee_Roo_JavaBean.aj manually.

OpenID UserDetailsService

In order to provide the authentication-manager with a user based on an employee’s openIdIdentifier rather than username, we implement a custom service for loading user details. This will be used when authenticating OpenID logins instead of the JDBC based UserDetailsService previously configured, which will still be used for non-openid based logins.

public class OpenIdUserDetailsService implements UserDetailsService {
    public UserDetails loadUserByUsername(String openIdIdentifier) {
        List employeeList =
                Employee.findEmployeesByOpenIdIdentifier(openIdIdentifier).getResultList();
        Employee employee = employeeList.size() == 0 ? null : employeeList.get(0);
        if (employee == null) {
            throw new UsernameNotFoundException("User not found for OpenID: " + openIdIdentifier);
        } else {
            if (!employee .isEnabled()) {
                throw new DisabledException("User is disabled");
            }
            return employee;
        }
    }
}

Having implemented the UserDetails interface, we are now able to return Employee objects in the loadUserByUsername() method. A UserDetailsService should only be used to lookup the user and not perform any sort of validation, however in this example application it was is convenient place to check if the employee had been disabled.

Configuring OpenID login

Configuring OpenID login is now simply achieved by adding an <openid-login> element to the HTTP security configurations in applicationContext-security.xml:

. . .
<intercept-url pattern="/**" access="isAuthenticated()" />
<openid-login authentication-failure-url="/login?login_error=t" user-service-ref="openIdUserService" />
</http>
. . .

 

We must also configure the OpenIdUserDetailsService bean referenced by the <openid-login> element’s user-service-ref property. We will add this after the </authentication-manager> closing tag:

. . .
</authentication-manager>
<beans:bean id="openIdUserService" class="za.co.bsg.rnd.trf.openidlogin.OpenIdUserDetailsService" />
. . .

 

Updating the login screen

We may now add the markup for the additional login methods to the login screen (login.jspx). We will place it just after the existing </util:panel> closing tag:

  . . .
<spring:message code="button_reset" var="reset_label" />
        <input id="reset" type="reset" value="${fn:escapeXml(reset_label)}" />
      </div>
    </form>
  </util:panel>

  <spring:url value="/j_spring_openid_security_check" var="form_url_openid" />
  <spring:url var="google" value="/resources/images/google-account-logo.png" />
  <spring:url var="openid" value="/resources/images/openid-logo.png" />

  <util:panel id="title_google" title="Google Login" >
    <img src="${google}" style="float:right" />
    <p style="height:0.25em"/>
    <form action="${fn:escapeXml(form_url_openid)}" method="post">
        <input name="openid_identifier" size="50"
               maxlength="100" type="hidden"
               value="http://www.google.com/accounts/o8/id"/>
        <div class="submit">
           <input id="proceed_google" type="submit" value="Sign in with Google" />
        </div>
    </form>
    <br/>
    <p/>
  </util:panel>

  <util:panel id="title_openid" title="OpenID Login" >
    <a href="http://openid.net/get-an-openid/" target="_blank" title="Where do I get one?">
        <img src="${openid}" style="float:right" />
    </a>
    <p/>
    <form action="${fn:escapeXml(form_url_openid)}" method="post">
       <div>
            <label for="openid_identifier">OpenID</label>
            <input id="openid_identifier" name="openid_identifier" type="text" style="width:150px" />
        </div>
        <br/>
        <div class="submit">
            <input id="proceed_openid" type="submit" value="${fn:escapeXml(submit_label)}" />
        </div>
    </form>
  </util:panel>
  . . .

 

Google login

When logging in with Google, the user is not required to provide an OpenID identifier/URL. The reason for this is that Google’s implementation of OpenID is slightly different to that of most providers. Rather than assign user-friendly OpenID identifiers to users, Google expects that websites allowing OpenID login through Google will use the Google canonical OpenID provider URL (http://www.google.com/accounts/o8/id), and users will provide their credentials directly to Google. To make this simpler for the user , websites commonly provide a Sign in with Google button, which hides and invokes the Google OpenID provider. This will redirect the user to to Google, and thereafter once logged into their account, the user is warned that they are signing into another site:

Google Login

When attempting to login with Google for the first time, the user’s unique identifier will be returned, but has not yet been registered with the site, resulting in the error message:

Your login attempt was not successful, try again. Reason: User not found for OpenID: https://www.google.com/accounts/o8/id?id=AItOawm4b0mXXXXXXXXXXXXXXX3bGCztybYUwWQ .

The identifier returned is unique to the user for this applications domain only (i.e. it will be different for the same user for another application with a different domain). Copy this identifier, login using a users username and password, and edit an employee saving the identifier in the employee’s openIdIdentifier field. Finally, logout and try to login again using the same Google account.

Conclusions

Spring Roo is extremely well suited to R&D work, providing a quick means of setting up a project to explore OpenID login using Spring Security. Providing OpenID login may provide users with a useful alternative means of authenticating with your site and is simple to implement.

Further research posibilities

  • OpenID Attribute Exchange (AX) – is an extension for OpenID service which allows the transfer of various user related details/information/attributes between the relying party and the identity provider. Spring Security supports this directly.
  • OpenID user registration (no username and password stored in the application) – currently the application requires that a user already be registered before associating an OpenID with their account. A useful extension would be to enable users to register using their OpenID, and not maintain a username and password on the application.
  • Facebook authentication – Facebook is an OpenID relying party rather than a provider, however Facebook Connect may be used to allow users to use their Facebook identity to login to other sites. See also the Facebook developer documentation
  • OAuth vs OpenID – While OpenID is used by an application to authenticate its users, OAuth, a complimentary open standard to OpenID, may be used by an application to request permission from another application to share a user’s private resources (e.g. photos, videos, contact lists) without the user having to share their username and password (wikipedia).

References

About these ads

13 Responses to “Exploring Google and OpenID login with Spring Security and Spring Roo”

  1. Zimu January 19, 2011 at 2:42 pm #

    Nice article, very informative.

  2. Gordon Dickens February 2, 2011 at 5:14 pm #

    Very nice article & demo.

    FYI, in the code samples the Maven POM wires in Spring Roo 1.1.0, current version is 1.1.1 and Spring Security version should be updated to 3.0.5.

    Regards,
    Gordon Dickens
    twitter.com/gdickens
    technophile.gordondickens.com

    • tfoxcroft February 8, 2011 at 8:46 am #

      Hi Gordon, thanks for the comments and the info on the latest versions. I’ll look into updating this when time allows!

  3. Dan February 8, 2011 at 6:28 am #

    I’m planning on trying your security setup soon on my under construction site. I’ve already adapted your InsertTestData class – very useful. – Thanks

    • tfoxcroft February 8, 2011 at 8:52 am #

      Hi Dan, thanks for the comments, I’m glad you found this method of supplying test data useful. All the best with the site!

  4. yerko February 18, 2011 at 9:26 pm #

    is UserDetails deprecated? my project doesn’t allow me to import this interface…
    help me please!
    thanks in advance

  5. amitnahata August 19, 2011 at 6:03 pm #

    Hey,

    I am planning to use Spring Security Kerberos Extenstion for implement SSO in my project.
    But i am unable to find crysp steps to set up the Kerberos environment on my Unix server.
    Can anyone suggest me the steps or any guide available, which can be used for this.
    I would appreciate the Response.

  6. Dennis Becton November 4, 2011 at 10:20 am #

    Hi Dan,

    Very straightforward article. I’m still struggling with getting Spring Security to use the jdbc-user-service as a authentication-provider. For some reason it seems to be getting ignored, so all I get is a ‘bad credentials’ when I attempt to login. Any suggestions as to how to debug this?

    Thanks!

    • Dennis Becton November 4, 2011 at 10:27 am #

      Sorry – should have provided my applicationContext-security.xml. Think I copy/pasted it correctly from the article.

      • tfoxcroft November 7, 2011 at 7:46 pm #

        Hi Dennis, are you able to post your applicationContext-security.xml? Specifically code relating to your authentication-manager and jdbc-user-service. As a guess, the first thing to check would be the jdbc-user-service queries (users-by-username-query and authorities-by-username-query).

  7. ericg July 18, 2012 at 11:51 pm #

    Thanks for the well written example. I hit a few issues when upgrading to Roo 1.2.1. After Roo did its automatic update I still had to change the following:

    * RooEntity -> RooJpaActiveRecord (on Employee.java)
    * ‘mvc.controller.RooWebScaffold -> mvc.controller.scaffold.RooWebScaffold

    Also, you should log in as user1/password and not admin/admin as stated in the login page.

    Thanks Again,

  8. simbo1905 October 14, 2012 at 8:01 pm #

    Great sample code; I only had to scan the article as the sourcecode and project was nice and crisp. It took me no time at all to lift and shift the security config into an app that I am writing (not Roo, but ZK) and its very exciting to be logging into an app you are debugging in eclipse using openid (in my case provided by clavid.com and using a yubkey so I get two factor authentication; just blows me away that not everyone is doing this yet). Once again thats a lot for a great bit of work!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: