Source code: com/xpn/xwiki/test/MyFormAuthentication.java
1 /**
2 * ===================================================================
3 *
4 * Copyright (c) 2003,2004 Ludovic Dubost, All rights reserved.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details, published at
15 * http://www.gnu.org/copyleft/lesser.html or in lesser.txt in the
16 * root folder of this distribution.
17
18 * Created by
19 * User: Ludovic Dubost
20 * Date: 29 avr. 2004
21 * Time: 16:06:29
22 */
23 package com.xpn.xwiki.test;
24
25 import org.apache.cactus.Cookie;
26 import org.apache.cactus.WebRequest;
27 import org.apache.cactus.client.authentication.AbstractAuthentication;
28 import org.apache.cactus.client.authentication.Authentication;
29 import org.apache.cactus.internal.WebRequestImpl;
30 import org.apache.cactus.internal.client.connector.http.HttpClientConnectionHelper;
31 import org.apache.cactus.internal.configuration.Configuration;
32 import org.apache.cactus.internal.configuration.WebConfiguration;
33 import org.apache.cactus.util.ChainedRuntimeException;
34 import org.apache.commons.httpclient.HttpMethod;
35 import org.apache.commons.httpclient.HttpState;
36 import org.apache.commons.logging.Log;
37 import org.apache.commons.logging.LogFactory;
38
39 import java.net.HttpURLConnection;
40 import java.net.MalformedURLException;
41 import java.net.URL;
42 import java.util.ArrayList;
43 import java.util.List;
44
45 /**
46 * Form-based authentication implementation. An instance of this class
47 * can be reused across several tests as it caches the session cookie.
48 * Thus the first time it is used to authenticate the user, it calls
49 * the security URL (which is by default the context URL prepended by
50 * "j_security_check"), caches the returned session cookie and adds the
51 * cookie for the next request. The second time it is called, it simply
52 * addes the session cookie for the next request.
53 *
54 * @since 1.5
55 *
56 * @version $Id: MyFormAuthentication.java 322 2004-06-29 17:58:57Z ldubost $
57 */
58 public class MyFormAuthentication extends AbstractAuthentication
59 {
60 /**
61 * The logger.
62 */
63 private static final Log LOGGER =
64 LogFactory.getLog(MyFormAuthentication.class);
65
66 /**
67 * The expected HTTP response status code when the authentication
68 * is succeeded.
69 */
70 private int expectedAuthResponse = HttpURLConnection.HTTP_MOVED_TEMP;
71
72 /**
73 * The URL to use when attempting to log in, if for whatever reason
74 * the default URL is incorrect.
75 */
76 private URL securityCheckURL;
77
78 /**
79 * The cookie name of the session.
80 */
81 private String sessionCookieName = "JSESSIONID";
82
83 /**
84 * We store the session cookie.
85 */
86 private Cookie jsessionCookie;
87
88 /**
89 * We store all cookies
90 */
91 private List cookies;
92
93 /**
94 * {@link org.apache.cactus.WebRequest} object that will be used to connect to the
95 * security URL.
96 */
97 private WebRequest securityRequest = new WebRequestImpl();
98
99 /**
100 * @param theName user name of the Credential
101 * @param thePassword user password of the Credential
102 */
103 public MyFormAuthentication(String theName, String thePassword)
104 {
105 super(theName, thePassword);
106 }
107
108 /**
109 * @see Authentication#configure
110 */
111 public void configure(HttpState theState, HttpMethod theMethod,
112 WebRequest theRequest, Configuration theConfiguration)
113 {
114 // Only authenticate the first time this instance is used.
115 if (this.jsessionCookie == null)
116 {
117 authenticate(theRequest, theConfiguration);
118 }
119
120 // Sets the session id cookie for the next request.
121 if (this.jsessionCookie != null)
122 {
123 for (int i=0;i<cookies.size();i++)
124 theRequest.addCookie((Cookie)cookies.get(i));
125 }
126 }
127
128 /**
129 * @return the {@link WebRequest} that will be used to connect to the
130 * security URL. It can be used to add additional HTTP parameters such
131 * as proprietary ones required by some containers.
132 */
133 public WebRequest getSecurityRequest()
134 {
135 return this.securityRequest;
136 }
137
138 /**
139 * This sets the URL to use when attempting to log in. This method is used
140 * if for whatever reason the default URL is incorrect.
141 *
142 * @param theUrl A URL to use to attempt to login.
143 */
144 public void setSecurityCheckURL(URL theUrl)
145 {
146 this.securityCheckURL = theUrl;
147 }
148
149 /**
150 * This returns the URL to use when attempting to log in. By default, it's
151 * the context URL defined in the Cactus configuration with
152 * "/j_security_check" appended.
153 *
154 * @param theConfiguration the Cactus configuration
155 * @return the URL that is being used to attempt to login.
156 */
157 public URL getSecurityCheckURL(Configuration theConfiguration)
158 {
159 if (this.securityCheckURL == null)
160 {
161 // Configure default
162 String stringUrl =
163 ((WebConfiguration) theConfiguration).getContextURL()
164 + "/j_security_check";
165
166 try
167 {
168 this.securityCheckURL = new URL(stringUrl);
169 }
170 catch (MalformedURLException e)
171 {
172 throw new ChainedRuntimeException(
173 "Unable to create default Security Check URL ["
174 + stringUrl + "]");
175 }
176 }
177
178 LOGGER.debug("Using security check URL [" + this.securityCheckURL
179 + "]");
180
181 return securityCheckURL;
182 }
183
184
185 /**
186 * Get the cookie name of the session.
187 * @return the cookie name of the session
188 */
189 private String getSessionCookieName()
190 {
191 return this.sessionCookieName;
192 }
193
194 /**
195 * Set the cookie name of the session to theName.
196 * If theName is null, the change request will be ignored.
197 * The default is "<code>JSESSIONID</code>".
198 * @param theName the cookie name of the session
199 */
200 public void setSessionCookieName(String theName)
201 {
202 if (theName != null)
203 {
204 this.sessionCookieName = theName;
205 }
206 }
207
208
209 /**
210 * Get the expected HTTP response status code for an authentication request
211 * which should be successful.
212 * @return the expected HTTP response status code
213 */
214 protected int getExpectedAuthResponse()
215 {
216 return this.expectedAuthResponse;
217 }
218
219 /**
220 * Set the expected HTTP response status code for an authentication request
221 * which should be successful.
222 * The default is HttpURLConnection.HTTP_MOVED_TEMP.
223 * @param theExpectedCode the expected HTTP response status code value
224 */
225 public void setExpectedAuthResponse(int theExpectedCode)
226 {
227 this.expectedAuthResponse = theExpectedCode;
228 }
229
230
231 /**
232 * Get a cookie required to be set by set-cookie header field.
233 * @param theConnection a {@link HttpURLConnection}
234 * @param theTarget the target cookie name
235 * @return the {@link Cookie}
236 */
237 private Cookie getCookie(HttpURLConnection theConnection, String theTarget)
238 {
239 // Check (possible multiple) cookies for a target.
240 int i = 1;
241 String key = theConnection.getHeaderFieldKey(i);
242 while (key != null)
243 {
244 if (key.equalsIgnoreCase("set-cookie"))
245 {
246 // Cookie is in the form:
247 // "NAME=VALUE; expires=DATE; path=PATH;
248 // domain=DOMAIN_NAME; secure"
249 // The only thing we care about is finding a cookie with
250 // the name "JSESSIONID" and caching the value.
251 String cookiestr = theConnection.getHeaderField(i);
252 String nameValue = cookiestr.substring(0,
253 cookiestr.indexOf(";"));
254 int equalsChar = nameValue.indexOf("=");
255 String name = nameValue.substring(0, equalsChar);
256 String value = nameValue.substring(equalsChar + 1);
257 if (name.equalsIgnoreCase(theTarget))
258 {
259 String host = theConnection.getURL().getHost();
260 // Let's force it to localhost as it seems to fail on Linux
261 host = "localhost";
262
263 /*
264 String[] hosts = {
265 "127.0.0.1",
266 "localhost",
267 "localhost.localdomain"
268 };
269 for (int h = 0; h < hosts.length; h++) {
270
271 }
272 */
273 return new Cookie(host,
274 name, value);
275 }
276 }
277 key = theConnection.getHeaderFieldKey(++i);
278 }
279 return null;
280 }
281
282 private List getAllCookies(HttpURLConnection theConnection)
283 {
284 // Check (possible multiple) cookies for a target.
285 int i = 1;
286 List cookies = new ArrayList();
287 String key = theConnection.getHeaderFieldKey(i);
288 while (key != null)
289 {
290 if (key.equalsIgnoreCase("set-cookie"))
291 {
292 // Cookie is in the form:
293 // "NAME=VALUE; expires=DATE; path=PATH;
294 // domain=DOMAIN_NAME; secure"
295 // The only thing we care about is finding a cookie with
296 // the name "JSESSIONID" and caching the value.
297 String cookiestr = theConnection.getHeaderField(i);
298 String nameValue = cookiestr.substring(0,
299 cookiestr.indexOf(";"));
300 int equalsChar = nameValue.indexOf("=");
301 String name = nameValue.substring(0, equalsChar);
302 String value = nameValue.substring(equalsChar + 1);
303 // String host = theConnection.getURL().getHost();
304 // Let's force it to localhost as it seems to fail on Linux
305 String [] hosts = { "localhost", "127.0.0.1", "localhost.localdomain" };
306 for (int h = 0; h < hosts.length; h ++) {
307 String host = hosts[h];
308 Cookie cookie = new Cookie(host, name, value);
309 cookie.setPath("/");
310 cookies.add(cookie);
311
312 }
313 }
314 key = theConnection.getHeaderFieldKey(++i);
315 }
316 return cookies;
317 }
318
319
320 /**
321 * Check if the pre-auth step can be considered as succeeded or not.
322 * As default, the step considered as succeeded
323 * if the response status code of <code>theConnection</code>
324 * is less than 400.
325 *
326 * @param theConnection a <code>HttpURLConnection</code> value
327 * @exception Exception if the pre-auth step should be considered as failed
328 */
329 protected void checkPreAuthResponse(HttpURLConnection theConnection)
330 throws Exception
331 {
332 if (theConnection.getResponseCode() >= 400)
333 {
334 throw new Exception("Received a status code ["
335 + theConnection.getResponseCode()
336 + "] and was expecting less than 400");
337 }
338 }
339
340
341 /**
342 * Get login session cookie.
343 * This is the first step to start login session:
344 * <dl>
345 * <dt> C->S: </dt>
346 * <dd> try to connect to a restricted resource </dd>
347 * <dt> S->C: </dt>
348 * <dd> redirect or forward to the login page with set-cookie header </dd>
349 * </ol>
350 * @param theRequest a request to connect to a restricted resource
351 * @param theConfiguration a <code>Configuration</code> value
352 * @return the <code>Cookie</code>
353 */
354 private Cookie getSecureSessionIdCookie(WebRequest theRequest,
355 Configuration theConfiguration)
356 {
357 HttpURLConnection connection;
358 String resource = null;
359
360 try
361 {
362 // Create a helper that will connect to a restricted resource.
363 WebConfiguration webConfig = (WebConfiguration) theConfiguration;
364 resource = webConfig.getRedirectorURL(theRequest);
365
366 HttpClientConnectionHelper helper =
367 new HttpClientConnectionHelper(resource);
368
369 WebRequest request =
370 new WebRequestImpl((WebConfiguration) theConfiguration);
371
372 theRequest.setAuthentication(null);
373
374 // Make the connection using a default web request.
375 connection = helper.connect(theRequest, theConfiguration);
376
377 theRequest.setAuthentication(this);
378
379 checkPreAuthResponse(connection);
380 }
381 catch (Throwable e)
382 {
383 throw new ChainedRuntimeException(
384 "Failed to connect to the secured redirector: " + resource, e);
385 }
386
387 return getCookie(connection, getSessionCookieName());
388 }
389
390
391 /**
392 * Check if the auth step can be considered as succeeded or not.
393 * As default, the step considered as succeeded
394 * if the response status code of <code>theConnection</code>
395 * equals <code>getExpectedAuthResponse()</code>.
396 *
397 * @param theConnection a <code>HttpURLConnection</code> value
398 * @exception Exception if the auth step should be considered as failed
399 */
400 protected void checkAuthResponse(HttpURLConnection theConnection)
401 throws Exception
402 {
403 if (theConnection.getResponseCode() != getExpectedAuthResponse())
404 {
405
406 throw new Exception("Received a status code ["
407 + theConnection.getResponseCode()
408 + "] and was expecting a ["
409 + getExpectedAuthResponse() + "]\nURL: "
410 + theConnection.getURL().toString() + "\nPost: "
411 + theConnection.getRequestProperties().toString());
412 }
413 }
414
415
416 /**
417 * Authenticate the principal by calling the security URL.
418 *
419 * @param theRequest the web request used to connect to the Redirector
420 * @param theConfiguration the Cactus configuration
421 */
422 public void authenticate(WebRequest theRequest,
423 Configuration theConfiguration)
424 {
425 this.jsessionCookie = getSecureSessionIdCookie(theRequest,
426 theConfiguration);
427
428 try
429 {
430 // Create a helper that will connect to the security check URL.
431 HttpClientConnectionHelper helper =
432 new HttpClientConnectionHelper(
433 getSecurityCheckURL(theConfiguration).toString());
434
435 // Configure a web request with the JSESSIONID cookie,
436 // the username and the password.
437 WebRequest request = getSecurityRequest();
438 ((WebRequestImpl) request).setConfiguration(theConfiguration);
439 request.addCookie(this.jsessionCookie);
440 request.addParameter("j_username", getName(),
441 WebRequest.POST_METHOD);
442 request.addParameter("j_password", getPassword(),
443 WebRequest.POST_METHOD);
444 request.addParameter("j_rememberme", "true",
445 WebRequest.POST_METHOD);
446
447 // Make the connection using the configured web request.
448 HttpURLConnection connection = helper.connect(request,
449 theConfiguration);
450
451 checkAuthResponse(connection);
452 cookies = getAllCookies(connection);
453 }
454 catch (Throwable e)
455 {
456 this.jsessionCookie = null;
457 throw new ChainedRuntimeException(
458 "Failed to authenticate the principal", e);
459 }
460 }
461 }