Showing posts with label security. Show all posts
Showing posts with label security. Show all posts

Saturday, December 04, 2010

A PKI based CAPTCHA Implementation

I'm thinking of implementing CAPTCHA verification for an upcoming application, so I decided to check out JCaptcha, which provides libraries for image and sound CAPTCHAs, as well as an implementation you can directly use in your web application. Based on my understanding of the code from its 5-minute tutorial, the way it works is that it generates a random word, sticks it into the session and renders an image of the random word on the browser. When the user types the word back in, it matches the value in the session against the user input.

Most production systems are load balanced across a bank of servers. In such cases, the JCaptcha wiki either advises using either a distributed singleton or sticky sessions. This is because the session is used to hold on to the original generated word.

An alternative approach that eliminates the need for sessions would be to carry an encrypted version of the original word as a hidden field in the CAPTCHA challege form. Various encryption approaches could be used:

  • One-way hash - the original word is encrypted with something like MD5 and put into the hidden field. On the way back, the user's response is also encrypted and the digests compared.

  • Symmetric two-way hash - the original word is encrypted with something like DES and put into the hidden field. On the way back, we can either decrypt the hidden field back and compare to the user input, or encrypt the user input and compare with the hidden field.

  • Asymetric two-way hash - the original word is encrypted with the public part of a RSA keypair (in my case), and on the way back, decrypted with the private part, and compared to the user input.

I guess that any of these approaches would be secure enough to serve as a CAPTCHA implementation, but I hadn't used the Java PKI API before, and I figured that this would be a good time to learn. The code presented here includes a service (that manages the keypair and does the encryption and decryption), a Spring controller (that calls a JCaptcha WordToImage implementation to generate a CAPTCHA image, and methods to show and submit a CAPTCHA challenge form), and a JSPX file that can be used as part of a Spring Roo application.

Service

Asymmetric two-way hashes (around which the PKI API is based) are based on the premise that:

  d(e(m, q), p) == m

  where:
    m = the message body.
    p = private part of a keypair {p,q}.
    q = public part of a keypair {p,q}.
    d = decoding function.
    e = encoding function.

In typical usage, user A sends a message to user B with keypair {p,q} by encrypting it with B's public key q who then decrypts it with his own private key p In our case, there is only keypair, which belongs to the server. On the way out, the generated word is encrypted with the server's public key, and on the way back, the encrypted value in the hidden field is decrypted back with the server's private key.

At startup, each server loads up its keypair from a pair of key files, deployed as part of the web application. That way each server decrypts the encrypted key exactly the same way. Also, since the encrypted version is carried through the request-response cycle, a CAPTCHA generated on any one machine in the cluster can be verified on any other machine.

To generate the word, the service generates a 32 character MD5 checksum of the Date string at the instant the form is called from the browser, then pull a random substring from it between 3 to 5 characters in length. Here's the code for the service:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Source: src/main/java/com/mycompany/ktm/security/CaptchaCryptoService.java
package com.mycompany.ktm.security;

import java.io.File;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;

import javax.crypto.Cipher;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

@Service("captchaCryptoService")
public class CaptchaCryptoService {

  private final Logger logger = Logger.getLogger(getClass());
  
  private static final File PRIV_KEY_FILE = 
    new File("src/main/resources/captcha.pri");
  private static final File PUB_KEY_FILE = 
    new File("src/main/resources/captcha.pub");
  
  private Cipher cipher;
  private PublicKey publicKey;
  private PrivateKey privateKey;
  
