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.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 /**
40 * Form-based authentication implementation. An instance of this class
41 * can be reused across several tests as it caches the session cookie.
42 * Thus the first time it is used to authenticate the user, it calls
43 * the security URL (which is by default the context URL prepended by
44 * "j_security_check"), caches the returned session cookie and adds the
45 * cookie for the next request. The second time it is called, it simply
46 * addes the session cookie for the next request.
47 *
48 * @since 1.5
49 *
50 * @version $Id: FormAuthentication.java 238991 2004-05-22 11:34:50Z vmassol $
51 */
52 public class FormAuthentication extends AbstractAuthentication
53 {
54 /**
55 * The logger.
56 */
57 private static final Log LOGGER =
58 LogFactory.getLog(FormAuthentication.class);
59
60 /**
61 * The expected HTTP response status code when the authentication
62 * is succeeded.
63 */
64 private int expectedAuthResponse = HttpURLConnection.HTTP_MOVED_TEMP;
65
66 /**
67 * The URL to use when attempting to log in, if for whatever reason
68 * the default URL is incorrect.
69 */
70 private URL securityCheckURL;
71
72 /**
73 * The cookie name of the session.
74 */
75 private String sessionCookieName = "JSESSIONID";
76
77 /**
78 * We store the session cookie.
79 */
80 private Cookie jsessionCookie;
81
82 /**
83 * {@link WebRequest} object that will be used to connect to the
84 * security URL.
85 */
86 private WebRequest securityRequest = new WebRequestImpl();
87
88 /**
89 * @param theName user name of the Credential
90 * @param thePassword user password of the Credential
91 */
92 public FormAuthentication(String theName, String thePassword)
93 {
94 super(theName, thePassword);
95 }
96
97 /**
98 * {@inheritDoc}
99 * @see Authentication#configure
100 */
101 public void configure(HttpState theState, HttpMethod theMethod,
102 WebRequest theRequest, Configuration theConfiguration)
103 {
104 // Only authenticate the first time this instance is used.
105 if (this.jsessionCookie == null)
106 {
107 authenticate(theRequest, theConfiguration);
108 }
109
110 // Sets the session id cookie for the next request.
111 if (this.jsessionCookie != null)
112 {
113 theRequest.addCookie(this.jsessionCookie);
114 }
115 }
116
117 /**
118 * @return the {@link WebRequest} that will be used to connect to the
119 * security URL. It can be used to add additional HTTP parameters such
120 * as proprietary ones required by some containers.
121 */
122 public WebRequest getSecurityRequest()
123 {
124 return this.securityRequest;
125 }
126
127 /**
128 * This sets the URL to use when attempting to log in. This method is used
129 * if for whatever reason the default URL is incorrect.
130 *
131 * @param theUrl A URL to use to attempt to login.
132 */
133 public void setSecurityCheckURL(URL theUrl)
134 {
135 this.securityCheckURL = theUrl;
136 }
137
138 /**
139 * This returns the URL to use when attempting to log in. By default, it's
140 * the context URL defined in the Cactus configuration with
141 * "/j_security_check" appended.
142 *
143 * @param theConfiguration the Cactus configuration
144 * @return the URL that is being used to attempt to login.
145 */
146 public URL getSecurityCheckURL(Configuration theConfiguration)
147 {
148 if (this.securityCheckURL == null)
149 {
150 // Configure default
151 String stringUrl =
152 ((WebConfiguration) theConfiguration).getContextURL()
153 + "/j_security_check";
154
155 try
156 {
157 this.securityCheckURL = new URL(stringUrl);
158 }
159 catch (MalformedURLException e)
160 {
161 throw new ChainedRuntimeException(
162 "Unable to create default Security Check URL ["
163 + stringUrl + "]");
164 }
165 }
166
167 LOGGER.debug("Using security check URL [" + this.securityCheckURL
168 + "]");
169
170 return securityCheckURL;
171 }
172
173
174 /**
175 * Get the cookie name of the session.
176 * @return the cookie name of the session
177 */
178 private String getSessionCookieName()
179 {
180 return this.sessionCookieName;
181 }
182
183 /**
184 * Set the cookie name of the session to theName.
185 * If theName is null, the change request will be ignored.
186 * The default is "<code>JSESSIONID</code>".
187 * @param theName the cookie name of the session
188 */
189 public void setSessionCookieName(String theName)
190 {
191 if (theName != null)
192 {
193 this.sessionCookieName = theName;
194 }
195 }
196
197
198 /**
199 * Get the expected HTTP response status code for an authentication request
200 * which should be successful.
201 * @return the expected HTTP response status code
202 */
203 protected int getExpectedAuthResponse()
204 {
205 return this.expectedAuthResponse;
206 }
207
208 /**
209 * Set the expected HTTP response status code for an authentication request
210 * which should be successful.
211 * The default is HttpURLConnection.HTTP_MOVED_TEMP.
212 * @param theExpectedCode the expected HTTP response status code value
213 */
214 public void setExpectedAuthResponse(int theExpectedCode)
215 {
216 this.expectedAuthResponse = theExpectedCode;
217 }
218
219
220 /**
221 * Get a cookie required to be set by set-cookie header field.
222 * @param theConnection a {@link HttpURLConnection}
223 * @param theTarget the target cookie name
224 * @return the {@link Cookie}
225 */
226 private Cookie getCookie(HttpURLConnection theConnection, String theTarget)
227 {
228 // Check (possible multiple) cookies for a target.
229 int i = 1;
230 String key = theConnection.getHeaderFieldKey(i);
231 while (key != null)
232 {
233 if (key.equalsIgnoreCase("set-cookie"))
234 {
235 // Cookie is in the form:
236 // "NAME=VALUE; expires=DATE; path=PATH;
237 // domain=DOMAIN_NAME; secure"
238 // The only thing we care about is finding a cookie with
239 // the name "JSESSIONID" and caching the value.
240 String cookiestr = theConnection.getHeaderField(i);
241 String nameValue = cookiestr.substring(0,
242 cookiestr.indexOf(";"));
243 int equalsChar = nameValue.indexOf("=");
244 String name = nameValue.substring(0, equalsChar);
245 String value = nameValue.substring(equalsChar + 1);
246 if (name.equalsIgnoreCase(theTarget))
247 {
248 return new Cookie(theConnection.getURL().getHost(),
249 name, value);
250 }
251 }
252 key = theConnection.getHeaderFieldKey(++i);
253 }
254 return null;
255 }
256
257
258 /**
259 * Check if the pre-auth step can be considered as succeeded or not.
260 * As default, the step considered as succeeded
261 * if the response status code of <code>theConnection</code>
262 * is less than 400.
263 *
264 * @param theConnection a <code>HttpURLConnection</code> value
265 * @exception Exception if the pre-auth step should be considered as failed
266 */
267 protected void checkPreAuthResponse(HttpURLConnection theConnection)
268 throws Exception
269 {
270 if (theConnection.getResponseCode() >= 400)
271 {
272 throw new Exception("Received a status code ["
273 + theConnection.getResponseCode()
274 + "] and was expecting less than 400");
275 }
276 }
277
278
279 /**
280 * Get login session cookie.
281 * This is the first step to start login session:
282 * <ol>
283 * <dt> C->S: </dt>
284 * <dd> try to connect to a restricted resource </dd>
285 * <dt> S->C: </dt>
286 * <dd> redirect or forward to the login page with set-cookie header </dd>
287 * </ol>
288 * @param theRequest a request to connect to a restricted resource
289 * @param theConfiguration a <code>Configuration</code> value
290 * @return the <code>Cookie</code>
291 */
292 private Cookie getSecureSessionIdCookie(WebRequest theRequest,
293 Configuration theConfiguration)
294 {
295 HttpURLConnection connection;
296 String resource = null;
297
298 try
299 {
300 // Create a helper that will connect to a restricted resource.
301 WebConfiguration webConfig = (WebConfiguration) theConfiguration;
302 resource = webConfig.getRedirectorURL(theRequest);
303
304 HttpClientConnectionHelper helper =
305 new HttpClientConnectionHelper(resource);
306
307 WebRequest request =
308 new WebRequestImpl((WebConfiguration) theConfiguration);
309
310 // Make the connection using a default web request.
311 connection = helper.connect(request, theConfiguration);
312
313 checkPreAuthResponse(connection);
314 }
315 catch (Throwable e)
316 {
317 throw new ChainedRuntimeException(
318 "Failed to connect to the secured redirector: " + resource, e);
319 }
320
321 return getCookie(connection, getSessionCookieName());
322 }
323
324
325 /**
326 * Check if the auth step can be considered as succeeded or not.
327 * As default, the step considered as succeeded
328 * if the response status code of <code>theConnection</code>
329 * equals <code>getExpectedAuthResponse()</code>.
330 *
331 * @param theConnection a <code>HttpURLConnection</code> value
332 * @exception Exception if the auth step should be considered as failed
333 */
334 protected void checkAuthResponse(HttpURLConnection theConnection)
335 throws Exception
336 {
337 if (theConnection.getResponseCode() != getExpectedAuthResponse())
338 {
339 throw new Exception("Received a status code ["
340 + theConnection.getResponseCode()
341 + "] and was expecting a ["
342 + getExpectedAuthResponse() + "]");
343 }
344 }
345
346
347 /**
348 * Authenticate the principal by calling the security URL.
349 *
350 * @param theRequest the web request used to connect to the Redirector
351 * @param theConfiguration the Cactus configuration
352 */
353 public void authenticate(WebRequest theRequest,
354 Configuration theConfiguration)
355 {
356 this.jsessionCookie = getSecureSessionIdCookie(theRequest,
357 theConfiguration);
358
359 try
360 {
361 // Create a helper that will connect to the security check URL.
362 HttpClientConnectionHelper helper =
363 new HttpClientConnectionHelper(
364 getSecurityCheckURL(theConfiguration).toString());
365
366 // Configure a web request with the JSESSIONID cookie,
367 // the username and the password.
368 WebRequest request = getSecurityRequest();
369 ((WebRequestImpl) request).setConfiguration(theConfiguration);
370 request.addCookie(this.jsessionCookie);
371 request.addParameter("j_username", getName(),
372 WebRequest.POST_METHOD);
373 request.addParameter("j_password", getPassword(),
374 WebRequest.POST_METHOD);
375
376 // Make the connection using the configured web request.
377 HttpURLConnection connection = helper.connect(request,
378 theConfiguration);
379
380 checkAuthResponse(connection);
381 }
382 catch (Throwable e)
383 {
384 this.jsessionCookie = null;
385 throw new ChainedRuntimeException(
386 "Failed to authenticate the principal", e);
387 }
388 }
389 }