1 // ======================================================================== 2 // Copyright 2002-2005 Mort Bay Consulting Pty. Ltd. 3 // ------------------------------------------------------------------------ 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 // ======================================================================== 14 15 package org.mortbay.jetty.security; 16 17 import java.io.IOException; 18 import java.security.MessageDigest; 19 import java.security.Principal; 20 21 import javax.servlet.http.HttpServletResponse; 22 23 import org.mortbay.jetty.HttpHeaders; 24 import org.mortbay.jetty.Request; 25 import org.mortbay.jetty.Response; 26 import org.mortbay.log.Log; 27 import org.mortbay.util.QuotedStringTokenizer; 28 import org.mortbay.util.StringUtil; 29 import org.mortbay.util.TypeUtil; 30 31 /* ------------------------------------------------------------ */ 32 /** DIGEST authentication. 33 * 34 * @author Greg Wilkins (gregw) 35 */ 36 public class DigestAuthenticator implements Authenticator 37 { 38 protected long maxNonceAge=0; 39 protected long nonceSecret=this.hashCode() ^ System.currentTimeMillis(); 40 protected boolean useStale=false; 41 42 43 /* ------------------------------------------------------------ */ 44 /** 45 * @return UserPrinciple if authenticated or null if not. If 46 * Authentication fails, then the authenticator may have committed 47 * the response as an auth challenge or redirect. 48 * @exception IOException 49 */ authenticate(UserRealm realm, String pathInContext, Request request, Response response)50 public Principal authenticate(UserRealm realm, 51 String pathInContext, 52 Request request, 53 Response response) 54 throws IOException 55 { 56 // Get the user if we can 57 boolean stale=false; 58 Principal user=null; 59 String credentials = request.getHeader(HttpHeaders.AUTHORIZATION); 60 61 if (credentials!=null ) 62 { 63 if(Log.isDebugEnabled())Log.debug("Credentials: "+credentials); 64 QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, 65 "=, ", 66 true, 67 false); 68 Digest digest=new Digest(request.getMethod()); 69 String last=null; 70 String name=null; 71 72 loop: 73 while (tokenizer.hasMoreTokens()) 74 { 75 String tok = tokenizer.nextToken(); 76 char c=(tok.length()==1)?tok.charAt(0):'\0'; 77 78 switch (c) 79 { 80 case '=': 81 name=last; 82 last=tok; 83 break; 84 case ',': 85 name=null; 86 case ' ': 87 break; 88 89 default: 90 last=tok; 91 if (name!=null) 92 { 93 if ("username".equalsIgnoreCase(name)) 94 digest.username=tok; 95 else if ("realm".equalsIgnoreCase(name)) 96 digest.realm=tok; 97 else if ("nonce".equalsIgnoreCase(name)) 98 digest.nonce=tok; 99 else if ("nc".equalsIgnoreCase(name)) 100 digest.nc=tok; 101 else if ("cnonce".equalsIgnoreCase(name)) 102 digest.cnonce=tok; 103 else if ("qop".equalsIgnoreCase(name)) 104 digest.qop=tok; 105 else if ("uri".equalsIgnoreCase(name)) 106 digest.uri=tok; 107 else if ("response".equalsIgnoreCase(name)) 108 digest.response=tok; 109 break; 110 } 111 } 112 } 113 114 int n=checkNonce(digest.nonce,request); 115 if (n>0) 116 user = realm.authenticate(digest.username,digest,request); 117 else if (n==0) 118 stale = true; 119 120 if (user==null) 121 Log.warn("AUTH FAILURE: user "+StringUtil.printable(digest.username)); 122 else 123 { 124 request.setAuthType(Constraint.__DIGEST_AUTH); 125 request.setUserPrincipal(user); 126 } 127 } 128 129 // Challenge if we have no user 130 if (user==null && response!=null) 131 sendChallenge(realm,request,response,stale); 132 133 return user; 134 } 135 136 /* ------------------------------------------------------------ */ getAuthMethod()137 public String getAuthMethod() 138 { 139 return Constraint.__DIGEST_AUTH; 140 } 141 142 /* ------------------------------------------------------------ */ sendChallenge(UserRealm realm, Request request, Response response, boolean stale)143 public void sendChallenge(UserRealm realm, 144 Request request, 145 Response response, 146 boolean stale) 147 throws IOException 148 { 149 String domain=request.getContextPath(); 150 if (domain==null) 151 domain="/"; 152 response.setHeader(HttpHeaders.WWW_AUTHENTICATE, 153 "Digest realm=\""+realm.getName()+ 154 "\", domain=\""+domain + 155 "\", nonce=\""+newNonce(request)+ 156 "\", algorithm=MD5, qop=\"auth\"" + (useStale?(" stale="+stale):"") 157 ); 158 response.sendError(HttpServletResponse.SC_UNAUTHORIZED); 159 } 160 161 /* ------------------------------------------------------------ */ newNonce(Request request)162 public String newNonce(Request request) 163 { 164 long ts=request.getTimeStamp(); 165 long sk=nonceSecret; 166 167 byte[] nounce = new byte[24]; 168 for (int i=0;i<8;i++) 169 { 170 nounce[i]=(byte)(ts&0xff); 171 ts=ts>>8; 172 nounce[8+i]=(byte)(sk&0xff); 173 sk=sk>>8; 174 } 175 176 byte[] hash=null; 177 try 178 { 179 MessageDigest md = MessageDigest.getInstance("MD5"); 180 md.reset(); 181 md.update(nounce,0,16); 182 hash = md.digest(); 183 } 184 catch(Exception e) 185 { 186 Log.warn(e); 187 } 188 189 for (int i=0;i<hash.length;i++) 190 { 191 nounce[8+i]=hash[i]; 192 if (i==23) 193 break; 194 } 195 196 return new String(B64Code.encode(nounce)); 197 } 198 199 /** 200 * @param nonce 201 * @param request 202 * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce 203 */ 204 /* ------------------------------------------------------------ */ checkNonce(String nonce, Request request)205 public int checkNonce(String nonce, Request request) 206 { 207 try 208 { 209 byte[] n = B64Code.decode(nonce.toCharArray()); 210 if (n.length!=24) 211 return -1; 212 213 long ts=0; 214 long sk=nonceSecret; 215 byte[] n2 = new byte[16]; 216 System.arraycopy(n, 0, n2, 0, 8); 217 for (int i=0;i<8;i++) 218 { 219 n2[8+i]=(byte)(sk&0xff); 220 sk=sk>>8; 221 ts=(ts<<8)+(0xff&(long)n[7-i]); 222 } 223 224 long age=request.getTimeStamp()-ts; 225 if (Log.isDebugEnabled()) Log.debug("age="+age); 226 227 byte[] hash=null; 228 try 229 { 230 MessageDigest md = MessageDigest.getInstance("MD5"); 231 md.reset(); 232 md.update(n2,0,16); 233 hash = md.digest(); 234 } 235 catch(Exception e) 236 { 237 Log.warn(e); 238 } 239 240 for (int i=0;i<16;i++) 241 if (n[i+8]!=hash[i]) 242 return -1; 243 244 if(maxNonceAge>0 && (age<0 || age>maxNonceAge)) 245 return 0; // stale 246 247 return 1; 248 } 249 catch(Exception e) 250 { 251 Log.ignore(e); 252 } 253 return -1; 254 } 255 256 /* ------------------------------------------------------------ */ 257 /* ------------------------------------------------------------ */ 258 /* ------------------------------------------------------------ */ 259 private static class Digest extends Credential 260 { 261 String method=null; 262 String username = null; 263 String realm = null; 264 String nonce = null; 265 String nc = null; 266 String cnonce = null; 267 String qop = null; 268 String uri = null; 269 String response=null; 270 271 /* ------------------------------------------------------------ */ Digest(String m)272 Digest(String m) 273 { 274 method=m; 275 } 276 277 /* ------------------------------------------------------------ */ check(Object credentials)278 public boolean check(Object credentials) 279 { 280 String password=(credentials instanceof String) 281 ?(String)credentials 282 :credentials.toString(); 283 284 try{ 285 MessageDigest md = MessageDigest.getInstance("MD5"); 286 byte[] ha1; 287 if(credentials instanceof Credential.MD5) 288 { 289 // Credentials are already a MD5 digest - assume it's in 290 // form user:realm:password (we have no way to know since 291 // it's a digest, alright?) 292 ha1 = ((Credential.MD5)credentials).getDigest(); 293 } 294 else 295 { 296 // calc A1 digest 297 md.update(username.getBytes(StringUtil.__ISO_8859_1)); 298 md.update((byte)':'); 299 md.update(realm.getBytes(StringUtil.__ISO_8859_1)); 300 md.update((byte)':'); 301 md.update(password.getBytes(StringUtil.__ISO_8859_1)); 302 ha1=md.digest(); 303 } 304 // calc A2 digest 305 md.reset(); 306 md.update(method.getBytes(StringUtil.__ISO_8859_1)); 307 md.update((byte)':'); 308 md.update(uri.getBytes(StringUtil.__ISO_8859_1)); 309 byte[] ha2=md.digest(); 310 311 312 313 314 315 // calc digest 316 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <"> 317 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> 318 319 320 321 md.update(TypeUtil.toString(ha1,16).getBytes(StringUtil.__ISO_8859_1)); 322 md.update((byte)':'); 323 md.update(nonce.getBytes(StringUtil.__ISO_8859_1)); 324 md.update((byte)':'); 325 md.update(nc.getBytes(StringUtil.__ISO_8859_1)); 326 md.update((byte)':'); 327 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1)); 328 md.update((byte)':'); 329 md.update(qop.getBytes(StringUtil.__ISO_8859_1)); 330 md.update((byte)':'); 331 md.update(TypeUtil.toString(ha2,16).getBytes(StringUtil.__ISO_8859_1)); 332 byte[] digest=md.digest(); 333 334 // check digest 335 return (TypeUtil.toString(digest,16).equalsIgnoreCase(response)); 336 } 337 catch (Exception e) 338 {Log.warn(e);} 339 340 return false; 341 } 342 toString()343 public String toString() 344 { 345 return username+","+response; 346 } 347 348 } 349 /** 350 * @return Returns the maxNonceAge. 351 */ getMaxNonceAge()352 public long getMaxNonceAge() 353 { 354 return maxNonceAge; 355 } 356 /** 357 * @param maxNonceAge The maxNonceAge to set. 358 */ setMaxNonceAge(long maxNonceAge)359 public void setMaxNonceAge(long maxNonceAge) 360 { 361 this.maxNonceAge = maxNonceAge; 362 } 363 /** 364 * @return Returns the nonceSecret. 365 */ getNonceSecret()366 public long getNonceSecret() 367 { 368 return nonceSecret; 369 } 370 /** 371 * @param nonceSecret The nonceSecret to set. 372 */ setNonceSecret(long nonceSecret)373 public void setNonceSecret(long nonceSecret) 374 { 375 this.nonceSecret = nonceSecret; 376 } 377 setUseStale(boolean us)378 public void setUseStale(boolean us) 379 { 380 this.useStale=us; 381 } 382 getUseStale()383 public boolean getUseStale() 384 { 385 return useStale; 386 } 387 } 388 389