원본: 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 파일을 다운로드 받는다.
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:
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176 |
package com.esapi.authenticator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.owasp.esapi.Authenticator; import org.owasp.esapi.User; import org.owasp.esapi.errors.AuthenticationException; import org.owasp.esapi.errors.EncryptionException; import org.owasp.esapi.reference.AbstractAuthenticator; import org.owasp.esapi.reference.DefaultUser; import org.owasp.esapi.reference.FileBasedAuthenticator; import com.esapi.accesscontrol.AccessControl; public class CustomAuthenticator extends AbstractAuthenticator { private static final CustomAuthenticator authImpl = new CustomAuthenticator(); private static Map<String, String> userCredentials = new HashMap<String, String>(); static { userCredentials.put( "jsmith" , "abc123" ); } private CustomAuthenticator() { } public static Authenticator getInstance(){ return authImpl; } @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 ; } @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; } 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; } 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." ); } } @Override public String generateStrongPassword() { return FileBasedAuthenticator.getInstance() .generateStrongPassword(); } @Override public String generateStrongPassword(User user, String oldPassword) { return FileBasedAuthenticator.getInstance() .generateStrongPassword(user, oldPassword); } @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." ); } } @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 ; } @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 removeUser(String accountName) throws AuthenticationException { userCredentials.remove(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 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 클래스를 사용했다.
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 |
package com.esapi.http.test; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.mockito.Mockito; import org.owasp.esapi.Authenticator; import org.owasp.esapi.ESAPI; import org.owasp.esapi.HTTPUtilities; import org.owasp.esapi.User; import org.owasp.esapi.errors.AuthenticationException; public class HTTPAuthenticationTest { public static void main(String[] args) { HttpServletRequest req = Mockito.mock(HttpServletRequest. class ); HttpServletResponse res = Mockito.mock(HttpServletResponse. class ); HttpSession session = Mockito.mock(HttpSession. class ); Mockito.when(req.getParameter( "userName" )).thenReturn( "jsmith" ); Mockito.when(req.getParameter( "password" )).thenReturn( "abc123" ); Mockito.when(req.getRequestURL()).thenReturn( 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); 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 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( 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.
'보안 > 시큐어코딩' 카테고리의 다른 글
정규식 테스트 예제 코드 (0) | 2013.10.30 |
---|---|
웹애플리케이션 해커의 공격 방법론 (0) | 2013.10.29 |
ESAPI를 이용한 OWASP Top 취약점 예방 (0) | 2013.10.25 |
[시큐어코딩실습] CSRF 방어코드 작성기법 (0) | 2013.10.25 |
[시큐어코딩실습] XSS 방어를 위한 시큐어코딩 기법 (0) | 2013.10.24 |