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