001: /*
002: * SunNetAuthHandler.java
003: *
004: * Brazil project web application Framework,
005: * export version: 1.1
006: * Copyright (c) 1998-2000 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, rinaldo, suhler.
022: *
023: * Version: 1.29
024: * Created by suhler on 98/09/30
025: * Last modified by suhler on 00/12/11 20:23:22
026: */
027:
028: package sunlabs.brazil.handler;
029:
030: import java.io.File;
031: import java.io.FileInputStream;
032: import java.io.IOException;
033: import java.util.Enumeration;
034: import java.util.Hashtable;
035: import java.util.Random;
036: import java.util.StringTokenizer;
037: import java.util.Vector;
038: import sunlabs.brazil.server.FileHandler;
039: import sunlabs.brazil.server.Handler;
040: import sunlabs.brazil.server.Request;
041: import sunlabs.brazil.server.Server;
042:
043: /**
044: * All-in-one Handler for doing supplier.net style authentication.
045: * <p>
046: * The purpose of this handler is to provide an authenticated "front end"
047: * to one or more web sites, using (hopefully) arbitrary challenge-
048: * response based authentication via a plug-in authentication interface.
049: * It can bridge disparate DNS domains by selectively mapping servers
050: * on one domain into another, based on the supplied credentials, by using
051: * the {@link MultiProxyHandler}.
052: * <p>
053: * The authentication step is expected to yield a list of roles, each of
054: * which represents permission to access a specific foreign site.
055: * Once authentication is complete, and the roles are obtained,
056: * the handler keeps a set of credentials (a lease)
057: * on behalf of the user, which can be tuned at setup time for a variety
058: * of expiration conditions. Once a lease expires, re-authentication is required.
059: * <p>
060: * This handler starts two sets of handlers of its own, an authentication handler -
061: * responsible for doing the authentication, and one of more virtual
062: * proxy handlers - one for each possible role. In the current
063: * implementation, the authentication handler is specified and a configuration
064: * property, and the proxy handlers are all instances of
065: * {@link MultiProxyHandler}, one per role.
066: * <p>
067: * Operation of the handler proceeds in the following steps:
068: * <ol>
069: * <li> When the server starts, the handler is initialized.
070: * <ul>
071: * <li>The template file is located and read.
072: * <li>One {@link MultiProxyHandler} is started for each possible role
073: * <li>The Authentication handler is started. Its operation is defined below.
074: * </ul>
075: * <li> Browser cookies are used as a reference to the user's credentials. If
076: * the cookie returned by the browser refers to a valid credential,
077: * the requested url is compared to the user's roles. If the requested
078: * URL is permitted, by matching one of the users's roles, the URL
079: * is forwarded to the proper virtual web site for delivery. Otherwise
080: * the URL is considered "not found".
081: * <li> If the credentials are not valid, either because thay had expired,
082: * were removed, or there is no browser cookie, the authentication
083: * sequence is started, for the purpose of obtaining valid credentials.
084: * <ul>
085: * <li> A browser cookie is chosen at random, and a "set-cookie" request
086: * is sent to the client (in lieu of the URL requested) along with
087: * the login template. An additional random value is created, retained
088: * by the handler on behalf of this client, and made available as a
089: * parameter to the login template.
090: * <li> The next response from the client is expected to contain the
091: * information required to authenticate the client. This is normally
092: * accomplished by having the user fill out the form that is contained
093: * on the login template, and clicking the submit button.
094: * <li> The client's response (e.g. query data), along with the random number
095: * generated in the previous step, are forwarded to the authentication
096: * handler.
097: * <li> The authentication handler is expected to place a user id and a list
098: * of roles in the resulting request object if authentication is successful
099: * or an error message otherwise. If the authentication suceeds, the
100: * roles are entered into the lease, and the original URL processing
101: * is resumed. If instead an error is returned, the authentication
102: * sequence is repeated. The error message is may be displayed
103: * to the user if it is included as a parameter on the login template.
104: * </ul>
105: * </ol>
106: * <p>
107: * The login template is ordinary HTML, except contructs of the form:
108: * <pre>
109: * <insert property=xx default=yy>
110: * </pre>
111: * may be used to substitute
112: * {@link sunlabs.brazil.server.Request#props}
113: * into the template. The properties <code>challenge</code> and
114: * <code>Message</code> are automatically set to indicate the random
115: * challange and error message (if any) from a previous attempt, respectively.
116: * <p>
117: * The following configuration parameters are recgnized:
118: *
119: * <dl class=props>
120: * <dt>prefix <dd> URL prefix for proxy
121: * <dt>authenticate <dd> URL for authentication page
122: * <dt>cookie <dd> name of the cookie
123: * <dt>roles <dd> list of roles
124: * <dt>proxy <dd> prefix for proxy handler
125: * <dt>idName <dd> property key for token id
126: * <dt>roleName <dd> property key for token roles
127: * <dt>maxIdle <dd> maximum idle time for token (seconds)
128: * <dt>maxAge <dd> maximum total age for token (seconds)
129: * <dt>maxUses <dd> maximum total uses for token
130: * <dt>exit <dd> prefix to exit a session
131: * <dt>all <dd> "free" directory suffixes
132: * <dt>template <dd> login template
133: *</dl>
134: *
135: * Currently, the "sunlabs.brazil.handler.MultiProxyHandler" class
136: * is called to do the actual proxying.
137: * <i>(There should be a link to a sample config file for this one)</i>
138: * <p>
139: * NOTE: This handler is included for historical purposes. It should be
140: * upated to take advantage of features not available when it was first written.
141: *
142: * @author Stephen Uhler
143: * @version 1.29, 00/12/11
144: */
145:
146: public class SunNetAuthHandler implements Handler {
147: Hashtable proxies; // where to keep proxy handlers
148: Handler tokenHandler; // the handler to verify the credentials
149: String propsPrefix; // my prefix into the properties file
150: String UrlPrefix; // The url prefix that triggers this handler
151: String cookieName; // how to name our session cookie
152: String authUrl; // the url for authentication
153: String idKey; // The property name for the token id
154: String roleKey; // The property name for the roles
155: String template; // The file name of the login template
156: String exitString; // Any url containing this string ends a session
157: int maxAge; // max allowable age for token (in seconds)
158: int maxIdle; // length of idle time (in seconds)
159: int maxUses; // max usages for token
160: Vector free; // no authorization required for these
161:
162: static Random random; // our random number generator
163: static final String PREFIX = "prefix"; // URL prefix for proxy
164: static final String AUTH = "authenticate"; // URL for authentication page
165: static final String COOKIE = "cookie"; // name of the cookie
166: static final String ROLES = "roles"; // list of roles
167: static final String PROXY = "proxy"; // prefix for proxy
168: static final String ID_KEY = "idName"; // property key for token id
169: static final String ROLE_KEY = "roleName"; // property key for token roles
170: static final String MAX_IDLE = "maxIdle"; // maximum idle time for token
171: static final String MAX_AGE = "maxAge"; // maximum total age for token
172: static final String MAX_USES = "maxUses"; // maximum total uses for token
173: static final String LOGOUT = "exit"; // prefix to exit a session
174: static final String ALL = "all"; // "free" directory suffixes
175: static final String PROXY_CLASS = "sunlabs.brazil.handler.MultiProxyHandler";
176: static final String TEMPLATE = "template"; // login template
177:
178: /**
179: * Create a "bag" for the handler hashtable
180: */
181:
182: static class RoleData {
183: public String role; // name of the role
184: public Handler handler; // handler reference
185:
186: RoleData(String role, Handler handler) {
187: this .role = role;
188: this .handler = handler;
189: }
190:
191: public String toString() {
192: return role
193: + ": "
194: + (handler == null ? "(null)" : handler.getClass()
195: .getName());
196: }
197: }
198:
199: /**
200: * Set up all of the handlers
201: * - Secure Token Services for authentication
202: * - MultiProxyHandler for dispatching to hosts
203: */
204:
205: public boolean init(Server server, String prefix) {
206: propsPrefix = prefix;
207:
208: /*
209: * Extract the configuration properties
210: * idKey The name of the request property to put the
211: * token id into. [id]
212: * roleKey The name of the request property to put the
213: * list of valid roles for this card into [roles]
214: * urlPrefix The global prefix for this handler [/]
215: * TEMPLATE The name of the login template page. It must have
216: * tags of the form:
217: * <insert property=xx default=yy>
218: * to substitute request properties. Useful
219: * values of xx include "random" and "Message" [login.html]
220: * authUrl The url to send to the token handler. The
221: * query info from the client will be added automatically
222: * cookieName The name to use for the browser cookie [brazil]
223: * roles The list of valid roles for this url []
224: * maxUses The total number of times the browser cookie
225: * may be used in this session
226: * maxIdle The longest interval allowed between uses of this
227: * cookie (seconds)
228: * maxAge Maximum lifetime allowed for this session (seconds)
229: * exitString any url containing this string teminates a session
230: * all Names of sub-directories anyone can access with
231: * no authentication
232: */
233:
234: idKey = server.props.getProperty(propsPrefix + ID_KEY, "id");
235: roleKey = server.props.getProperty(propsPrefix + ROLE_KEY,
236: "roles");
237: UrlPrefix = server.props.getProperty(propsPrefix + PREFIX, "/");
238: authUrl = server.props.getProperty(propsPrefix + AUTH,
239: "/SecureTokenServices/VerifyPin");
240: cookieName = server.props.getProperty(propsPrefix + COOKIE,
241: "brazil");
242: exitString = server.props.getProperty(propsPrefix + LOGOUT,
243: "exit");
244: StringTokenizer roles = new StringTokenizer(server.props
245: .getProperty(propsPrefix + ROLES, ""));
246:
247: StringTokenizer st = new StringTokenizer(server.props
248: .getProperty(propsPrefix + ALL, ""));
249: free = new Vector(st.countTokens());
250: for (int i = 0; i < free.capacity(); i++) {
251: free.addElement(UrlPrefix + st.nextToken());
252: }
253:
254: maxUses = Integer.decode(
255: server.props
256: .getProperty(propsPrefix + MAX_USES, "1000"))
257: .intValue();
258: maxIdle = Integer.decode(
259: server.props
260: .getProperty(propsPrefix + MAX_IDLE, "1200"))
261: .intValue();
262: maxAge = Integer.decode(
263: server.props
264: .getProperty(propsPrefix + MAX_AGE, "86400"))
265: .intValue();
266:
267: server.log(Server.LOG_DIAGNOSTIC, prefix,
268: "Config parameters:\n" + " url prefix: " + UrlPrefix
269: + "\n" + " token URL: " + authUrl + "\n"
270: + " browser cookie: " + cookieName + "\n"
271: + " exit string: " + exitString + "\n"
272: + " roles: " + roles + "\n" + " max uses: "
273: + maxUses + "\n" + " max idle: " + maxIdle
274: + "\n" + " max age: " + maxAge + "\n"
275: + " free dirs: " + free + "\n" + "---");
276:
277: /*
278: * Read in the template file. If it doesn't start with /,
279: * make it relative to the document root
280: */
281:
282: String templateFile = server.props.getProperty(propsPrefix
283: + TEMPLATE, "login.html");
284: try {
285: FileInputStream in;
286:
287: if (!templateFile.startsWith(File.separator)) {
288: templateFile = server.props
289: .getProperty(FileHandler.ROOT)
290: + File.separator + templateFile;
291: }
292: server.log(Server.LOG_DIAGNOSTIC, prefix, "Template: "
293: + templateFile);
294: in = new FileInputStream(templateFile);
295: byte[] contents = new byte[in.available()];
296: in.read(contents);
297: template = new String(contents);
298: in.close();
299: } catch (Exception e) {
300: server.log(Server.LOG_ERROR, prefix,
301: "Can't read template: " + e);
302: return false;
303: }
304:
305: /*
306: * Start the token handler. Set the properties appropriately
307: */
308:
309: try {
310: String tokenName = server.props.getProperty(propsPrefix
311: + "token.class", "");
312: server.log(Server.LOG_DIAGNOSTIC, prefix, "tokenHndlr: "
313: + tokenName);
314: Class tokenClass = Class.forName(tokenName);
315: tokenHandler = (Handler) tokenClass.newInstance();
316: } catch (Exception e) {
317: server.log(Server.LOG_WARNING, prefix,
318: "Error creating token handler: " + e);
319: return false;
320: }
321: if (!tokenHandler.init(server, propsPrefix + "token.")) {
322: server.log(Server.LOG_ERROR, prefix,
323: "token handler won't start");
324: return false;
325: }
326:
327: /*
328: * Kick off all the proxy handlers. Make up a default
329: * prefix for those prefixes that aren't specified.
330: * If there is no handler, then fall through (how XXX)
331: * New strategy: Look for a "host" property, only start the proxy
332: * if "host" is specified, otherwise, skip it!
333: */
334:
335: proxies = new Hashtable(roles.countTokens());
336: while (roles.hasMoreTokens()) {
337: String role = roles.nextToken();
338: System.out.println("Working on role: " + role);
339: String proxyPrefix = propsPrefix + role + ".";
340:
341: if (!server.props.containsKey(proxyPrefix + PREFIX)) {
342: server.props.put(proxyPrefix + PREFIX, UrlPrefix
343: + (UrlPrefix.endsWith("/") ? "" : "/") + role
344: + "/");
345: }
346: server.log(Server.LOG_DIAGNOSTIC, prefix, "role " + role
347: + " prefix: "
348: + server.props.get(proxyPrefix + PREFIX));
349: try {
350: Class proxyClass = Class.forName(PROXY_CLASS);
351: Handler h = (Handler) proxyClass.newInstance();
352: if (h.init(server, proxyPrefix)) {
353: proxies.put(server.props.get(proxyPrefix + PREFIX),
354: new RoleData(role, h));
355: } else {
356: server.log(Server.LOG_WARNING, prefix,
357: "No proxy specified" + " for role: " + role
358: + ", using local directory");
359: proxies.put(server.props.get(proxyPrefix + PREFIX),
360: new RoleData(role, null));
361: }
362: } catch (Exception e) {
363: server.log(Server.LOG_WARNING, prefix,
364: "Error creating handler: " + e);
365: e.printStackTrace();
366: }
367: }
368: if (proxies.size() < 1) {
369: server.log(Server.LOG_ERROR, prefix,
370: "Can't start any proxies");
371: server.log(Server.LOG_ERROR, prefix, " SECURITY ALERT!");
372: return false;
373: }
374: server.log(Server.LOG_DIAGNOSTIC, prefix, "roles: " + proxies);
375: return true;
376: }
377:
378: /**
379: * Act like a "gatekeeper". If we have a valid browser cookie,
380: * Then dispatch to one of the proxies. If not, try to authenticate
381: * by returning the login "template", fetching the credentials, and
382: * establising a session.
383: */
384:
385: public boolean respond(Request request) throws IOException {
386:
387: if (!request.url.startsWith(UrlPrefix)) {
388: request.log(Server.LOG_DIAGNOSTIC, "Handler " + propsPrefix
389: + " ignoring: " + request.url);
390: return false;
391: }
392:
393: /*
394: * Extract the browser cookie - if any
395: */
396:
397: String cookieValue = null;
398: try {
399: String header = (String) request.headers.get("Cookie");
400: System.out.println("HEADERS.GET.COOKIE: " + header);
401: int index = header.indexOf(cookieName + "=");
402: StringTokenizer st = new StringTokenizer(header.substring(
403: index).substring(cookieName.length() + 1));
404: cookieValue = st.nextToken();
405: // Strip off the semicolon if there is one
406: if (cookieValue.endsWith(";")) {
407: cookieValue = cookieValue.substring(0, cookieValue
408: .length() - 1);
409: }
410: request.log(Server.LOG_DIAGNOSTIC, " Got cookie: "
411: + cookieValue);
412: } catch (Exception e) {
413: }
414:
415: /**
416: * Look for session termination.
417: */
418:
419: if (request.url.indexOf(exitString) != -1) {
420: Token.removeToken(cookieValue);
421: request.log(Server.LOG_DIAGNOSTIC, " Session teminated: "
422: + request.url);
423: return false;
424: }
425:
426: /*
427: * See if its a free-be
428: */
429:
430: Enumeration e = free.elements();
431: while (e.hasMoreElements()) {
432: if (request.url.startsWith((String) e.nextElement())) {
433: request.log(Server.LOG_DIAGNOSTIC,
434: " Unprotected url: " + request.url);
435: return false;
436: }
437: }
438:
439: /*
440: * See if this prefix matches one of the roles
441: */
442:
443: Handler handler = null; // the handler for this url
444: String role = null; // the role for this url
445: Enumeration keys = proxies.keys();
446: while (keys.hasMoreElements()) {
447: String key = (String) keys.nextElement();
448: if (request.url.startsWith(key)) {
449: RoleData data = (RoleData) proxies.get(key);
450: handler = data.handler;
451: role = data.role;
452: break;
453: }
454: }
455:
456: if (role == null) {
457: request.log(Server.LOG_DIAGNOSTIC,
458: " No prefix match for: " + request.url);
459: request.sendError(400, "Not found", "No matching role");
460: return true;
461: }
462:
463: /*
464: * Set a browser cookie, if it doesn't exist
465: */
466:
467: if (cookieValue == null) {
468: do {
469: cookieValue = Long.toHexString(random.nextLong());
470: } while (cookieValue.length() < 14);
471: cookieValue = cookieValue.substring(0, 14);
472: request.log(Server.LOG_DIAGNOSTIC, " New cookie: "
473: + cookieValue);
474: request.addHeader("Set-Cookie", cookieName + "="
475: + cookieValue + "; path=" + UrlPrefix);
476: }
477:
478: request.props.put("challenge", cookieValue);
479:
480: /*
481: * No token, Send client the login page. Then the request should
482: * be re-issued by the client with the credentials in the query data.
483: */
484:
485: if (!Token.haveToken(cookieValue)) {
486: Token.getToken(cookieValue); // create a blank token
487: returnLogin(request, "");
488: return true;
489: }
490: Token token = Token.getToken(cookieValue);
491:
492: /*
493: * Have an empty token, Call the STS handler to get the
494: * proper credentials. The client card data should be
495: * in the query data. Make sure we add the challenge to the
496: * query data.
497: *
498: * XXX Technicaly this is incorrect. We need to generate our own request object, instead of
499: * trying to pervert the original one.
500: */
501:
502: if (token.getId() == null) {
503: String save = request.url;
504: request.url = authUrl;
505:
506: if (request.query.length() > 0) {
507: request.query += "&random=" + cookieValue;
508: } else {
509: request.query = "random=" + cookieValue;
510: }
511:
512: request.log(Server.LOG_DIAGNOSTIC,
513: " About to call token handler: " + authUrl
514: + " query: " + request.query + " params: "
515: + request.getQueryData(null) + " post: "
516: + request.postData + " headers: "
517: + request.headers + " request.method:"
518: + request.method);
519:
520: boolean ok = tokenHandler.respond(request);
521: request.log(Server.LOG_DIAGNOSTIC, " result " + ok + " ("
522: + request.props + ")");
523: request.url = save;
524:
525: /*
526: * at this point we should have the credentials in the request.
527: * If not - return to the login page.
528: * If so, remember the credentials in our token object.
529: */
530:
531: String id = (String) request.props.get(idKey);
532: String error = request.props
533: .getProperty("error", "unknown");
534: if (id == null) {
535: request.log(Server.LOG_DIAGNOSTIC, " Can't find: "
536: + idKey + " in request data");
537: returnLogin(request,
538: "No token id found in request data: "
539: + error.substring(error
540: .lastIndexOf(":") + 1));
541: return true;
542: }
543: String roles = (String) request.props.get(roleKey);
544: if (roles == null) {
545: request.log(Server.LOG_DIAGNOSTIC, " Can't find: "
546: + roleKey + " in request data");
547: returnLogin(request, "No roles available for id " + id);
548: return true;
549: }
550: token.setToken(id, roles);
551:
552: /*
553: * Strip off the query data used for token validation.
554: * This should restore the query info that was presented as
555: * part of the original request.
556: */
557:
558: // request.query="";
559: }
560:
561: /*
562: * Have a token, make sure its still valid. If so, call the
563: * proper handler, otherwise redirect to the login page with
564: * the appropriate error message. We should remember the URL,
565: * so we can redirect back here when reauthentication is complete.
566: */
567:
568: if (token.getAge() > maxAge || token.getIdle() > maxIdle
569: || token.getUses() > maxUses) {
570: String message;
571: if (token.getAge() > maxAge) {
572: message = "Session is too old";
573: } else if (token.getIdle() > maxIdle) {
574: message = "Session was idle too long";
575: } else {
576: message = "Session was used up";
577: }
578: Token.removeToken(cookieValue);
579: returnLogin(request, message);
580: return true;
581: }
582: request.log(Server.LOG_DIAGNOSTIC, "Credentials check: "
583: + " age=" + token.getAge() + " idle=" + token.getIdle()
584: + " uses=" + token.getUses());
585:
586: /*
587: * Now check the url against the list of allowed roles
588: */
589:
590: Vector valid = token.getRoles();
591: if (valid.contains(role)) {
592: if (handler != null) {
593: request.log(Server.LOG_DIAGNOSTIC,
594: " dispatching to proxy " + role);
595: return handler.respond(request);
596: } else {
597: request.log(Server.LOG_DIAGNOSTIC,
598: " dispatching next handler");
599: return false;
600: }
601: } else {
602: request.sendError(400, "Not found", "Invalid role");
603: return true;
604: }
605: }
606:
607: /**
608: * return the login page with the appropriate message substituted in
609: */
610:
611: public void returnLogin(Request request, String message) {
612: request.props.put("Message", message);
613: request.log(Server.LOG_DIAGNOSTIC, "sending login page: "
614: + message);
615: String result = processTemplate(template, request.props);
616: try {
617: request.sendResponse(result);
618: } catch (IOException e) {
619: request.log(Server.LOG_ERROR, e.toString());
620: }
621: }
622:
623: /**
624: * Process a template page, and send to the client.
625: * This should be re-done to use the template handler.
626: * Look for html tags of the form:
627: * <insert property=[name] default=[default]>
628: * Also look for:
629: * <param name=[name] value=[value]>
630: * and replace the tag with the value of the request property.
631: * @param template The template to process
632: * @param data The hashtable containing the data to subst
633: */
634:
635: public static String processTemplate(String template, Hashtable data) {
636: // System.out.println(" munge: " + data);
637: HtmlRewriter hr = new HtmlRewriter(template);
638: while (hr.nextTag()) {
639: String tag = hr.getTag();
640: if (tag.equals("insert")) {
641: String property = hr.get("property");
642: String def;
643: if ((property != null) && data.containsKey(property)) {
644: hr.append((String) data.get(property));
645: } else if ((def = hr.get("default")) != null) {
646: hr.append(def);
647: }
648: } else if (tag.equals("param")) {
649: // System.out.println(" param: " + h);
650: String property = hr.get("name");
651: if (data.containsKey(property)) {
652: hr.put("value", (String) data.get(property));
653: }
654: }
655: }
656: return hr.toString();
657: }
658:
659: /**
660: * Seed the random number generator.
661: */
662:
663: static {
664: random = new Random();
665: }
666: }
|