001: /*
002: * ========================================================================
003: *
004: * Copyright 2001-2004 The Apache Software Foundation.
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: * ========================================================================
019: */
020: package org.apache.cactus.client.authentication;
021:
022: import java.net.HttpURLConnection;
023: import java.net.MalformedURLException;
024: import java.net.URL;
025:
026: import org.apache.cactus.Cookie;
027: import org.apache.cactus.WebRequest;
028: import org.apache.cactus.internal.WebRequestImpl;
029: import org.apache.cactus.internal.client.connector.http.HttpClientConnectionHelper;
030: import org.apache.cactus.internal.configuration.Configuration;
031: import org.apache.cactus.internal.configuration.WebConfiguration;
032: import org.apache.cactus.util.ChainedRuntimeException;
033: import org.apache.commons.httpclient.HttpMethod;
034: import org.apache.commons.httpclient.HttpState;
035: import org.apache.commons.logging.Log;
036: import org.apache.commons.logging.LogFactory;
037:
038: /**
039: * Form-based authentication implementation. An instance of this class
040: * can be reused across several tests as it caches the session cookie.
041: * Thus the first time it is used to authenticate the user, it calls
042: * the security URL (which is by default the context URL prepended by
043: * "j_security_check"), caches the returned session cookie and adds the
044: * cookie for the next request. The second time it is called, it simply
045: * addes the session cookie for the next request.
046: *
047: * @since 1.5
048: *
049: * @version $Id: FormAuthentication.java 238991 2004-05-22 11:34:50Z vmassol $
050: */
051: public class FormAuthentication extends AbstractAuthentication {
052: /**
053: * The logger.
054: */
055: private static final Log LOGGER = LogFactory
056: .getLog(FormAuthentication.class);
057:
058: /**
059: * The expected HTTP response status code when the authentication
060: * is succeeded.
061: */
062: private int expectedAuthResponse = HttpURLConnection.HTTP_MOVED_TEMP;
063:
064: /**
065: * The URL to use when attempting to log in, if for whatever reason
066: * the default URL is incorrect.
067: */
068: private URL securityCheckURL;
069:
070: /**
071: * The cookie name of the session.
072: */
073: private String sessionCookieName = "JSESSIONID";
074:
075: /**
076: * We store the session cookie.
077: */
078: private Cookie jsessionCookie;
079:
080: /**
081: * {@link WebRequest} object that will be used to connect to the
082: * security URL.
083: */
084: private WebRequest securityRequest = new WebRequestImpl();
085:
086: /**
087: * @param theName user name of the Credential
088: * @param thePassword user password of the Credential
089: */
090: public FormAuthentication(String theName, String thePassword) {
091: super (theName, thePassword);
092: }
093:
094: /**
095: * @see Authentication#configure
096: */
097: public void configure(HttpState theState, HttpMethod theMethod,
098: WebRequest theRequest, Configuration theConfiguration) {
099: // Only authenticate the first time this instance is used.
100: if (this .jsessionCookie == null) {
101: authenticate(theRequest, theConfiguration);
102: }
103:
104: // Sets the session id cookie for the next request.
105: if (this .jsessionCookie != null) {
106: theRequest.addCookie(this .jsessionCookie);
107: }
108: }
109:
110: /**
111: * @return the {@link WebRequest} that will be used to connect to the
112: * security URL. It can be used to add additional HTTP parameters such
113: * as proprietary ones required by some containers.
114: */
115: public WebRequest getSecurityRequest() {
116: return this .securityRequest;
117: }
118:
119: /**
120: * This sets the URL to use when attempting to log in. This method is used
121: * if for whatever reason the default URL is incorrect.
122: *
123: * @param theUrl A URL to use to attempt to login.
124: */
125: public void setSecurityCheckURL(URL theUrl) {
126: this .securityCheckURL = theUrl;
127: }
128:
129: /**
130: * This returns the URL to use when attempting to log in. By default, it's
131: * the context URL defined in the Cactus configuration with
132: * "/j_security_check" appended.
133: *
134: * @param theConfiguration the Cactus configuration
135: * @return the URL that is being used to attempt to login.
136: */
137: public URL getSecurityCheckURL(Configuration theConfiguration) {
138: if (this .securityCheckURL == null) {
139: // Configure default
140: String stringUrl = ((WebConfiguration) theConfiguration)
141: .getContextURL()
142: + "/j_security_check";
143:
144: try {
145: this .securityCheckURL = new URL(stringUrl);
146: } catch (MalformedURLException e) {
147: throw new ChainedRuntimeException(
148: "Unable to create default Security Check URL ["
149: + stringUrl + "]");
150: }
151: }
152:
153: LOGGER.debug("Using security check URL ["
154: + this .securityCheckURL + "]");
155:
156: return securityCheckURL;
157: }
158:
159: /**
160: * Get the cookie name of the session.
161: * @return the cookie name of the session
162: */
163: private String getSessionCookieName() {
164: return this .sessionCookieName;
165: }
166:
167: /**
168: * Set the cookie name of the session to theName.
169: * If theName is null, the change request will be ignored.
170: * The default is "<code>JSESSIONID</code>".
171: * @param theName the cookie name of the session
172: */
173: public void setSessionCookieName(String theName) {
174: if (theName != null) {
175: this .sessionCookieName = theName;
176: }
177: }
178:
179: /**
180: * Get the expected HTTP response status code for an authentication request
181: * which should be successful.
182: * @return the expected HTTP response status code
183: */
184: protected int getExpectedAuthResponse() {
185: return this .expectedAuthResponse;
186: }
187:
188: /**
189: * Set the expected HTTP response status code for an authentication request
190: * which should be successful.
191: * The default is HttpURLConnection.HTTP_MOVED_TEMP.
192: * @param theExpectedCode the expected HTTP response status code value
193: */
194: public void setExpectedAuthResponse(int theExpectedCode) {
195: this .expectedAuthResponse = theExpectedCode;
196: }
197:
198: /**
199: * Get a cookie required to be set by set-cookie header field.
200: * @param theConnection a {@link HttpURLConnection}
201: * @param theTarget the target cookie name
202: * @return the {@link Cookie}
203: */
204: private Cookie getCookie(HttpURLConnection theConnection,
205: String theTarget) {
206: // Check (possible multiple) cookies for a target.
207: int i = 1;
208: String key = theConnection.getHeaderFieldKey(i);
209: while (key != null) {
210: if (key.equalsIgnoreCase("set-cookie")) {
211: // Cookie is in the form:
212: // "NAME=VALUE; expires=DATE; path=PATH;
213: // domain=DOMAIN_NAME; secure"
214: // The only thing we care about is finding a cookie with
215: // the name "JSESSIONID" and caching the value.
216: String cookiestr = theConnection.getHeaderField(i);
217: String nameValue = cookiestr.substring(0, cookiestr
218: .indexOf(";"));
219: int equalsChar = nameValue.indexOf("=");
220: String name = nameValue.substring(0, equalsChar);
221: String value = nameValue.substring(equalsChar + 1);
222: if (name.equalsIgnoreCase(theTarget)) {
223: return new Cookie(theConnection.getURL().getHost(),
224: name, value);
225: }
226: }
227: key = theConnection.getHeaderFieldKey(++i);
228: }
229: return null;
230: }
231:
232: /**
233: * Check if the pre-auth step can be considered as succeeded or not.
234: * As default, the step considered as succeeded
235: * if the response status code of <code>theConnection</code>
236: * is less than 400.
237: *
238: * @param theConnection a <code>HttpURLConnection</code> value
239: * @exception Exception if the pre-auth step should be considered as failed
240: */
241: protected void checkPreAuthResponse(HttpURLConnection theConnection)
242: throws Exception {
243: if (theConnection.getResponseCode() >= 400) {
244: throw new Exception("Received a status code ["
245: + theConnection.getResponseCode()
246: + "] and was expecting less than 400");
247: }
248: }
249:
250: /**
251: * Get login session cookie.
252: * This is the first step to start login session:
253: * <dl>
254: * <dt> C->S: </dt>
255: * <dd> try to connect to a restricted resource </dd>
256: * <dt> S->C: </dt>
257: * <dd> redirect or forward to the login page with set-cookie header </dd>
258: * </ol>
259: * @param theRequest a request to connect to a restricted resource
260: * @param theConfiguration a <code>Configuration</code> value
261: * @return the <code>Cookie</code>
262: */
263: private Cookie getSecureSessionIdCookie(WebRequest theRequest,
264: Configuration theConfiguration) {
265: HttpURLConnection connection;
266: String resource = null;
267:
268: try {
269: // Create a helper that will connect to a restricted resource.
270: WebConfiguration webConfig = (WebConfiguration) theConfiguration;
271: resource = webConfig.getRedirectorURL(theRequest);
272:
273: HttpClientConnectionHelper helper = new HttpClientConnectionHelper(
274: resource);
275:
276: WebRequest request = new WebRequestImpl(
277: (WebConfiguration) theConfiguration);
278:
279: // Make the connection using a default web request.
280: connection = helper.connect(request, theConfiguration);
281:
282: checkPreAuthResponse(connection);
283: } catch (Throwable e) {
284: throw new ChainedRuntimeException(
285: "Failed to connect to the secured redirector: "
286: + resource, e);
287: }
288:
289: return getCookie(connection, getSessionCookieName());
290: }
291:
292: /**
293: * Check if the auth step can be considered as succeeded or not.
294: * As default, the step considered as succeeded
295: * if the response status code of <code>theConnection</code>
296: * equals <code>getExpectedAuthResponse()</code>.
297: *
298: * @param theConnection a <code>HttpURLConnection</code> value
299: * @exception Exception if the auth step should be considered as failed
300: */
301: protected void checkAuthResponse(HttpURLConnection theConnection)
302: throws Exception {
303: if (theConnection.getResponseCode() != getExpectedAuthResponse()) {
304: throw new Exception("Received a status code ["
305: + theConnection.getResponseCode()
306: + "] and was expecting a ["
307: + getExpectedAuthResponse() + "]");
308: }
309: }
310:
311: /**
312: * Authenticate the principal by calling the security URL.
313: *
314: * @param theRequest the web request used to connect to the Redirector
315: * @param theConfiguration the Cactus configuration
316: */
317: public void authenticate(WebRequest theRequest,
318: Configuration theConfiguration) {
319: this .jsessionCookie = getSecureSessionIdCookie(theRequest,
320: theConfiguration);
321:
322: try {
323: // Create a helper that will connect to the security check URL.
324: HttpClientConnectionHelper helper = new HttpClientConnectionHelper(
325: getSecurityCheckURL(theConfiguration).toString());
326:
327: // Configure a web request with the JSESSIONID cookie,
328: // the username and the password.
329: WebRequest request = getSecurityRequest();
330: ((WebRequestImpl) request)
331: .setConfiguration(theConfiguration);
332: request.addCookie(this .jsessionCookie);
333: request.addParameter("j_username", getName(),
334: WebRequest.POST_METHOD);
335: request.addParameter("j_password", getPassword(),
336: WebRequest.POST_METHOD);
337:
338: // Make the connection using the configured web request.
339: HttpURLConnection connection = helper.connect(request,
340: theConfiguration);
341:
342: checkAuthResponse(connection);
343: } catch (Throwable e) {
344: this .jsessionCookie = null;
345: throw new ChainedRuntimeException(
346: "Failed to authenticate the principal", e);
347: }
348: }
349: }
|