--- /dev/null
+// DigestAuthFilter.java\r
+// $Id: DigestAuthFilter.java,v 1.2 2010/06/15 17:53:03 smhuang Exp $\r
+// (c) COPYRIGHT MIT, INRIA and Keio, 1999.\r
+// Please first read the full copyright statement in file COPYRIGHT.html\r
+\r
+package org.w3c.jigsaw.auth;\r
+\r
+import java.util.Date;\r
+import java.security.MessageDigest;\r
+import java.security.NoSuchAlgorithmException;\r
+import org.w3c.tools.resources.Attribute;\r
+import org.w3c.tools.resources.AttributeRegistry;\r
+import org.w3c.tools.resources.FramedResource;\r
+import org.w3c.tools.resources.IntegerAttribute;\r
+import org.w3c.tools.resources.InvalidResourceException;\r
+import org.w3c.tools.resources.ProtocolException;\r
+import org.w3c.tools.resources.ReplyInterface;\r
+import org.w3c.tools.resources.RequestInterface;\r
+import org.w3c.tools.resources.ResourceReference;\r
+import org.w3c.tools.resources.StringArrayAttribute;\r
+import org.w3c.tools.resources.StringAttribute;\r
+import org.w3c.jigsaw.http.Client;\r
+import org.w3c.jigsaw.http.HTTPException;\r
+import org.w3c.jigsaw.http.Reply;\r
+import org.w3c.jigsaw.http.Request;\r
+import org.w3c.jigsaw.http.httpd;\r
+import org.w3c.jigsaw.html.HtmlGenerator;\r
+import org.w3c.www.http.HTTP;\r
+import org.w3c.www.http.HttpChallenge;\r
+import org.w3c.www.http.HttpCredential;\r
+import org.w3c.www.http.HttpFactory;\r
+import org.w3c.jigsaw.frames.HTTPFrame;\r
+\r
+import org.w3c.util.StringUtils;\r
+\r
+import org.w3c.tools.resources.ProtocolException;\r
+\r
+/**\r
+ * Internal exception class\r
+ */\r
+\r
+class DigestAuthFilterException extends Exception {\r
+\r
+ DigestAuthFilterException (String msg) {\r
+ super (msg);\r
+ }\r
+}\r
+\r
+/**\r
+ * DigestAuthFilter provides for both IP and digest authentication.\r
+ * This is really a first implementation. It looses on several points:\r
+ * <ul>\r
+ * <li>AuthUser instances, being a subclass of resource dump their classes\r
+ * along with their attributes, although here we know that they will all\r
+ * be instances of AuthUser.\r
+ * <li>The way the ipmatcher is maintained doesn't make much sense.\r
+ * <li>The way groups are handled is no good.\r
+ * <li>The SimpleResourceStore is not an adequat store for the user database,\r
+ * it should rather use the jdbmResourceStore (not written yet).\r
+ * </ul>\r
+ * However, this provides for the digest functionnalities.\r
+ */\r
+\r
+public class DigestAuthFilter extends AuthFilter {\r
+\r
+ public class DigestAuthContext {\r
+ String dac_user = null;\r
+ String dac_realm = null;\r
+ String dac_nonce = null;\r
+ String dac_uri = null;\r
+ String dac_response = null;\r
+ String dac_algorithm = null;\r
+ String dac_method = null;\r
+ boolean stale = false;\r
+ // qop and nc may be added at some point\r
+ // maybe not as it implies session tracking\r
+\r
+ DigestAuthContext (Request request)\r
+ throws DigestAuthFilterException, ProtocolException\r
+ {\r
+ HttpCredential credential = null;\r
+ \r
+ credential = (request.isProxy()\r
+ ? request.getProxyAuthorization()\r
+ : request.getAuthorization());\r
+ if ( ! credential.getScheme().equalsIgnoreCase("Digest") ) {\r
+ String msg = ("Invalid authentication scheme \""\r
+ + credential.getScheme()\r
+ + " expecting \"Digest\"");\r
+ throw new DigestAuthFilterException (msg) ;\r
+ }\r
+ // now split things and decode things\r
+ dac_user = credential.getAuthParameter("username");\r
+ dac_uri = credential.getAuthParameter("uri");\r
+ dac_response = credential.getAuthParameter("response");\r
+ dac_realm = credential.getAuthParameter("realm");\r
+ dac_method = request.getMethod();\r
+ dac_nonce = credential.getAuthParameter("nonce");\r
+ if (dac_user == null || dac_uri == null || dac_response == null ||\r
+ dac_realm == null) {\r
+ String msg = ("Invalid authentication header");\r
+ throw new DigestAuthFilterException(msg);\r
+ }\r
+ }\r
+ \r
+ boolean authenticate(String username, String realm, String passwd)\r
+ {\r
+ stale = false;\r
+ if (!dac_user.equals(username))\r
+ return false;\r
+ if (!dac_realm.equals(realm))\r
+ return false;\r
+ if (dac_algorithm != null && !dac_algorithm.equals(getAlgorithm()))\r
+ return false;\r
+ if (!dac_nonce.equals(nonce)) {\r
+ if (!dac_nonce.equals(old_nonce)) {\r
+ // check if the user knows the right passwd\r
+ String a1, a2, ha1, ha2;\r
+ a1 = username + ":" + realm + ":" + passwd;\r
+ a2 = dac_method + ":" + dac_uri;\r
+ MessageDigest md = null;\r
+ try {\r
+ md = MessageDigest.getInstance(getAlgorithm());\r
+ } catch (NoSuchAlgorithmException algex) {\r
+ // fatal error, can't authenticate\r
+ return false;\r
+ }\r
+ md.update(a1.getBytes());\r
+ ha1 = StringUtils.toHexString(md.digest());\r
+ md.reset();\r
+ md.update(a2.getBytes());\r
+ ha2 = StringUtils.toHexString(md.digest());\r
+ md.reset();\r
+ String kd, hkd;\r
+ // KD( H(A1), unq(nonce-value) ":" H(A2)\r
+ kd = ha1 + ":" + dac_nonce + ":" + ha2;\r
+ md.update(kd.getBytes());\r
+ hkd = StringUtils.toHexString(md.digest());\r
+ stale = hkd.equals(dac_response);\r
+ return false;\r
+ } else\r
+ stale = true;\r
+ }\r
+ // basic things have been checked... now try the real thing\r
+ String a1, a2, ha1, ha2;\r
+ a1 = username + ":" + realm + ":" + passwd;\r
+ a2 = dac_method + ":" + dac_uri;\r
+ MessageDigest md = null;\r
+ try {\r
+ md = MessageDigest.getInstance(getAlgorithm());\r
+ } catch (NoSuchAlgorithmException algex) {\r
+ // fatal error, can't authenticate\r
+ return false;\r
+ }\r
+ md.update(a1.getBytes());\r
+ ha1 = StringUtils.toHexString(md.digest());\r
+ md.reset();\r
+ md.update(a2.getBytes());\r
+ ha2 = StringUtils.toHexString(md.digest());\r
+ md.reset();\r
+ String kd, hkd;\r
+ if (stale) // KD( H(A1), unq(nonce-value) ":" H(A2)\r
+ kd = ha1 + ":" + old_nonce + ":" + ha2;\r
+ else\r
+ kd = ha1 + ":" + nonce + ":" + ha2;\r
+ md.update(kd.getBytes());\r
+ hkd = StringUtils.toHexString(md.digest());\r
+ if (!hkd.equals(dac_response))\r
+ return false;\r
+ // yeah!!!\r
+ return true;\r
+ }\r
+ }\r
+ \r
+ /**\r
+ * Attribute index - The list of allowed users.\r
+ */\r
+ protected static int ATTR_ALLOWED_USERS = -1 ;\r
+ /**\r
+ * Attribute index - The list of allowed groups.\r
+ */\r
+ protected static int ATTR_ALLOWED_GROUPS = -1 ;\r
+ /**\r
+ * Attribute index - The algorithm used\r
+ */\r
+ protected static int ATTR_ALGORITHM = -1 ;\r
+ /**\r
+ * Attribute index - The nonce time to live (in seconds)\r
+ */\r
+ protected static int ATTR_NONCE_TTL = -1 ;\r
+\r
+ static {\r
+ Attribute a = null ;\r
+ Class c = null ;\r
+ try {\r
+ c = Class.forName("org.w3c.jigsaw.auth.DigestAuthFilter");\r
+ //Added by Jeff Huang\r
+ //TODO: FIXIT\r
+ } catch (Exception ex) {\r
+ ex.printStackTrace() ;\r
+ System.exit(1) ;\r
+ }\r
+ // The list of allowed users\r
+ a = new StringArrayAttribute("users"\r
+ , null\r
+ , Attribute.EDITABLE) ;\r
+ ATTR_ALLOWED_USERS = AttributeRegistry.registerAttribute(c, a) ;\r
+ // The list of allowed groups:\r
+ a = new StringArrayAttribute("groups"\r
+ , null\r
+ , Attribute.EDITABLE);\r
+ ATTR_ALLOWED_GROUPS = AttributeRegistry.registerAttribute(c, a) ;\r
+ // The algorithm used for digest and checksum\r
+ a = new StringAttribute("algorithm"\r
+ , null\r
+ , Attribute.EDITABLE);\r
+ ATTR_ALGORITHM = AttributeRegistry.registerAttribute(c, a) ;\r
+ a = new IntegerAttribute("nonce_ttl"\r
+ , new Integer(300)\r
+ , Attribute.EDITABLE);\r
+ ATTR_NONCE_TTL = AttributeRegistry.registerAttribute(c, a) ;\r
+ }\r
+\r
+ /**\r
+ * The catalog of realms that make our scope.\r
+ */\r
+ protected RealmsCatalog catalog = null ;\r
+ /**\r
+ * Our associated realm.\r
+ */\r
+ protected ResourceReference rr_realm = null ;\r
+ /**\r
+ * The nam of the realm we cache in <code>realm</code>.\r
+ */\r
+ protected String loaded_realm = null ;\r
+\r
+ /**\r
+ * The challenge to issue to any client for Digest Authentication.\r
+ */\r
+ protected HttpChallenge challenge = null;\r
+\r
+ /**\r
+ * The nonce value of the digest, changed every X mn\r
+ */\r
+ protected String nonce = null;\r
+ /**\r
+ * The previous nonce value of the digest, changed every X mn\r
+ */\r
+ protected String old_nonce = null;\r
+\r
+ private long prev_date = 0;\r
+ private int nonce_ttl = 600; /* 10mn by default */\r
+ /**\r
+ * Get a pointer to our realm, and initialize our ipmatcher.\r
+ */\r
+\r
+ protected synchronized void acquireRealm() {\r
+ // Get our catalog:\r
+ if ( catalog == null ) {\r
+ httpd server = (httpd) \r
+ ((FramedResource) getTargetResource()).getServer() ;\r
+ catalog = server.getRealmsCatalog() ;\r
+ }\r
+ // Check that our realm name is valid:\r
+ String name = getRealm() ;\r
+ if ( name == null )\r
+ return ;\r
+ if ((rr_realm != null) && name.equals(loaded_realm)) \r
+ return ;\r
+ // Load the realm and create the ipmtacher object\r
+ rr_realm = catalog.loadRealm(name) ;\r
+ loaded_realm = name;\r
+ }\r
+\r
+ /**\r
+ * Check that our realm does exist.\r
+ * Otherwise we are probably being initialized, and we don't authenticate\r
+ * yet.\r
+ * @return A boolean <strong>true</strong> if realm can be initialized.\r
+ */\r
+\r
+ protected synchronized boolean checkRealm() {\r
+ acquireRealm() ;\r
+ return true;// (ipmatcher != null) ;\r
+ }\r
+\r
+ /**\r
+ * Get the list of allowed users.\r
+ */\r
+\r
+ public String[] getAllowedUsers() {\r
+ return (String[]) getValue(ATTR_ALLOWED_USERS, null) ;\r
+ }\r
+\r
+ /**\r
+ * Get the list of allowed groups.\r
+ */\r
+\r
+ public String[] getAllowedGroups() {\r
+ return (String[]) getValue(ATTR_ALLOWED_GROUPS, null) ;\r
+ }\r
+\r
+ /**\r
+ * Get the algorithm used\r
+ */\r
+\r
+ public String getAlgorithm() {\r
+ return (String) getValue(ATTR_ALGORITHM, "MD5") ;\r
+ }\r
+\r
+ /**\r
+ * Lookup a user by its name.\r
+ * @param name The user's name.\r
+ * @return An AuthUser instance, or <strong>null</strong>.\r
+ */\r
+\r
+ public synchronized ResourceReference lookupUser (String name) {\r
+ if ( rr_realm == null )\r
+ acquireRealm() ;\r
+ try {\r
+ AuthRealm realm = (AuthRealm) rr_realm.lock();\r
+ return realm.loadUser(name) ;\r
+ } catch (InvalidResourceException ex) {\r
+ return null;\r
+ } finally {\r
+ rr_realm.unlock();\r
+ }\r
+ }\r
+\r
+ /*\r
+ * Is this user allowed in the realm ?\r
+ * First check in the list of allowed users (if any), than in the list\r
+ * of allowed groups (if any). If no allowed users or allowed groups\r
+ * are defined, than simply check for the existence of this user.\r
+ * @return A boolean <strong>true</strong> if access allowed.\r
+ */\r
+\r
+ protected boolean checkUser(AuthUser user) {\r
+ String allowed_users[] = getAllowedUsers() ;\r
+ // Check in the list of allowed users:\r
+ if ( allowed_users != null ) {\r
+ for (int i = 0 ; i < allowed_users.length ; i++) {\r
+ if (allowed_users[i].equals(user.getName()))\r
+ return true ;\r
+ }\r
+ }\r
+ // Check in the list of allowed groups:\r
+ String allowed_groups[] = getAllowedGroups() ;\r
+ if ( allowed_groups != null ) {\r
+ String ugroups[] = user.getGroups() ;\r
+ if ( ugroups != null ) {\r
+ for (int i = 0 ; i < ugroups.length ; i++) {\r
+ for (int j = 0 ; j < allowed_groups.length ; j++) {\r
+ if ( allowed_groups[j].equals(ugroups[i]) ) \r
+ return true ;\r
+ }\r
+ }\r
+ }\r
+ }\r
+ // If no users or groups specified, return true\r
+ if ((allowed_users == null) && (allowed_groups == null)) \r
+ return true ;\r
+ return false ;\r
+ }\r
+\r
+ /**\r
+ * Catch set value on the realm, to maintain cached values.\r
+ */\r
+\r
+ public void setValue(int idx, Object value) {\r
+ super.setValue(idx, value);\r
+ if ( idx == ATTR_REALM ) {\r
+ // Initialize the filter challenge:\r
+ challenge = HttpFactory.makeChallenge("Digest");\r
+ challenge.setAuthParameter("realm", getRealm());\r
+ }\r
+ if ( idx == ATTR_NONCE_TTL ) {\r
+ if ( value instanceof Integer)\r
+ nonce_ttl = ((Integer) value).intValue();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Authenticate the given request.\r
+ * We first check for valid authentication information. If no \r
+ * authentication is provided, than we try to map the IP address to some\r
+ * of the ones we know about. If the IP address is not found, we challenge\r
+ * the client for a password.\r
+ * <p>If the IP address is found, than either our user entry requires an\r
+ * extra password step (in wich case we challenge it), or simple IP\r
+ * based authentication is enough, so we allow the request.\r
+ * @param request The request to be authentified.\r
+ * @exception org.w3c.tools.resources.ProtocolException if authentication\r
+ * failed\r
+ */\r
+\r
+ public void authenticate (Request request) \r
+ throws ProtocolException \r
+ {\r
+ // Are we being edited ?\r
+ if ( ! checkRealm() )\r
+ return ;\r
+ // Internal requests always allowed:\r
+ Client client = request.getClient() ;\r
+ if ( client == null )\r
+ return ;\r
+ // check for nonce validity\r
+ Date d = new Date();\r
+ if ((d.getTime() - prev_date) / 1000 > nonce_ttl) {\r
+ prev_date = d.getTime();\r
+ updateNonce();\r
+ }\r
+ DigestAuthContext dac = null;\r
+\r
+ // Check authentication according to auth method:\r
+ if ((request.hasAuthorization() && ! request.isProxy())\r
+ || (request.isProxy() && request.hasProxyAuthorization())) {\r
+ try {\r
+ dac = new DigestAuthContext(request);\r
+ } catch (DigestAuthFilterException ex) {\r
+ dac = null;\r
+ }\r
+ if (dac != null) {\r
+ ResourceReference rr_user = \r
+ (ResourceReference)lookupUser(dac.dac_user) ;\r
+ try {\r
+ AuthUser user = (AuthUser) rr_user.lock();\r
+ // This user doesn't even exists !\r
+ if ( user != null ) {\r
+ // If it has a password check it\r
+ if (user.definesAttribute("password") ) {\r
+ if (dac.authenticate(user.getName(), \r
+ loaded_realm, \r
+ user.getPassword())) {\r
+ request.setState(STATE_AUTHUSER, dac.dac_user);\r
+ request.setState(STATE_AUTHTYPE, "Digest");\r
+ request.setState(STATE_AUTHCONTEXT, dac);\r
+ return;\r
+ }\r
+ }\r
+ }\r
+ } catch (InvalidResourceException ex) {\r
+ } finally {\r
+ rr_user.unlock();\r
+ }\r
+ }\r
+ }\r
+ \r
+ // Every possible scheme has failed for this request, emit an error\r
+ Reply e = null;\r
+ HttpChallenge new_c;\r
+ if (dac != null && dac.stale) {\r
+ new_c = challenge.getClone();\r
+ if (new_c != null)\r
+ new_c.setAuthParameter("stale","true", false);\r
+ else\r
+ new_c = challenge;\r
+ } else\r
+ new_c = challenge;\r
+ if ( request.isProxy() ) {\r
+ e = request.makeReply(HTTP.PROXY_AUTH_REQUIRED);\r
+ e.setProxyAuthenticate(new_c);\r
+ } else {\r
+ e = request.makeReply(HTTP.UNAUTHORIZED);\r
+ e.setWWWAuthenticate (new_c);\r
+ }\r
+ HtmlGenerator g = new HtmlGenerator("Unauthorized");\r
+ g.append ("<h1>Unauthorized access</h1>"\r
+ + "<p>You are denied access to this resource.");\r
+ e.setStream(g);\r
+ throw new HTTPException (e);\r
+ }\r
+\r
+ /**\r
+ * update the nonce string\r
+ */\r
+\r
+ private void updateNonce() {\r
+ updateNonce(getResource());\r
+ }\r
+\r
+ private synchronized void updateNonce(FramedResource fr) {\r
+ HTTPFrame htf;\r
+ if (fr instanceof HTTPFrame) {\r
+ htf = (HTTPFrame) fr;\r
+ try {\r
+ MessageDigest md = MessageDigest.getInstance(getAlgorithm());\r
+ md.update((new Date()).toString().getBytes());\r
+ try {\r
+ md.update(htf.getETag().getTag().getBytes());\r
+ } catch (Exception ex) {\r
+ // hum... try without it\r
+ md.update(htf.getURLPath().getBytes());\r
+ }\r
+ byte b[] = md.digest();\r
+ if (nonce != null) \r
+ old_nonce = nonce;\r
+ nonce = StringUtils.toHexString(b);\r
+ challenge.setAuthParameter("nonce", nonce);\r
+ } catch (NoSuchAlgorithmException algex) {\r
+ // bad algorithm, prevent access by firing an error\r
+/* Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;\r
+ error.setContent("The algorithm specified in the "+\r
+ "DigestAuthFilterprocess filter "+\r
+ "is not available, you are then unable to "+\r
+ "access protected space");\r
+ throw new HTTPException(error);\r
+*/\r
+ }\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Add the appropriate cache control directives on the way back.\r
+ * @param request The request that has been processed.\r
+ * @param reply The original reply.\r
+ * @return Always <strong>null</strong>.\r
+ */\r
+\r
+ public ReplyInterface outgoingFilter(RequestInterface request,\r
+ ReplyInterface reply) \r
+ {\r
+ Request req = (Request) request;\r
+ Reply rep = (Reply) reply;\r
+ if ( getPrivateCachability() ) {\r
+ rep.setMustRevalidate(true);\r
+ } else if ( getSharedCachability() ) {\r
+ rep.setProxyRevalidate(true);\r
+ } else if ( getPublicCachability() ) {\r
+ rep.setPublic(true);\r
+ }\r
+ if (req.hasState(AuthFilter.STATE_AUTHCONTEXT)) {\r
+ DigestAuthContext dac;\r
+ dac =(DigestAuthContext)req.getState(AuthFilter.STATE_AUTHCONTEXT);\r
+ if (dac.stale) {\r
+ rep.addAuthenticationInfo("nextnonce", nonce);\r
+ }\r
+ }\r
+ return null;\r
+ }\r
+\r
+ /**\r
+ * Initialize the filter.\r
+ */\r
+\r
+ public void initialize(Object values[]) {\r
+ super.initialize(values) ;\r
+ if ( getRealm() != null ) {\r
+ // Initialize the filter challenge:\r
+ challenge = HttpFactory.makeChallenge("Digest");\r
+ challenge.setAuthParameter("realm", getRealm());\r
+ updateNonce();\r
+ challenge.setAuthParameter("domain", getURLPath());\r
+ challenge.setAuthParameter("algorithm", getAlgorithm(), false);\r
+ }\r
+ }\r
+}\r