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