001: /*
002: * BasicAuthHandler.java
003: *
004: * Brazil project web application Framework,
005: * export version: 1.1
006: * Copyright (c) 1998-2001 Sun Microsystems, Inc.
007: *
008: * Sun Public License Notice
009: *
010: * The contents of this file are subject to the Sun Public License Version
011: * 1.0 (the "License"). You may not use this file except in compliance with
012: * the License. A copy of the License is included as the file "license.terms",
013: * and also available at http://www.sun.com/
014: *
015: * The Original Code is from:
016: * Brazil project web application Framework release 1.1.
017: * The Initial Developer of the Original Code is: suhler.
018: * Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
019: * All Rights Reserved.
020: *
021: * Contributor(s): cstevens, suhler.
022: *
023: * Version: 1.27
024: * Created by suhler on 98/09/14
025: * Last modified by suhler on 01/01/12 08:25:13
026: */
027:
028: package sunlabs.brazil.handler;
029:
030: import sunlabs.brazil.server.Handler;
031: import sunlabs.brazil.server.Request;
032: import sunlabs.brazil.server.Server;
033: import sunlabs.brazil.session.SessionManager;
034: import sunlabs.brazil.util.Format;
035:
036: import java.io.InputStream;
037: import java.util.Properties;
038: import java.util.Hashtable;
039: import java.util.Enumeration;
040: import java.util.StringTokenizer;
041: import java.io.IOException;
042:
043: /**
044: * The <code>BasicAuthHandler</code> obtains a Session ID by performing
045: * "basic" authentication, using either the "Authorization" or the
046: * "Proxy-Authorization" headers. This handler prevents
047: * subsequent downstream handlers from being accessed unless the proper
048: * authentication was seen in the request. The Session ID obtained by this
049: * handler is meant to be used by those downsteams handlers to access
050: * whatever session-dependent information they need.
051: * <p>
052: * If the request does not contain the authentication headers or the
053: * authentication information is not valid, this handler sends an HTTP
054: * error message along with the "WWW-Authenticate" or "Proxy-Authenticate"
055: * header, as appropriate. See
056: * <code><a href=#config.code>code</a></code>,
057: * <code><a href=#config.authorization>authorization</a></code>,
058: * <code><a href=#config.authenticate>authenticate</a></code>
059: * <p>
060: * If the request does contain valid authentication information, the
061: * Session ID associated with the authentication information is inserted
062: * into the request properties, for use by downstream handlers. After
063: * inserting the Session ID, this handler returns <code>false</code> to
064: * allow the downstream handlers to run.
065: * <p>
066: * The set of valid Session IDs is contained either in (1) a static file or
067: * in (2) a globally accessible table managed by the <code>SessionManager</code>.
068: * The second case allows the list of Session IDs to be dynamically
069: * configurable and shareable amongst two or more authentication handlers.
070: * For instance, the web developer could set up one handler to dynamically
071: * populate the shared table with Session IDs based on submitted HTML forms,
072: * and then use the <code>BasicAuthHandler</code> to ensure that all other
073: * requests have a valid Session ID based on the shared table. See
074: * <code><a href=#config.code>mapFile</a></code>,
075: * <code><a href=#config.authorization>session</a></code>,
076: * <code><a href=#config.authenticate>ident</a></code>
077: * <p>
078: * The format of the static file described in case (1) above is a
079: * Java properties file where keys are the Base64 encoded strings obtained
080: * from the Authentication header and the values are the associated Session
081: * IDs. Base64 strings can contain the '=' character, but the keys in a
082: * Java properties file cannot contain an '=' character, so all '=' characters
083: * in the Base64 strings must be converted to '!' in the properties file,
084: * as shown in the following sample properties file:
085: * <pre>
086: * bXIuIGhhdGU6a2ZqYw!! = radion
087: * Zm9vOmJhcg!! = foo
088: * </pre>
089: * The format of the dynamic table described in case (2) above is a
090: * <code>Hashtable</code> where the keys are the Base64 encoded strings
091: * obtained from the Authentication header and the values are the associated
092: * Session IDs. This <code>Hashtable</code> is accessed via the
093: * <code>SessionManager.getSession</code> method, with the <code>session</code>
094: * argument of <code>null</code> and the <code>ident</code> argument
095: * specified by a configuration parameter.
096: * <hr>
097: * There are several different types of authentication possible. All
098: * authentication handlers should follow these basic principles: <ul>
099: * <li> The authentication handler examines some aspect of the request
100: * to decide if the appropriate authentication is present.
101: * <li> If the request is acceptable, the authentication handler should
102: * insert the extracted Session ID into a request property
103: * and then return <code>false</code>, to allow subsequent handlers
104: * to run and perhaps use the Session ID.
105: * <li> If the request is not acceptable, the authentication handler can
106: * return an error message or do some other thing to try to obtain a
107: * valid authentication.
108: * <li> Handlers wishing to be protected by authentication should not
109: * subclass an authentication handler. Instead, such handler should
110: * be written to assume that authentication has already been performed
111: * and then just examine the Session ID present.
112: * The web developer is then responsible for choosing which one (of
113: * possibly many) forms of authentication to use and installing those
114: * authentication handlers before the "sensitive" handler.
115: * <li> Handlers that are protected by an authentication handler can
116: * use the Session ID stored in the request properties regardless of
117: * the specifics of the authentication handler.
118: * </ul>
119: * <pre>
120: * handlers=auth history file
121: *
122: * auth.class=BasicAuthHandler
123: * auth.session=account
124: * auth.message=Go away, you're not allowed here!
125: *
126: * history.class=HistoryHandler
127: * history.session=account
128: *
129: * file.class=FileHandler
130: * file.root=htdocs
131: * </pre>
132: * In the sample pseudo-configuation file specified above, the
133: * <code>BasicAuthHandler</code> is first invoked to see if the HTTP "basic"
134: * authentication header is present in the request. If it isn't, a nasty
135: * message is sent back. If the "basic" authentication header is present
136: * and corresponds to a user that the <code>BasicAuthHandler</code> knows
137: * about, the Session ID associated with that user is stored in the specified
138: * property named "account".
139: * <p>
140: * Subsequently, the <code>HistoryHandler</code> examines its specified
141: * property (also "account") for the Session ID and uses that to keep
142: * track of which session is issuing the HTTP request.
143: * <p>
144: * Each handler that needs a Session ID should have a
145: * configuration parameter that allows the web developer to specify the
146: * name of the request property that holds the Session ID.
147: * Multiple handlers can all use the same request property as each other,
148: * all protected by the same authentication handler.
149: * <hr>
150: * This handler uses the following configuration properties: <dl class=props>
151: *
152: * <dt> <code>prefix</code>
153: * <dd> This handler will attempt to authenticate URLs beginning with
154: * this string only. The default value is "/".
155: *
156: * <a name=config.code> </a>
157: * <dt> <code>code</code>
158: * <dd> The type of authentication to perform. The default value is 401.
159: * <p>
160: * The value 401 corresponds to standard "basic" authentication.
161: * The "Authorization" request header is supposed to contain the
162: * authentication string. If the request was not authenticated, the
163: * "WWW-Authenticate" header is sent in the HTTP error response
164: * to cause the browser to prompt the client to authenticate.
165: * <p>
166: * The value 407 corresponds to "basic" proxy/firewall authentication.
167: * The "Proxy-Authorization" request header is supposed to contain the
168: * authentication string. If the request was not authenticated, the
169: * "Proxy-Authenticate" header is sent in the HTTP error response
170: * to cause the browser to prompt the client to authenticate.
171: * <p>
172: * Any other value may also be specified. Whatever the value, it will
173: * be returned as the HTTP result code of the error message.
174: *
175: * <a name=config.authorization> </a>
176: * <dt> <code>authorization</code>
177: * <dd> If specified, this is the request header that will contain the
178: * "basic" authentication string, instead of the "Authorization"
179: * or "Proxy-Authorization" header implied by <code>code</code>.
180: *
181: * <a name=config.authenticate> </a>
182: * <dt> <code>authenticate</code>
183: * <dd> If specified, this is the response header that will be sent in the
184: * HTTP error response if the user is not authenticated.
185: * <p>
186: * If this string is "", then this handler will
187: * authenticate the request if the authorization header is present,
188: * but <b>will not</b> send an HTTP error message if the request could
189: * not be authenticated. This is useful if the web developer wants to
190: * do something more complex (such as invoking an arbitrary set of
191: * handlers) instead of just sending a simple error message if the
192: * request was not authenticated. In this case, the web developer can
193: * determine that the request was not authenticated because <b>no</b>
194: * Session ID will be present in the request properties.
195: *
196: * <dt> <code>realm</code>
197: * <dd> The "realm" of the HTTP authentication error message. This is a
198: * string that the browser is supposed to present to the client when
199: * asking the client the authenticate. It provides a human-friendly
200: * name describing who wants the authentication.
201: *
202: * <dt> <code>message</code>
203: * <dd> The body of the HTTP authentication error message. This will be
204: * displayed by the browser if the client chooses not to authenticate.
205: * The default value is "". Patterns of the form <i>${xxx}</i> are
206: * replaced with the value of the <i>xxx</i>
207: * entry of <code>request.props</code>.
208: *
209: * <dt> <code>mapFile</code>
210: * <dd> If specified, this is the Session ID file. This is expected to be
211: * a java properties file, whose keys are the authentication tokens,
212: * and whose values are the Session IDs that are inserted into the
213: * request properties.
214: * <p>
215: * The keys in the file are basic authentication (base64) tokens with
216: * any trailing <code>"="</code> characters changed to <code>"!"</code>.
217: *
218: * <dt> <code>session</code>
219: * <dd> The name of the request property that the Session ID will be stored
220: * in, to be passed to downstream handlers. The default value is
221: * "SessionID".
222: *
223: * <dt> <code>ident</code>
224: * <dd> The <code>ident</code> argument to {@link SessionManager#getSession}
225: * to get the table of valid sessions. The default value is
226: * "authorized".
227: * </dl>
228: *
229: * @author Stephen Uhler (stephen.uhler@sun.com)
230: * @author Colin Stevens (colin.stevens@sun.com)
231: * @version 1.27, 01/01/12
232: */
233: public class BasicAuthHandler implements Handler {
234: private static final String PREFIX = "prefix";
235: private static final String CODE = "code";
236: private static final String AUTHORIZATION = "authorization";
237: private static final String AUTHENTICATE = "authenticate";
238: private static final String REALM = "realm";
239: private static final String MESSAGE = "message";
240: private static final String MAP_FILE = "mapFile";
241: private static final String SESSION = "session";
242: private static final String IDENT = "ident";
243:
244: public String prefix = "/";
245: public int code = 401;
246: public String authorization = "Authorization";
247: public String authenticate = "WWW-Authenticate";
248: public String realm = "realm";
249: public String message = "Invalid credentials supplied";
250: public String mapFile = null;
251: public String session = "SessionID";
252: public String ident = "authorized";
253:
254: String propsPrefix;
255:
256: public Properties map; // The authorization mapping table
257:
258: /**
259: * Initializes this handler. It is an error if the <code>mapFile</code>
260: * parameter is specified but that file cannot be loaded.
261: *
262: * @param server
263: * The HTTP server that created this handler.
264: *
265: * @param prefix
266: * A prefix to prepend to all of the keys that this
267: * handler uses to extract configuration information.
268: *
269: * @return <code>true</code> if this <code>Handler</code> initialized
270: * successfully, <code>false</code> otherwise.
271: */
272: public boolean init(Server server, String propsPrefix) {
273: Properties props = server.props;
274:
275: this .prefix = props.getProperty(propsPrefix + PREFIX,
276: this .prefix);
277: this .propsPrefix = propsPrefix;
278: try {
279: String str = props.getProperty(propsPrefix + CODE);
280: code = Integer.decode(str).intValue();
281: } catch (Exception e) {
282: }
283:
284: if (code == 407) {
285: authorization = "Proxy-Authorization";
286: authenticate = "Proxy-Authenticate";
287: }
288: authorization = props.getProperty(propsPrefix + AUTHORIZATION,
289: authorization);
290: authenticate = props.getProperty(propsPrefix + AUTHENTICATE,
291: authenticate);
292: realm = props.getProperty(propsPrefix + REALM, realm);
293: message = props.getProperty(propsPrefix + MESSAGE, message);
294: mapFile = props.getProperty(propsPrefix + MAP_FILE, mapFile);
295: session = props.getProperty(propsPrefix + SESSION, session);
296: ident = props.getProperty(propsPrefix + IDENT, ident);
297:
298: try {
299: if (message.startsWith("@")) {
300: message = ResourceHandler.getResourceString(
301: server.props, propsPrefix, message);
302: }
303: } catch (IOException e) {
304: server.log(Server.LOG_ERROR, propsPrefix,
305: "Can't get \"denied\" message");
306: }
307:
308: if (mapFile != null)
309: try {
310: server.log(Server.LOG_DIAGNOSTIC, propsPrefix,
311: "Loading credentials file " + mapFile);
312: InputStream in = ResourceHandler.getResourceStream(
313: server.props, propsPrefix, mapFile);
314: Properties p = new Properties();
315: p.load(in);
316: in.close();
317:
318: map = new Properties();
319: Enumeration keys = p.keys();
320: while (keys.hasMoreElements()) {
321: String key = (String) keys.nextElement();
322: String value = (String) p.get(key);
323: map.put(key.replace('!', '='), value);
324: }
325: } catch (Exception e) {
326: server.log(Server.LOG_DIAGNOSTIC, propsPrefix,
327: "Credentials file (" + mapFile
328: + ") not available: " + e);
329: }
330: mapFile = null;
331: return true;
332: }
333:
334: /**
335: * Looks up the credentials for this request, and insert them into the
336: * request stream. If no credentials are found, prompt the user for them.
337: */
338:
339: public boolean respond(Request request) throws IOException {
340: if (request.url.startsWith(prefix) == false) {
341: return false;
342: }
343:
344: String auth = request.headers.get(authorization);
345: if (auth == null) {
346: return complain(request, "Missing http header: "
347: + authorization);
348: }
349:
350: try {
351: /*
352: * Strip off "basic" at beginning of value.
353: */
354:
355: StringTokenizer st = new StringTokenizer(auth);
356: if ("basic".equalsIgnoreCase(st.nextToken()) == false) {
357: return complain(request, "Non-basic realm: " + auth);
358: }
359:
360: auth = st.nextToken();
361:
362: String id;
363: if (map == null) {
364: Hashtable h = (Hashtable) SessionManager.getSession(
365: null, ident, Hashtable.class);
366: id = (String) h.get(auth);
367: } else {
368: id = map.getProperty(auth);
369: }
370: if (id == null) {
371: return complain(request, "no id matching: " + auth);
372: }
373:
374: /*
375: * Found registered user, so add user to request properties.
376: */
377: request.props.put(session, id);
378: request.props.put("gotCookie", "true");
379: request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Setting "
380: + session + " to: " + id);
381: return false;
382: } catch (Exception e) {
383: /* Malformed authorization. */
384: return complain(request, e.toString());
385: }
386: }
387:
388: /**
389: * Authentication failed.
390: * Send the appropriate authentication required header as a response.
391: * @param request The request to respond to
392: * @param reason The reason for failure (for diagnostics)
393: * @returns True
394: */
395:
396: public boolean complain(Request request, String reason)
397: throws IOException {
398: if (authenticate.length() == 0) {
399: request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
400: "no authenticate?");
401: return false;
402: }
403: request
404: .addHeader(authenticate, "basic realm=\"" + realm
405: + "\"");
406: request.sendResponse(Format.subst(request.props, message),
407: "text/html", code);
408: request.log(Server.LOG_DIAGNOSTIC, propsPrefix, reason);
409: return true;
410: }
411: }
|