본문 바로가기

보안/시큐어코딩

OWASP ESAPI Authenticator tutorial

원본: http://technicalmumbojumbo.wordpress.com/2013/05/22/owasp-esapi-authenticator-tutorial/

OWASP ESAPI Authenticator tutorial

Shortlink: http://wp.me/p5Jvc-bL

The internet is slowly but steadily becoming all pervasive in our lives. From rudimentary surfing for information, it has become a repository for storing private information and conversations. We regularly use it to do financial transactions and also share personally identifiable information. Availability of such information in the public domain is harmful and prone to misuse. Therefore it is necessary that we use suitable techniques to protect our information shared on the internet.

The Open Web Application Security Project (OWASP) is an organization focused on improving the security of software. Their aim is to make software security visible so that we can make informed decisions around application security. To assist developers in their endeavor to implement secure applications, OWASP provides the ESAPI (The OWASP Enterprise Security API) a free, open source web application security control library.

 

The ESAPI library PHP, .NET, Pthon, Java 언어를 지원한다. 

ESAPI 라이브러리를 사용하기 위해서는 OWASP에서 제공하는 esapi-2.0.1.jar 파일을 다운로드 받는다.

 download link.

 

ESAPI는 애플리케이션 보안에 필요한 다양한 종류의 인터페이스를 제공하고 있다.

  • Authentication (Authenticator)
  • Role based access control (AccessController)
  • HTTP specific handler (HTTPUtilities)
  • HTML/XML encoding (Encoder)
  • Data encryption (Encryptor)
  • OS commands protection (Executor)
  • Detect security acts (IntrusionDetector)
  • Crytographically random numbers/strings (Randomizer)
  • Application data validation (Validator)

 

이 포스트는 ESAPI의 Authenticator 클래스를 이용하여 웹 애플리케이션에서 안전하게 사용자를 관리하는 방법을 명하고 있다.

 

샘플 코드를 테스트하기위해 HTTP 요청이 필요한데 이 요청을 가짜로 만들어 줄수 있는 Mockito 프레임워크 1.9.5 버전을 다운로드 받아서 사용할 수 있다. 다운로드는 라이브러리 다운로드는 here  소드 다운로드는 source. 링크를 이용한다.

 

이클립스와 JDK7 버전이 필요하다.

http://www.eclipse.org   http://java.sun.com 에서 다운로드 받아서 설치한다.

 

Authenticator ESAPI 라이브러리를 사용하기 위해서는 ESAPI 구성파일을 먼저 설정해야 한다.

 

ESAPI.properties을 소스코드 폴더에 저장한다. 그리고 인증 모듈을 사용하기 위해 인증에 사용될 username, password의 파라미터 이름을 등록한다. 

 

1
2
3
4
5
6
Authenticator.UsernameParameterName=userName
Authenticator.PasswordParameterName=password
 
ESAPI.Authenticator=com.esapi.authenticator.CustomAuthenticator
Authenticator.IdleTimeoutDuration=100000
Authenticator.AbsoluteTimeoutDuration=100000

 

 

How each of these property definition is used will be explained in the subsequent walk thru. The org.owasp.esapi.Authenticator interface defines methods for creating/handling user credentials. To provide consistency and reuse ESAPI also provides an abstract implementation of the Authenticator interface AbstractAuthenticator. The AbstractAuthenticator class provides standard implementation for non-user specific methods. The ESAPI framework also provides a standard file based implementation via the FileBasedAuthenticator class.

To implement your own custom based Authenticator implementation extend the abstract class AbstractAuthenticator. Let’s call the custom implementation class CustomAuthenticator. To inform ESAPI to use this custom authenticator, we assign the fully qualified class name of CustomAuthenticator as value to the key ESAPI.Authenticator in the ESAPI.properties file.

The source code of the CustomAuthenticator class is shown below:

 

 

The CustomAuthenticator class leverages the default behavior of the AbstractAuthenticator class and overrides only the User specific methods. Let’s understand the custom behavior. The CustomAuthenticator class is implemented as a singleton class. The instance object of the class is stored as a private static variable in the CustomAuthenticator class. This instance is made accessible to consumer entities via the getInstance static method. All custom or default implementations of the various ESAPI facets (Authenticator, Validator etc) are available via ESAPI class’s static methods. These methods internally use the org.owasp.esapi.util.ObjFactory’s make method to create appropriate interface implementations. The method looks for two things within the interface implementation classes. First if there exists a getInstance method and second if there exists a constructor with no arguments. In that order it triggers the method/constructor invocation using reflection to return back an instance of the interface implementation to the invoking entity.