  public CaptchaCryptoService() {
    try {
      byte[] pri = FileUtils.readFileToByteArray(PRIV_KEY_FILE);
      PKCS8EncodedKeySpec priSpec = new PKCS8EncodedKeySpec(pri);
      KeyFactory priKeyFactory = KeyFactory.getInstance("RSA");
      this.privateKey = priKeyFactory.generatePrivate(priSpec);
      byte[] pub = FileUtils.readFileToByteArray(PUB_KEY_FILE);
      X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pub);
      KeyFactory pubKeyFactory = KeyFactory.getInstance("RSA");
      this.publicKey = pubKeyFactory.generatePublic(pubSpec);
      this.cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  public String getEncodedCaptchaString() {
    byte[] currentTime = new Date().toString().getBytes();
    String md5 = DigestUtils.md5DigestAsHex(currentTime);
    int start = random(0, 32, 100);
    int length = random(3, 5, 10);
    String plainText = slice(md5, start, length);
    return encrypt(plainText);
  }

  public String getDecodedCaptchaString(String encoded) {
    String decoded = decrypt(encoded);
    return decoded;
  }
  
  public boolean validate(String encoded, String response) {
    return response.equalsIgnoreCase(getDecodedCaptchaString(encoded));
  }

  private boolean keyFilesExist() {
    return PRIV_KEY_FILE.exists() && PUB_KEY_FILE.exists();
  }

  private String encrypt(String plainText) {
    try {
      cipher.init(Cipher.ENCRYPT_MODE, this.publicKey);
      byte[] encrypted = cipher.doFinal(plainText.getBytes());
      Hex hex = new Hex();
      return new String(hex.encode(encrypted));
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private String decrypt(String encodedText) {
    try {
      cipher.init(Cipher.DECRYPT_MODE, this.privateKey);
      Hex hex = new Hex();
      byte[] encrypted = hex.decode(encodedText.getBytes());
      byte[] decrypted = cipher.doFinal(encrypted);
      return new String(decrypted);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  private int random(int min, int max, int multiplier) {
    while (true) {
      double rand = Math.round(Math.random() * multiplier);
      if (rand >= min && rand <= max) {
        return (int) rand;
      }
    }
  }

  private String slice(String str, int start, int length) {
    StringBuilder buf = new StringBuilder();
    int pos = start;
    for (int i = 0; i < length; i++) {
      if (pos == str.length()) {
        pos = 0;
      }
      buf.append(str.charAt(pos));
      pos++;
    }
    return buf.toString();
  }
}

To generate the key pair, I just used the following Java code, although you can also use ssh-keygen or something similar to generate them from the command line.

1
2
3
4
5
6
7
8
9
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(1024);
    KeyPair keyPair = keyPairGenerator.generateKeyPair();
    FileUtils.writeByteArrayToFile(
      PRIV_KEY_FILE, keyPair.getPrivate().getEncoded());
    FileUtils.writeByteArrayToFile(
      PUB_KEY_FILE, keyPair.getPublic().getEncoded());
    this.privateKey = keyPair.getPrivate();
    this.publicKey = keyPair.getPublic();

Controller

I want to add the CAPTCHA into a Spring Roo application (which does not exist at the moment), so I just tried to add it into an existing app to test out the code. The controller code consists of three methods:

  1. Load the CAPTCHA form - in real-life, this would be a create/edit form for a domain object with the CAPTCHA stuff added to it. For testing, all it does is show the image and offer a single text box.
  2. Generate the CAPTCHA image - Generates the image using JCaptcha's WordToImage implementation and writes to as a PNG file.
  3. Handle the CAPTCHA form submit - as before, this would be part of another form in real-life. This takes the encrypted string and the user input and validates it against each other.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
// Source: src/main/java/com/mycompany/ktm/web/CaptchaController.java
package com.mycompany.ktm.web;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.net.BindException;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import com.mycompany.ktm.fbo.CaptchaForm;
import com.mycompany.ktm.security.CaptchaCryptoService;
import com.octo.captcha.component.image.backgroundgenerator.EllipseBackgroundGenerator;
import com.octo.captcha.component.image.fontgenerator.TwistedRandomFontGenerator;
import com.octo.captcha.component.image.textpaster.SimpleTextPaster;
import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage;
import com.octo.captcha.component.image.wordtoimage.WordToImage;

@RequestMapping(value="/captcha/**")
@Controller
public class CaptchaController {

  private final Logger logger = Logger.getLogger(getClass());
  
  @Autowired private CaptchaCryptoService captchaCryptoService;

  private WordToImage imageGenerator;
  
  @ModelAttribute("captchaForm")
  public CaptchaForm formBackingObject() {
    return new CaptchaForm();
  }
  
  @PostConstruct
  protected void init() throws Exception {
    this.imageGenerator = new ComposedWordToImage(
      new TwistedRandomFontGenerator(40, 50),
      new EllipseBackgroundGenerator(150, 75),
      new SimpleTextPaster(3, 5, Color.YELLOW)
    );
  }
  
  @RequestMapping(value = "/captcha/form", method = RequestMethod.GET)
  public ModelAndView form(
      @RequestParam(value="ec", required=false) String ec,
      @ModelAttribute("captchaForm") CaptchaForm form,
      BindingResult result) {
    ModelAndView mav = new ModelAndView();
    if (StringUtils.isEmpty(ec)) {
      form.setEc(captchaCryptoService.getEncodedCaptchaString());
    } else {
      result.addError(new ObjectError("dc", "Bzzt! You missed! Try again!"));
      form.setEc(ec);
    }
    mav.addObject("form", form);
    mav.addObject("ec", form.getEc());
    mav.setViewName("captcha/create");
    return mav;
  }

  @RequestMapping(value="/captcha/image", method=RequestMethod.GET)
  public ModelAndView image(HttpServletRequest req, HttpServletResponse res)
      throws Exception {
    String encoded = ServletRequestUtils.getRequiredStringParameter(req, "ec");
    String decoded = captchaCryptoService.getDecodedCaptchaString(encoded);
    BufferedImage image = imageGenerator.getImage(decoded);
    res.setHeader("Cache-Control", "no-store");
    res.setHeader("Pragma", "no-cache");
    res.setDateHeader("Expires", 0);
    res.setContentType("image/png");
    ServletOutputStream ostream = res.getOutputStream();
    ImageIO.write(image, "png", ostream);
    ostream.flush();
    ostream.close();
    return null;
  }

  @RequestMapping(value = "/captcha/validate", method = RequestMethod.POST)
  public ModelAndView validate(
      @ModelAttribute("captchaForm") CaptchaForm form,
      BindingResult result) {
    ModelAndView mav = new ModelAndView();
    String ec = form.getEc();
    String dc = form.getDc();
    if (captchaCryptoService.validate(ec, dc)) {
      mav.setViewName("redirect:/captcha/form");
    } else {
      mav.setViewName("redirect:/captcha/form?ec=" + ec);
    }
    return mav;
  }
}

The form backing object is a plain POJO with only two fields. If we are using a domain object as our form backing object, then I think we can just add these two fields as @Transient, although I haven't tried it. Theres not much to this POJO, I show it below, with getters and setters omitted.

1
2
3
4
5
6
7
8
9
// Source: src/main/java/com/mycompany/ktm/fbo/CaptchaForm.java
package com.mycompany.ktm.fbo;

public class CaptchaForm {

  private String ec; // encrypted string
  private String dc; // plaintext string
  ...  
}

And finally, the JSPX file. This is a copy of one of the Roo generated JSPX files, with the image generated using an <img> tag, a single text field to get the user input, and a submit button.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<div xmlns:c="http://java.sun.com/jsp/jstl/core" 
     xmlns:form="http://www.springframework.org/tags/form"
     xmlns:jsp="http://java.sun.com/JSP/Page"
     xmlns:spring="http://www.springframework.org/tags" version="2.0">
  <jsp:output omit-xml-declaration="yes"/>
  <script type="text/javascript">
    dojo.require('dijit.TitlePane');
  </script>
  <div id="_title_div">
    <spring:message var="title_msg" text="Captcha Challenge"/>
    <script type="text/javascript">
      Spring.addDecoration(new Spring.ElementDecoration({
        elementId : '_title_div', 
        widgetType : 'dijit.TitlePane', 
        widgetAttrs : {title: '${title_msg}'}
      })); 
    </script>
    <form:form action="/ktm/captcha/validate" method="POST" 
        modelAttribute="captchaForm">
      <form:errors cssClass="errors" delimiter="&lt;p/&gt;"/>
      <div id="roo_captcha_image">
        <img src="/ktm/captcha/image?ec=${ec}"/>
      </div>
      <div id="roo_captcha_dc">
        <label for="_captcha_dc">
          Enter the characters you see in the image above:
        </label>
        <form:input cssStyle="width:250px" id="_captcha_dc" 
          maxlength="5" path="dc" size="0"/>
        <br/>
        <form:errors cssClass="errors" id="_dc_error_id" path="dc"/>
      </div>
      <div class="submit" id="roo_captcha_submit">
        <spring:message code="button.save" var="save_button"/>
        <script type="text/javascript">
          Spring.addDecoration(new Spring.ValidateAllDecoration({
            elementId:'proceed', 
            event:'onclick'
          }));
        </script>
        <input id="proceed" type="submit" value="${save_button}"/>
      </div>
      <form:hidden id="_roo_captcha_ec" path="ec"/>
    </form:form>
  </div>
</div>

I also had to create a new views.xml for this controller, similar to other Roo custom controllers. It only has a single entry, here it is:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE tiles-definitions 
  PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN" 
  "http://tiles.apache.org/dtds/tiles-config_2_1.dtd">
<tiles-definitions>
  <definition extends="default" name="captcha/create">
    <put-attribute name="body" value="/WEB-INF/views/captcha/create.jspx"/>
  </definition>
</tiles-definitions>

Putting it all together

As I said before, a "real" use-case of this would be to embed it into an edit form for a domain object - here, I am only interested in testing the functionality, so its being used standalone. Here are some screenshots demonstrating the flow:

Entering /captcha/form in my browser would take me to the challenge form with a CAPTCHA challenge image generated:

If I enter an incorrect value, it shows me an error message and regenerates the same string into a different CAPTCHA image, hopefully more readable this time. This is not the standard behavior for most CAPTCHA's I've used, they generally give you a new image of a different string in these cases. Notice that the URL now has the ec parameter which contains the encrypted string.

If I enter a correct value, it is silently accepted and the form is regenerated with a new CAPTCHA challenge.

References

Apart from the references above, I found the following links very helpful. All the APIs here (except Spring and Roo) are new to me, so I had to do a bit of reading before everything came together.

  • Bouncy Castle Crypto package - contains lots of code examples to get you started quickly with this encryption stuff. My CaptchaCryptoService is based heavily on the PublicExample.java here.

  • Using Public Key encryption in Java - contains general information about PKI and Java. I found out how to store keypairs into files and load them from my service.
  • JCaptcha and Spring Framework - contains a discussion on how to configure JCaptcha beans with Spring. I ended up building my WordToImage implementation in the Controller's @PostConstruct method, but the information in here was invaluable in figuring out whats available.

Thursday, July 22, 2010

KTM - Customizing Roo Security

KTM is a read-write app, ie, its functionality is driven by data that is entered into it. So I needed some way to restrict different classes of user to different areas of the app. Specifically, I would like one class of user (administrator) to control the Person entity, another class (managers) to control the Client, Project, Item and Allocations entities, and yet another class (developers) to control the Task and Hours entities. By control, I mean the ability to create, update or delete an entity - read-only operations, such as show, list, find, etc, are unrestricted.

Generate default Security Setup

I started off by letting Roo generate the default security setup, by issuing the following command from the Roo shell.

1
security setup

This creates and modifies a bunch of files in the app, but the one of interest to me was the applicationContext-security.xml file, which is shown below. I have edited it slightly to make it more readable. I also removed the password hashes in the password attribute for the user tag and replaced it with ellipsis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <!-- HTTP security configurations -->
    <http auto-config="true" use-expressions="true">
      <form-login login-processing-url="/static/j_spring_security_check"
           login-page="/login" 
           authentication-failure-url="/login?login_error=t"/>
        <logout logout-url="/static/j_spring_security_logout"/>
        
        <!-- Configure these elements to secure URIs in your application -->
        <intercept-url pattern="/choice/**" access="hasRole('ROLE_ADMIN')"/>
        <intercept-url pattern="/member/**" access="isAuthenticated()" />
        <intercept-url pattern="/resources/**" access="permitAll" />
        <intercept-url pattern="/static/**" access="permitAll" />
        <intercept-url pattern="/**" access="permitAll" />
    </http>

    <!-- Configure Authentication mechanism -->
    <authentication-manager alias="authenticationManager">
      <!-- SHA-256 values can be produced using 'echo -n
             your_desired_password | sha256sum' (using normal 
             *nix environments) -->
      <authentication-provider>
        <password-encoder hash="sha-256"/>
        <user-service>
          <user name="admin" password="..." authorities="ROLE_ADMIN"/>
          <user name="user" password="..." authorities="ROLE_USER"/>
        </user-service>
      </authentication-provider>
    </authentication-manager>
</beans:beans>

As you can see, its completely generic, it has no references to the application generated so far. My understanding is that this is meant more as a template that you have to customize for your app.

Modifying Person

The Roo generated security configuration presented above uses an in-memory authentication provider that is configured using the user-service element. Since my app maintains a set of Person entities, I figured I could build a custom provider that used Person data. To do that though, I needed to add a password field to Person. I do that from the Roo shell.

I also wanted to use the Person's email address as his username. This has the advantage of being unique and is easy to type (compared to the Person.name which contains the full name). So I would also need to be able to look a Person up by his email address. Once again, I use the Roo shell to generate the finder.

1
2
field string --fieldName password --class com.healthline.ktm.domain.Person
finder add --finderName findPeopleByEmailAddress

This would save the password in plain-text in the database, which is probably not desirable. Unfortunately there is no --password switch for the Roo field command. So I added a method to encrypt the password in the Person bean to a 32-character MD5 Hash and annotate it so it is called before an INSERT or UPDATE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Person {
  ...
  @PrePersist
  @PreUpdate
  protected void encryptPassword() {
    if (password != null && (! password.matches("^[0-9a-fA-F]+$"))) {
      // prevent encryption if already encrypted
      password = DigestUtils.md5DigestAsHex(password.getBytes());
    }
  }
}

The Roo generated JSPs for Person will also need to be modified. Before I do that though, I want to make sure that our modifications don't get overwritten the next time I do something with the entity, so I set automaticallyMaintainView=false in the @RooWebScaffold annotation for PersonController.

For the create.jspx and update.jspx, the changes involve replacing the <form:input> tag for the password field with <form:password> tag. By default, the form:password tag will initialize the field, so for the update.jspx, I added another attribute showPassword="true" so it doesn't.

I left the generated password in for the list.jspx and show.jspx, since its encrypted on its way in anyway, and what shows up is a 32-character hex string which wouldn't mean much to most people, and because it could be helpful for debugging if you are the developer.

Custom Authentication Provider

My custom authentication provider extends AbstractUserDetailsAuthenticationProvider, which works with username/password kind of setups. Since KTM deals with projects, the administrator's details are not recorded in the Person table. So the provider declares a administrator pseudo-user, the username and password for which is injected into the provider via Spring configuration.

The provider calls its retrieveUser() method to authenticate the user using the email address as username and entered password for password, encrypting the password to match the one it looks up using the Person.findPeopleByEmailAddress() from the database. It then uses the WorkRole enum value in the Person entity to figure out the authorizations. For WorkRole.Manager, the GrantedAuthority is ROLE_MANAGER, for WorkRole.Developer it is ROLE_DEVELOPER, and for WorkRole.Combined, it is ROLE_MANAGER and ROLE_DEVELOPER. For the admin user, the GrantedAuthority is ROLE_ADMIN. The method returns a populated UserDetails object if the login succeeded, or throws a BadCredentialsException with the appropriate message if not.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package com.healthline.ktm.security;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityNotFoundException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import com.healthline.ktm.domain.Person;
import com.healthline.ktm.domain.WorkRoles;

@Service("ktmAuthenticationProvider")
public class KtmAuthenticationProvider extends 
    AbstractUserDetailsAuthenticationProvider {

  private final Logger logger = Logger.getLogger(getClass());

  private String adminUser;
  private String adminPassword;
  
  @Required
  public void setAdminUser(String adminUser) {
    this.adminUser = adminUser;
  }
  
  @Required
  public void setAdminPassword(String adminPassword) {
    this.adminPassword = adminPassword;
  }
  
  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    return;
  }

  @Override
  protected UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    String password = (String) authentication.getCredentials();
    if (! StringUtils.hasText(password)) {
      throw new BadCredentialsException("Please enter password");
    }
    String encryptedPassword = DigestUtils.md5DigestAsHex(password.getBytes()); 
    UserDetails user = null;
    String expectedPassword = null;
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    if (adminUser.equals(username)) {
      // pseudo-user admin (ie not configured via Person)
      expectedPassword = DigestUtils.md5DigestAsHex(adminPassword.getBytes()); 
      // authenticate admin
      if (! encryptedPassword.equals(expectedPassword)) {
        throw new BadCredentialsException("Invalid password");
      }
      // authorize admin
      authorities.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
    } else {
      try {
        Query query = Person.findPeopleByEmailAddress(username);
        Person person = (Person) query.getSingleResult();
        // authenticate the person
        expectedPassword = person.getPassword();
        if (! StringUtils.hasText(expectedPassword)) {
          throw new BadCredentialsException("No password for " + username + 
            " set in database, contact administrator");
        }
        if (! encryptedPassword.equals(expectedPassword)) {
          throw new BadCredentialsException("Invalid Password");
        }
        // authorize the person
        WorkRoles role = person.getWorkRole();
        switch (role) {
          case Manager:
            authorities.add(new GrantedAuthorityImpl("ROLE_MANAGER"));
            break;
          case Combined:
            authorities.add(new GrantedAuthorityImpl("ROLE_MANAGER"));
            authorities.add(new GrantedAuthorityImpl("ROLE_DEVELOPER"));
            break;
          case Developer:
            authorities.add(new GrantedAuthorityImpl("ROLE_DEVELOPER"));
            break;
          default:
            // should never happen since Person will have one of
            // the above WorkRoles defined, but just in case we
            // decide to add a new role in the future...
            throw new BadCredentialsException("User:[" + username + 
              "] has unknown role: " + role);
        }
      } catch (EntityNotFoundException e) {
        throw new BadCredentialsException("Invalid user");
      } catch (NonUniqueResultException e) {
        throw new BadCredentialsException(
          "Non-unique user, contact administrator");
      }
    }
    return new User(
      username,
      password,
      true, // enabled 
      true, // account not expired
      true, // credentials not expired 
      true, // account not locked
      authorities
    );
  }
}

In the applicationContext-security.xml file, the KtmAuthenticationProvider bean replaces the default in-memory authentication provider generated by Roo. Here is what the block under the "Configure Authentication Mechanism" looks like with my custom authentication provider.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  <!-- Configure Authentication mechanism -->
  <beans:bean id="ktmAuthenticationProvider" 
      class="com.healthline.ktm.security.KtmAuthenticationProvider">
    <beans:property name="adminUser" value="admin"/>
    <beans:property name="adminPassword" value="admin"/>
  </beans:bean>
  
  <authentication-manager alias="authenticationManager">
    <authentication-provider ref="ktmAuthenticationProvider"/>
  </authentication-manager>

Customizing Intercept URL Patterns

Now that our authentication provider returns a UserDetail with a List of GrantedAuthority objects that correspond to our app, we can update the block titled "HTTP Security configurations" in the applicationContext-security.xml. This snippet from my updated applicationContext-security.xml file is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  <!-- HTTP security configurations -->
  <http auto-config="true" use-expressions="true" 
      access-denied-page="/app/accessDenied">
    <form-login login-processing-url="/static/j_spring_security_check" 
        login-page="/login" 
        authentication-failure-url="/login?login_error=t"/>
    <logout logout-url="/static/j_spring_security_logout"/>
        
    <!-- ROLE_ADMIN has create/edit/delete on Person -->
        
    <intercept-url pattern="/person/form" access="hasRole('ROLE_ADMIN')"/>
    <intercept-url pattern="/person/\\d+/form" access="hasRole('ROLE_ADMIN')"/>
    <intercept-url pattern="/person/**" method="DELETE" 
      access="hasRole('ROLE_ADMIN')"/>
        
    <!-- ROLE_MANAGER has create/edit/delete on Client, Project, -->
    <!-- Item, Allocations                                       -->
      
    <intercept-url pattern="/client/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/client/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/client/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/project/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/project/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/project/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/item/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/item/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/item/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/allocations/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/allocations/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/allocations/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <!-- ROLE_DEVELOPER has create/edit/delete on Task, Hours -->
    
    <intercept-url pattern="/task/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/task/\\d+/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/task/**" method="DELETE" 
      access="hasRole('ROLE_DEVELOPER')"/>
        
    <intercept-url pattern="/hours/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/hours/\\d+/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/hours/**" method="DELETE" 
      access="hasRole('ROLE_DEVELOPER')"/>

    <!-- Everything else can be accessed by anybody, logged in or not -->
    <intercept-url pattern="/resources/**" access="permitAll" />
    <intercept-url pattern="/static/**" access="permitAll" />
    <intercept-url pattern="/**" access="permitAll" />
  </http>

The inline comments are pretty-self explanatory - the XML above basically codifies the rules outlined in the first paragraph in this post. Since Roo generates a REST-ful app, the deletes are handled using a HTTP delete method, which we need to handle using the method attribute in the intercept-url elements above as explained here.

At this point, if someone clicks "Create new Person" from the main page, they will be presented with a login screen. Once they login with admin/admin (as configured in the KtmAuthenticationProvider Spring config), they would then see the Create new Person screen. The same workflow would exist for the other "Create" links - based on the authorization, they would either be sent to the form page or to an "Access Denied" page (more on this in a bit).

Turning off Unauthorized Functionality

The interception style of authenticating is good for external facing web applications (such as websites), since most of the content is for public consumption, and you authenticate either when a user decides to participate, or when you are exposing personal content. For intranet/tool applications such as KTM, I prefer to start the user with a login page, and expose only the functionality which they are authorized for when they do login.

Spring Security (or Acegi) has some JSP tags which allow you to do this fairly easily. If I wanted to go the "put everything behind a login screen" route, I would need to wrap the protected portions (of menu.jspx) in <sec:authorize> tags and replace index.jspx with login.jspx (I think, haven't tried it). But I was too lazy to do this - since this would be an Intranet app, doing it one way or the other is merely a matter of user training.

I did, however, want to hide the update and delete icons from the "List all XXX" pages from unauthorized users. So I basically had to do wrap the update and delete icon columns in the table representing the listing, and to bind the sec: namespace to the URI for the Spring security TLD. The basic pattern is:

1
2
3
  <sec:authorize access="hasRole('ROLE_ADMIN')">
    // stuff you want to show only to admin goes here
  </sec:authorize>

Once this was done, the person listing page will look different based on whether you are logged in as "admin" (the one on the left below), or if you are either not logged in, or logged in with some other role (the one on the right below).

Static Access Denied Page

One of the problems with the intercept pattern is multiple roles. For example, if a user clicks on "Create a Person", and is presented with a login screen, it is not immediately obvious what he should login as. For example, if he logs in as himself, and he has ROLE_MANAGER, then what happens is that he falls through to a 403 Access Denied page served by the container (in my case Jetty).

So I decided to build an accessDenied.jspx page along the same lines as resourceNotFound.jspx - ie, a static page with an appropriate message and a short description of the different roles and what functions they can perform. It looks like this:

To build this page, I copied resourceNotFound.jspx into accessDenied.jspx and changed the title and problemdescription label names, then mimicked its configuration by doing a "find | xargs grep". Here are the locations I had to modify.

  • messages*.properties - created the contents of the accessdenied.title and accessdenied.problemdescription labels in 5 different languages and add them as properties in these files.
  • webmvc-config.xml - add in a line <mvc:view-controller path="/aceessDenied.jspx"/>, similar to the one for resourceNotFound. This creates a "static" Spring controller.
  • web.xml - add an <error-page> entry for HTTP error code 403 for /app/accessDenied.
  • views.xml (top level) - In the top level views.xml tiles definition file (under WEB-INF/views), I created an entry pointing to the actual JSPX file.
  • applicationContext-security.xml - I added the access-denied-page attribute to the http element in our applicationContext-security.xml pointing to /app/accessDenied. This is already in the snippet shown above.

Manual Change Password Controller

With the setup described so far, the administrator is the only one who can create or update a user's password. This is a bit inflexible in most real-world scenarios - ideally, the user should be able to change his password to something else. I built a simple manual Roo controller for this. Skeleton code is generated using the Roo shell:

1
controller class --class com.healthline.ktm.web.ChangePasswordController

The generated controller is similar to the SimpleFormController of the pre-annotation Spring days. I don't know about you, but to me, the SimpleFormController, while powerful, was anything but simple, and my work did not involve that much form handling anyway, so I never used it. In any case, based on the advice from here and here, I managed to figure out how to work with it, although the code I ended up with exposed different, but equivalent, method signatures compared to the one generated.

First, the controller. As you can see, it exposes a named bean via the @ModelAttribute annotation. When the "Change Password" link is clicked, the index() method is called. This sends an empty model attribute bean (the form bean) to the JSP which displays the form. When the user fills out the form and clicks submit, the update() method is called, which validates the data using the @Autowired validator instance. If everything is good, it sets the encrypted password into the Person instance and updates it, and forwards to the "thanks" view, powered by the thanks() method. If not, the form is redisplayed with the appropriate error message(s).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.healthline.ktm.web;

import java.util.List;

import javax.persistence.Query;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.healthline.ktm.domain.Person;
import com.healthline.ktm.fbo.ChangePasswordForm;
import com.healthline.ktm.fbo.ChangePasswordValidator;

@RequestMapping("/changepassword/**")
@Controller
public class ChangePasswordController {

  private final Logger logger = Logger.getLogger(getClass());

  @Autowired private ChangePasswordValidator validator;
  
  @ModelAttribute("changePasswordForm")
  public ChangePasswordForm formBackingObject() {
    return new ChangePasswordForm();
  }

  @RequestMapping(value="/changepassword/index")
  public String index() {
    return "changepassword/index";
  }

  @RequestMapping(value="/changepassword/update", method=RequestMethod.POST)
  public String update(
      @ModelAttribute("changePasswordForm") ChangePasswordForm form, 
      BindingResult result) {
    validator.validate(form, result);
    if (result.hasErrors()) {
      return "changepassword/index"; // back to form
    } else {
      String newPassword = form.getNewPassword();
      Query query = Person.findPeopleByEmailAddress(form.getEmailAddress());
      Person person = (Person) query.getSingleResult();
      person.setPassword(newPassword);
      person.merge();
      return "changepassword/thanks";
    }
  }
  
  @RequestMapping(value="/changepassword/thanks")
  public String thanks() {
    return "changepassword/thanks";
  }
}

The form bean exposed through the @ModelAttribute annotation is a POJO that exposes getters and setters for the form fields. Like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.healthline.ktm.fbo;

public class ChangePasswordForm {

  private String emailAddress;
  private String oldPassword;
  private String newPassword;
  private String newPasswordAgain;

  // getters and setters omitted, use your IDE to fill them out  
}

The controller also autowires in a custom validator to validate the fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.healthline.ktm.fbo;

import javax.persistence.EntityNotFoundException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import com.healthline.ktm.domain.Person;

@Service("changePasswordValidator")
public class ChangePasswordValidator implements Validator {

  @Override
  public boolean supports(Class<?> clazz) {
    return ChangePasswordForm.class.equals(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ChangePasswordForm form = (ChangePasswordForm) target;
    String emailAddress = form.getEmailAddress();
    try {
      Query query = Person.findPeopleByEmailAddress(emailAddress);
      Person person = (Person) query.getSingleResult();
      String storedPassword = person.getPassword();
      String currentPassword = DigestUtils.md5DigestAsHex(
        form.getOldPassword().getBytes());
      if (! currentPassword.equals(storedPassword)) {
        errors.rejectValue("oldPassword", "changepassword.invalidpassword");
      }
      String newPassword = form.getNewPassword();
      String newPasswordAgain = form.getNewPasswordAgain();
      if (! newPassword.equals(newPasswordAgain)) {
        errors.reject("changepassword.passwordsnomatch");
      }
    } catch (EntityNotFoundException e) {
      errors.rejectValue("emailAddress", "changepassword.invalidemailaddress");
    } catch (NonUniqueResultException e) {
      errors.rejectValue("emailAddress", 
        "changepassword.duplicateemailaddress");
    }
  }
}

On the JSP side, I modified the generated index.jspx to look like this, using snippets from the various other generated JSPX files to make the end result look similar to the other forms.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<div xmlns:c="http://java.sun.com/jsp/jstl/core" 
    xmlns:form="http://www.springframework.org/tags/form" 
    xmlns:jsp="http://java.sun.com/JSP/Page" 
    xmlns:spring="http://www.springframework.org/tags" version="2.0">
  <jsp:output omit-xml-declaration="yes"/>
  <script type="text/javascript">dojo.require('dijit.TitlePane');dojo.require('dijit.form.SimpleTextarea');dojo.require('dijit.form.FilteringSelect');</script>
  <div id="_title_div">
    <spring:message code="label.changepassword" var="title_msg"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:url value="/changepassword" var="form_url"/>
    <spring:message var="title" code="label.changepassword"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <form:form action="/ktm/changepassword/update" method="POST" commandName="changePasswordForm">
      <div id="changepassword_emailaddress">
        <label for="_emailaddress_id">Email Address:</label>
        <form:input cssStyle="width:250px" id="_changepassword_emailaddress" maxlength="30" path="emailAddress"/>
        <br/>
        <form:errors cssClass="errors" path="emailAddress"/>
      </div>
      <br/>
      <div id="changepassword_oldpassword">
        <label for="_oldpassword_id">Current Password:</label>
        <form:password cssStyle="width:250px" id="_changepassword_oldpassword" maxlength="30" path="oldPassword"/>
        <br/>
        <form:errors cssClass="errors" path="oldPassword"/>
      </div>
      <br/>
      <div id="changepassword_newpassword">
        <label for="_newpassword_id">New Password:</label>
        <form:password cssStyle="width:250px" id="_changepassword_newpassword" maxlength="30" path="newPassword"/>
        <br/>
        <form:errors cssClass="errors" path="newPassword"/>
      </div>
      <br/>
      <div id="changepassword_newpasswordagain">
        <label for="_newpasswordagain_id">New Password (again):</label>
        <form:password cssStyle="width:250px" id="_changepassword_newpasswordagain" maxlength="30" path="newPasswordAgain"/>
        <br/>
        <form:errors cssClass="errors" path="newPasswordAgain"/>
      </div>
      <br/><br/>
      <div class="submit" id="changepassword_submit">
        <spring:message code="button.save" var="save_button"/>
        <script type="text/javascript">Spring.addDecoration(new Spring.ValidateAllDecoration({elementId: 'proceed', event : 'onclick'}));</script>
        <input id="proceed" type="submit" value="${save_button}"/>
      </div>
      <br/>
      <form:errors cssClass="errors" delimiter="&lt;p/&gt;"/>
    </form:form>
  </div>
</div>

The thanks.jspx is even simpler. Here it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<div xmlns:c="http://java.sun.com/jsp/jstl/core" 
    xmlns:form="http://www.springframework.org/tags/form" 
    xmlns:jsp="http://java.sun.com/JSP/Page" 
    xmlns:spring="http://www.springframework.org/tags" version="2.0">
  <jsp:output omit-xml-declaration="yes"/>
  <script type="text/javascript">dojo.require('dijit.TitlePane');dojo.require('dijit.form.SimpleTextarea');dojo.require('dijit.form.FilteringSelect');</script>
  <div id="_title_div">
    <spring:message code="label.changepassword" var="title_msg"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:url value="/changepassword" var="form_url"/>
    <spring:message var="title" code="label.changepassword"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:message code="changepassword.thankyoumessage" var="thankyou_message"/>
    <h3>${thankyou_message}</h3>
  </div>
</div>

As with the accessDenied.jspx, I had to add a bunch of labels for values of keys that are referred to from the JSPX files and the Validator. Here they are (for messages.properties).

1
2
3
4
5
6
7
#changepassword
changepassword.invalidpassword=Invalid Current Password
changepassword.passwordsnomatch=Passwords do not match
changepassword.invalidemailaddress=Invalid Email Address
changepassword.duplicateemailaddress=\
Duplicate Email Address, contact administrator
changepassword.thankyoumessage=Your password has been changed.

I also had to register the thanks.jspx into the views.xml tiles definition file for changepassword, similar to the index.jspx that was already set in there by Roo.

Once all this was set up, clicking the Change Password link from the left nav led to the form shown on the left below. On hitting submit after entering my email address, old and new password, I get the confirmation message shown on the right below.

Conclusion

I still haven't gotten to the "interesting" part of my application :-). But the process of customizing the standard Roo authentication template for my own purposes has taught me a great deal. I found it fairly easy to do the customization, even though a lot of time was spent trying to find all the places to update. But that is a one time learning effort, the process should go much quicker the next time.

Building the Change Password functionality has also taught me about the mechanics of building a manual Roo controller, and the places to add and update, so hopefully I will be able to concentrate on application logic for the next (and final) part of this project.