001: // DigestAuthFilter.java
002: // $Id: DigestAuthFilter.java,v 1.7 2000/12/08 12:28:56 ylafon Exp $
003: // (c) COPYRIGHT MIT, INRIA and Keio, 1999.
004: // Please first read the full copyright statement in file COPYRIGHT.html
005:
006: package org.w3c.jigsaw.auth;
007:
008: import java.util.Date;
009: import java.security.MessageDigest;
010: import java.security.NoSuchAlgorithmException;
011: import org.w3c.tools.resources.Attribute;
012: import org.w3c.tools.resources.AttributeRegistry;
013: import org.w3c.tools.resources.FramedResource;
014: import org.w3c.tools.resources.IntegerAttribute;
015: import org.w3c.tools.resources.InvalidResourceException;
016: import org.w3c.tools.resources.ProtocolException;
017: import org.w3c.tools.resources.ReplyInterface;
018: import org.w3c.tools.resources.RequestInterface;
019: import org.w3c.tools.resources.ResourceReference;
020: import org.w3c.tools.resources.StringArrayAttribute;
021: import org.w3c.tools.resources.StringAttribute;
022: import org.w3c.jigsaw.http.Client;
023: import org.w3c.jigsaw.http.HTTPException;
024: import org.w3c.jigsaw.http.Reply;
025: import org.w3c.jigsaw.http.Request;
026: import org.w3c.jigsaw.http.httpd;
027: import org.w3c.jigsaw.html.HtmlGenerator;
028: import org.w3c.www.http.HTTP;
029: import org.w3c.www.http.HttpChallenge;
030: import org.w3c.www.http.HttpCredential;
031: import org.w3c.www.http.HttpFactory;
032: import org.w3c.jigsaw.frames.HTTPFrame;
033:
034: import org.w3c.util.StringUtils;
035:
036: import org.w3c.tools.resources.ProtocolException;
037:
038: /**
039: * Internal exception class
040: */
041:
042: class DigestAuthFilterException extends Exception {
043:
044: DigestAuthFilterException(String msg) {
045: super (msg);
046: }
047: }
048:
049: /**
050: * DigestAuthFilter provides for both IP and digest authentication.
051: * This is really a first implementation. It looses on several points:
052: * <ul>
053: * <li>AuthUser instances, being a subclass of resource dump their classes
054: * along with their attributes, although here we know that they will all
055: * be instances of AuthUser.
056: * <li>The way the ipmatcher is maintained doesn't make much sense.
057: * <li>The way groups are handled is no good.
058: * <li>The SimpleResourceStore is not an adequat store for the user database,
059: * it should rather use the jdbmResourceStore (not written yet).
060: * </ul>
061: * However, this provides for the digest functionnalities.
062: */
063:
064: public class DigestAuthFilter extends AuthFilter {
065:
066: public class DigestAuthContext {
067: String dac_user = null;
068: String dac_realm = null;
069: String dac_nonce = null;
070: String dac_uri = null;
071: String dac_response = null;
072: String dac_algorithm = null;
073: String dac_method = null;
074: boolean stale = false;
075:
076: // qop and nc may be added at some point
077: // maybe not as it implies session tracking
078:
079: DigestAuthContext(Request request)
080: throws DigestAuthFilterException, ProtocolException {
081: HttpCredential credential = null;
082:
083: credential = (request.isProxy() ? request
084: .getProxyAuthorization() : request
085: .getAuthorization());
086: if (!credential.getScheme().equalsIgnoreCase("Digest")) {
087: String msg = ("Invalid authentication scheme \""
088: + credential.getScheme() + " expecting \"Digest\"");
089: throw new DigestAuthFilterException(msg);
090: }
091: // now split things and decode things
092: dac_user = credential.getAuthParameter("username");
093: dac_uri = credential.getAuthParameter("uri");
094: dac_response = credential.getAuthParameter("response");
095: dac_realm = credential.getAuthParameter("realm");
096: dac_method = request.getMethod();
097: dac_nonce = credential.getAuthParameter("nonce");
098: if (dac_user == null || dac_uri == null
099: || dac_response == null || dac_realm == null) {
100: String msg = ("Invalid authentication header");
101: throw new DigestAuthFilterException(msg);
102: }
103: }
104:
105: boolean authenticate(String username, String realm,
106: String passwd) {
107: stale = false;
108: if (!dac_user.equals(username))
109: return false;
110: if (!dac_realm.equals(realm))
111: return false;
112: if (dac_algorithm != null
113: && !dac_algorithm.equals(getAlgorithm()))
114: return false;
115: if (!dac_nonce.equals(nonce)) {
116: if (!dac_nonce.equals(old_nonce)) {
117: // check if the user knows the right passwd
118: String a1, a2, ha1, ha2;
119: a1 = username + ":" + realm + ":" + passwd;
120: a2 = dac_method + ":" + dac_uri;
121: MessageDigest md = null;
122: try {
123: md = MessageDigest.getInstance(getAlgorithm());
124: } catch (NoSuchAlgorithmException algex) {
125: // fatal error, can't authenticate
126: return false;
127: }
128: md.update(a1.getBytes());
129: ha1 = StringUtils.toHexString(md.digest());
130: md.reset();
131: md.update(a2.getBytes());
132: ha2 = StringUtils.toHexString(md.digest());
133: md.reset();
134: String kd, hkd;
135: // KD( H(A1), unq(nonce-value) ":" H(A2)
136: kd = ha1 + ":" + dac_nonce + ":" + ha2;
137: md.update(kd.getBytes());
138: hkd = StringUtils.toHexString(md.digest());
139: stale = hkd.equals(dac_response);
140: return false;
141: } else
142: stale = true;
143: }
144: // basic things have been checked... now try the real thing
145: String a1, a2, ha1, ha2;
146: a1 = username + ":" + realm + ":" + passwd;
147: a2 = dac_method + ":" + dac_uri;
148: MessageDigest md = null;
149: try {
150: md = MessageDigest.getInstance(getAlgorithm());
151: } catch (NoSuchAlgorithmException algex) {
152: // fatal error, can't authenticate
153: return false;
154: }
155: md.update(a1.getBytes());
156: ha1 = StringUtils.toHexString(md.digest());
157: md.reset();
158: md.update(a2.getBytes());
159: ha2 = StringUtils.toHexString(md.digest());
160: md.reset();
161: String kd, hkd;
162: if (stale) // KD( H(A1), unq(nonce-value) ":" H(A2)
163: kd = ha1 + ":" + old_nonce + ":" + ha2;
164: else
165: kd = ha1 + ":" + nonce + ":" + ha2;
166: md.update(kd.getBytes());
167: hkd = StringUtils.toHexString(md.digest());
168: if (!hkd.equals(dac_response))
169: return false;
170: // yeah!!!
171: return true;
172: }
173: }
174:
175: /**
176: * Attribute index - The list of allowed users.
177: */
178: protected static int ATTR_ALLOWED_USERS = -1;
179: /**
180: * Attribute index - The list of allowed groups.
181: */
182: protected static int ATTR_ALLOWED_GROUPS = -1;
183: /**
184: * Attribute index - The algorithm used
185: */
186: protected static int ATTR_ALGORITHM = -1;
187: /**
188: * Attribute index - The nonce time to live (in seconds)
189: */
190: protected static int ATTR_NONCE_TTL = -1;
191:
192: static {
193: Attribute a = null;
194: Class c = null;
195: try {
196: c = Class.forName("org.w3c.jigsaw.auth.DigestAuthFilter");
197: } catch (Exception ex) {
198: ex.printStackTrace();
199: System.exit(1);
200: }
201: // The list of allowed users
202: a = new StringArrayAttribute("users", null, Attribute.EDITABLE);
203: ATTR_ALLOWED_USERS = AttributeRegistry.registerAttribute(c, a);
204: // The list of allowed groups:
205: a = new StringArrayAttribute("groups", null, Attribute.EDITABLE);
206: ATTR_ALLOWED_GROUPS = AttributeRegistry.registerAttribute(c, a);
207: // The algorithm used for digest and checksum
208: a = new StringAttribute("algorithm", null, Attribute.EDITABLE);
209: ATTR_ALGORITHM = AttributeRegistry.registerAttribute(c, a);
210: a = new IntegerAttribute("nonce_ttl", new Integer(300),
211: Attribute.EDITABLE);
212: ATTR_NONCE_TTL = AttributeRegistry.registerAttribute(c, a);
213: }
214:
215: /**
216: * The catalog of realms that make our scope.
217: */
218: protected RealmsCatalog catalog = null;
219: /**
220: * Our associated realm.
221: */
222: protected ResourceReference rr_realm = null;
223: /**
224: * The nam of the realm we cache in <code>realm</code>.
225: */
226: protected String loaded_realm = null;
227:
228: /**
229: * The challenge to issue to any client for Digest Authentication.
230: */
231: protected HttpChallenge challenge = null;
232:
233: /**
234: * The nonce value of the digest, changed every X mn
235: */
236: protected String nonce = null;
237: /**
238: * The previous nonce value of the digest, changed every X mn
239: */
240: protected String old_nonce = null;
241:
242: private long prev_date = 0;
243: private int nonce_ttl = 600; /* 10mn by default */
244:
245: /**
246: * Get a pointer to our realm, and initialize our ipmatcher.
247: */
248:
249: protected synchronized void acquireRealm() {
250: // Get our catalog:
251: if (catalog == null) {
252: httpd server = (httpd) ((FramedResource) getTargetResource())
253: .getServer();
254: catalog = server.getRealmsCatalog();
255: }
256: // Check that our realm name is valid:
257: String name = getRealm();
258: if (name == null)
259: return;
260: if ((rr_realm != null) && name.equals(loaded_realm))
261: return;
262: // Load the realm and create the ipmtacher object
263: rr_realm = catalog.loadRealm(name);
264: loaded_realm = name;
265: }
266:
267: /**
268: * Check that our realm does exist.
269: * Otherwise we are probably being initialized, and we don't authenticate
270: * yet.
271: * @return A boolean <strong>true</strong> if realm can be initialized.
272: */
273:
274: protected synchronized boolean checkRealm() {
275: acquireRealm();
276: return true;// (ipmatcher != null) ;
277: }
278:
279: /**
280: * Get the list of allowed users.
281: */
282:
283: public String[] getAllowedUsers() {
284: return (String[]) getValue(ATTR_ALLOWED_USERS, null);
285: }
286:
287: /**
288: * Get the list of allowed groups.
289: */
290:
291: public String[] getAllowedGroups() {
292: return (String[]) getValue(ATTR_ALLOWED_GROUPS, null);
293: }
294:
295: /**
296: * Get the algorithm used
297: */
298:
299: public String getAlgorithm() {
300: return (String) getValue(ATTR_ALGORITHM, "MD5");
301: }
302:
303: /**
304: * Lookup a user by its name.
305: * @param name The user's name.
306: * @return An AuthUser instance, or <strong>null</strong>.
307: */
308:
309: public synchronized ResourceReference lookupUser(String name) {
310: if (rr_realm == null)
311: acquireRealm();
312: try {
313: AuthRealm realm = (AuthRealm) rr_realm.lock();
314: return realm.loadUser(name);
315: } catch (InvalidResourceException ex) {
316: return null;
317: } finally {
318: rr_realm.unlock();
319: }
320: }
321:
322: /*
323: * Is this user allowed in the realm ?
324: * First check in the list of allowed users (if any), than in the list
325: * of allowed groups (if any). If no allowed users or allowed groups
326: * are defined, than simply check for the existence of this user.
327: * @return A boolean <strong>true</strong> if access allowed.
328: */
329:
330: protected boolean checkUser(AuthUser user) {
331: String allowed_users[] = getAllowedUsers();
332: // Check in the list of allowed users:
333: if (allowed_users != null) {
334: for (int i = 0; i < allowed_users.length; i++) {
335: if (allowed_users[i].equals(user.getName()))
336: return true;
337: }
338: }
339: // Check in the list of allowed groups:
340: String allowed_groups[] = getAllowedGroups();
341: if (allowed_groups != null) {
342: String ugroups[] = user.getGroups();
343: if (ugroups != null) {
344: for (int i = 0; i < ugroups.length; i++) {
345: for (int j = 0; j < allowed_groups.length; j++) {
346: if (allowed_groups[j].equals(ugroups[i]))
347: return true;
348: }
349: }
350: }
351: }
352: // If no users or groups specified, return true
353: if ((allowed_users == null) && (allowed_groups == null))
354: return true;
355: return false;
356: }
357:
358: /**
359: * Catch set value on the realm, to maintain cached values.
360: */
361:
362: public void setValue(int idx, Object value) {
363: super .setValue(idx, value);
364: if (idx == ATTR_REALM) {
365: // Initialize the filter challenge:
366: challenge = HttpFactory.makeChallenge("Digest");
367: challenge.setAuthParameter("realm", getRealm());
368: }
369: if (idx == ATTR_NONCE_TTL) {
370: if (value instanceof Integer)
371: nonce_ttl = ((Integer) value).intValue();
372: }
373: }
374:
375: /**
376: * Authenticate the given request.
377: * We first check for valid authentication information. If no
378: * authentication is provided, than we try to map the IP address to some
379: * of the ones we know about. If the IP address is not found, we challenge
380: * the client for a password.
381: * <p>If the IP address is found, than either our user entry requires an
382: * extra password step (in wich case we challenge it), or simple IP
383: * based authentication is enough, so we allow the request.
384: * @param request The request to be authentified.
385: * @exception org.w3c.tools.resources.ProtocolException if authentication
386: * failed
387: */
388:
389: public void authenticate(Request request) throws ProtocolException {
390: // Are we being edited ?
391: if (!checkRealm())
392: return;
393: // Internal requests always allowed:
394: Client client = request.getClient();
395: if (client == null)
396: return;
397: // check for nonce validity
398: Date d = new Date();
399: if ((d.getTime() - prev_date) / 1000 > nonce_ttl) {
400: prev_date = d.getTime();
401: updateNonce();
402: }
403: DigestAuthContext dac = null;
404:
405: // Check authentication according to auth method:
406: if ((request.hasAuthorization() && !request.isProxy())
407: || (request.isProxy() && request
408: .hasProxyAuthorization())) {
409: try {
410: dac = new DigestAuthContext(request);
411: } catch (DigestAuthFilterException ex) {
412: dac = null;
413: }
414: if (dac != null) {
415: ResourceReference rr_user = (ResourceReference) lookupUser(dac.dac_user);
416: try {
417: AuthUser user = (AuthUser) rr_user.lock();
418: // This user doesn't even exists !
419: if (user != null) {
420: // If it has a password check it
421: if (user.definesAttribute("password")) {
422: if (dac.authenticate(user.getName(),
423: loaded_realm, user.getPassword())) {
424: request.setState(STATE_AUTHUSER,
425: dac.dac_user);
426: request.setState(STATE_AUTHTYPE,
427: "Digest");
428: request
429: .setState(STATE_AUTHCONTEXT,
430: dac);
431: return;
432: }
433: }
434: }
435: } catch (InvalidResourceException ex) {
436: } finally {
437: rr_user.unlock();
438: }
439: }
440: }
441:
442: // Every possible scheme has failed for this request, emit an error
443: Reply e = null;
444: HttpChallenge new_c;
445: if (dac != null && dac.stale) {
446: new_c = challenge.getClone();
447: if (new_c != null)
448: new_c.setAuthParameter("stale", "true", false);
449: else
450: new_c = challenge;
451: } else
452: new_c = challenge;
453: if (request.isProxy()) {
454: e = request.makeReply(HTTP.PROXY_AUTH_REQUIRED);
455: e.setProxyAuthenticate(new_c);
456: } else {
457: e = request.makeReply(HTTP.UNAUTHORIZED);
458: e.setWWWAuthenticate(new_c);
459: }
460: HtmlGenerator g = new HtmlGenerator("Unauthorized");
461: g.append("<h1>Unauthorized access</h1>"
462: + "<p>You are denied access to this resource.");
463: e.setStream(g);
464: throw new HTTPException(e);
465: }
466:
467: /**
468: * update the nonce string
469: */
470:
471: private void updateNonce() {
472: updateNonce(getResource());
473: }
474:
475: private synchronized void updateNonce(FramedResource fr) {
476: HTTPFrame htf;
477: if (fr instanceof HTTPFrame) {
478: htf = (HTTPFrame) fr;
479: try {
480: MessageDigest md = MessageDigest
481: .getInstance(getAlgorithm());
482: md.update((new Date()).toString().getBytes());
483: try {
484: md.update(htf.getETag().getTag().getBytes());
485: } catch (Exception ex) {
486: // hum... try without it
487: md.update(htf.getURLPath().getBytes());
488: }
489: byte b[] = md.digest();
490: if (nonce != null)
491: old_nonce = nonce;
492: nonce = StringUtils.toHexString(b);
493: challenge.setAuthParameter("nonce", nonce);
494: } catch (NoSuchAlgorithmException algex) {
495: // bad algorithm, prevent access by firing an error
496: /* Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
497: error.setContent("The algorithm specified in the "+
498: "DigestAuthFilterprocess filter "+
499: "is not available, you are then unable to "+
500: "access protected space");
501: throw new HTTPException(error);
502: */
503: }
504: }
505: }
506:
507: /**
508: * Add the appropriate cache control directives on the way back.
509: * @param request The request that has been processed.
510: * @param reply The original reply.
511: * @return Always <strong>null</strong>.
512: */
513:
514: public ReplyInterface outgoingFilter(RequestInterface request,
515: ReplyInterface reply) {
516: Request req = (Request) request;
517: Reply rep = (Reply) reply;
518: if (getPrivateCachability()) {
519: rep.setMustRevalidate(true);
520: } else if (getSharedCachability()) {
521: rep.setProxyRevalidate(true);
522: } else if (getPublicCachability()) {
523: rep.setPublic(true);
524: }
525: if (req.hasState(AuthFilter.STATE_AUTHCONTEXT)) {
526: DigestAuthContext dac;
527: dac = (DigestAuthContext) req
528: .getState(AuthFilter.STATE_AUTHCONTEXT);
529: if (dac.stale) {
530: rep.addAuthenticationInfo("nextnonce", nonce);
531: }
532: }
533: return null;
534: }
535:
536: /**
537: * Initialize the filter.
538: */
539:
540: public void initialize(Object values[]) {
541: super .initialize(values);
542: if (getRealm() != null) {
543: // Initialize the filter challenge:
544: challenge = HttpFactory.makeChallenge("Digest");
545: challenge.setAuthParameter("realm", getRealm());
546: updateNonce();
547: challenge.setAuthParameter("domain", getURLPath());
548: challenge.setAuthParameter("algorithm", getAlgorithm(),
549: false);
550: }
551: }
552: }
|