Next we have created ‘userCredentials’ HashMap. This Map contains username and password as a key value pair. For our custom implementation the userCredentials map is the authentication information repository. In a more ‘real life’ scenario the map can be replaced by a connection like object to a suitable repository like LDAP, file, XML, database etc. For our demonstration I have filled this map with a single userid ‘jsmith’ and his security credential. Before moving on to overridden methods, just wanted to cover something. ESAPI provides an interface User which abstracts User related information. Developers are expected to create custom implementation of the User interface or use the DefaultUser implementation made available by the framework. Instead of reinventing the wheel I propose to use the default implementation.

 

 

패스워드 검증 작업을 수행할 함수를 오버라이딩 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean verifyPassword(User user, String password) {
    String userid = user.getAccountName();
    String value = userCredentials.get(userid);
    if (userid != null && value != null){
        if (password.equals(value)) {
            return true;
        } else {
            return false;
        }
    }
    return false;
}

 

The method accepts two arguments the User instance and password string. The userid/account name is retrieved from the User object. A lookup is done in the userCredentials Map and corresponding password value is retrieved. In case the retrieved value matches the password received as argument, the method returns true.

Next we look at three methods createUser, loadUser and checkPassword method. The method createUser is an overridden implementation, while the checkPassword and loadUser methods are supplementary methods created to support the createUser method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public User createUser(String accountName, String password1,
        String password2) throws AuthenticationException {
     
    checkPassword(accountName, password1, password2);
     
    User user = getUser(accountName);
    if (user != null) {
        throw new AuthenticationException("User Exists",
            "User " + accountName + " exists.");
    }
         
    DefaultUser newUser = loadUser(accountName);
    newUser.resetCSRFToken();
     
    userCredentials.put(accountName, password1);
         
    return newUser;
}
1
2
3
4
5
6
7
8
9
10
11
12
private void checkPassword(String accountName, String password1,
        String password2) throws AuthenticationException {
    if (password1 != null && password2 != null) {
        if (! password1.equals(password2)) {
            throw new AuthenticationException("Password mismatch",
                "User " + accountName + " needs a matching password entries.");
        }
    } else {
        throw new AuthenticationException("Password required",
            "User " + accountName + " needs password information.");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
private DefaultUser loadUser(String accountName) throws AuthenticationException {
         
    DefaultUser newUser = new DefaultUser(accountName);
    newUser.enable();
         
    Set<String> roles = new HashSet<String>();
    roles.add(AccessControl.DATA_ENTRY_OPERATOR);
    newUser.addRoles(roles);
    newUser.setScreenName("John Smith");
     
    return newUser;
}

The createUser method first invokes the checkPassword method to verify if the password values are not null and are matching values. Next it checks if the user exists using the interface’s getUser method. If not, then it creates a standardized User instance object using the loadUser method, updates the userCredentials map and returns the instantiated User object.

Some applications provide default passwords to new users. To support this functionality the Authenticator interface provides overloaded versions of the generateStrongPassword. I have leveraged the FileBasedAuthenticator’s implementation. You are free to implement your own custom strong password generation routine. The same thing is done for hashPassword method.

1
2
3
4
5
6
7
8
9
10
11
@Override
public String generateStrongPassword() {
    return FileBasedAuthenticator.getInstance()
        .generateStrongPassword();
}
 
@Override
public String generateStrongPassword(User user, String oldPassword) {
    return FileBasedAuthenticator.getInstance()
        .generateStrongPassword(user, oldPassword);
}
1
2
3
4
5
6
@Override
public String hashPassword(String password, String accountName)
    throws EncryptionException {
    return FileBasedAuthenticator.getInstance()
        .hashPassword(password, accountName);
}

The next overridden method changePassword, checks if the two values of new password are the same and that the user and old password are a valid combination.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void changePassword(User user, String currentPassword,
    String newPassword, String newPassword2)
        throws AuthenticationException {
         
    String userId = user.getAccountName();
    this.checkPassword(userId, newPassword, newPassword2);
    if (this.verifyPassword(user, currentPassword)) {
        userCredentials.put(userId, newPassword);
    } else {
        throw new AuthenticationException("Password Invalid",
            "Please enter the correct password.");
    }
}

The next overridden method is getUser. It has two overloaded versions. The first one which accepts accountId as an argument currently returns null. The second one accepts account name / userid as input argument. The input argument is checked against userCredentials map to check if the user exists. In case the user exists, an appropriately instantiated User instance is returned back to the invoking application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public User getUser(long accountId) {
    return null;
}
     
@Override
public User getUser(String accountName) {
    if (userCredentials.containsKey(accountName)) {
        try {
            DefaultUser user = loadUser(accountName);
            return user;
        } catch(AuthenticationException ex) {
            return null;
        }
    }
    return null;
}

The following overriden method implementations namely getUserNames, verifyAccountNameStrength and verifyPasswordStrength I am not interested in. Hence they throw an UnsupportedOperationException.

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
@Override
public Set getUserNames() {
    throw new UnsupportedOperationException("The Authenticator " +
        "does not support this operation.");
}
 
@Override
public String hashPassword(String password, String accountName)
    throws EncryptionException {
    return FileBasedAuthenticator.getInstance()
        .hashPassword(password, accountName);
}
 
@Override
public void verifyAccountNameStrength(String accountName)
        throws AuthenticationException {
    throw new UnsupportedOperationException("The Authenticator " +
        "does not support this operation.");       
}
 
@Override
public void verifyPasswordStrength(String oldPassword, String newPassword,
    User user) throws AuthenticationException {
    throw new UnsupportedOperationException("The Authenticator " +
        "does not support this operation.");
     
}

The overridden method removeUser removes the entry for the userid from the userCredentials map.

1
2
3
4
@Override
public void removeUser(String accountName) throws AuthenticationException {
    userCredentials.remove(accountName);
}

Now that we are done with creating a CustomAuthenticator, let’s create a test class to validate our implementation. Refer the source code of the test class HTTPAuthenticationTest.

 

ESAPI AUthenticator API를 테스트 하기위해 가짜 HTTP 요청을 만들기 위해 Mockito 클래스를 사용했다.

 

In the main method, refer to code between lines 17 and 19. Here we use Mockito mock object framework to create mock HTTP objects for HTTPServletRequest, HTTPServletResponse and HTTPSession.

Next piece of code between the lines 21 and 33 helps set up the expected return values of the HTTPServletRequest and HTTPSession instance objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTPUtilities httpUtil = ESAPI.httpUtilities();
httpUtil.setCurrentHTTP(req, res);
 
Authenticator auth = ESAPI.authenticator();
try {
    User user = auth.login();
    user.addSession(session);
    System.out.println("User Name: " + user.getAccountName() +
        " id: " + user.getAccountId());
    auth.setCurrentUser(user);
    user.logout();
} catch (AuthenticationException e) {
    e.printStackTrace();
}

In line 1 and 2 of the above code snippet, we use ESAPI provided HTTPUtilities class. We get a reference to the current thread’s instance using httpUtilities static method of ESAPI. We assign the current thread’s HTTPServletrequest and HTTPServletResponse objects to the HTTPUtilities. This ensures that the current request and response objects are available for further processing. Next in line 4 we get a handle to the Authenticator interface via ESAPI’s authenticator static method. Our CustomAuthenticator instance is returned by the authenticator method.

At line 6, the login method of the authenticator instance is invoked. It utilizes the login method defined in AbstractAuthenticator. The method uses login information maintained in the request object. The handle to the request object is made available by the HTTPUtilities class. The login method internally obtains the username and password information from the request object. The parameter to use for username and password is defined in ESAPI.properties. In our case refer lines 1 and 2 and the keys Authenticator.UsernameParameterName and Authenticator.PasswordParameterName. The values for these keys have been assigned in the request object. Refer lines 21 and 22 of the HTTPAuthenticationTest class or the code snippet below.

1
2
Mockito.when(req.getParameter("userName")).thenReturn("jsmith");
Mockito.when(req.getParameter("password")).thenReturn("abc123");

I have defined the user name as ‘jsmith’ and his password as ‘abc123′. Next I have also set the requestURL, http method of request and creation time and last accessed time of session object. Refer code snippet below:

1
2
3
4
5
6
7
8
9
10
11
Mockito.when(req.getRequestURL()).thenReturn(
    new StringBuffer("https://wwww.google.com"));
Mockito.when(req.getMethod()).thenReturn("POST");
Mockito.when(req.getSession()).thenReturn(session);
Mockito.when(req.getSession(false)).thenReturn(session);
java.util.Date currentDt = new java.util.Date();
long duration = currentDt.getTime();
Mockito.when(session.getLastAccessedTime())
    .thenReturn(duration);
Mockito.when(session.getCreationTime())
    .thenReturn(duration);

The login method internally invokes DefaultUser’s(Default implementation class for User interface) loginWithPassword method. The method checks if the user is enabled, not locked, not expired and has a valid username password combination. Next it validates the session’s creation time and last access time.
The session last access time is compared with idle time out value defined by Authenticator.IdleTimeoutDuration key in ESAPI.properties file. The session creation time is compare with absolute time out value defined in Authenticator.AbsoluteTimeoutDuration key in ESAPI.properties file.

The subsequent code after login method invocation is self-explanatory.

Thats all on ESAPI’s Authenticator interface.