1/* 2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java $ 3 * $Revision: 659595 $ 4 * $Date: 2008-05-23 09:47:14 -0700 (Fri, 23 May 2008) $ 5 * 6 * ==================================================================== 7 * 8 * Licensed to the Apache Software Foundation (ASF) under one or more 9 * contributor license agreements. See the NOTICE file distributed with 10 * this work for additional information regarding copyright ownership. 11 * The ASF licenses this file to You under the Apache License, Version 2.0 12 * (the "License"); you may not use this file except in compliance with 13 * the License. You may obtain a copy of the License at 14 * 15 * http://www.apache.org/licenses/LICENSE-2.0 16 * 17 * Unless required by applicable law or agreed to in writing, software 18 * distributed under the License is distributed on an "AS IS" BASIS, 19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 * See the License for the specific language governing permissions and 21 * limitations under the License. 22 * ==================================================================== 23 * 24 * This software consists of voluntary contributions made by many 25 * individuals on behalf of the Apache Software Foundation. For more 26 * information on the Apache Software Foundation, please see 27 * <http://www.apache.org/>. 28 * 29 */ 30 31package org.apache.http.impl.auth; 32 33import java.security.MessageDigest; 34import java.util.ArrayList; 35import java.util.List; 36import java.util.StringTokenizer; 37 38import org.apache.http.Header; 39import org.apache.http.HttpRequest; 40import org.apache.http.auth.AuthenticationException; 41import org.apache.http.auth.Credentials; 42import org.apache.http.auth.AUTH; 43import org.apache.http.auth.MalformedChallengeException; 44import org.apache.http.auth.params.AuthParams; 45import org.apache.http.message.BasicNameValuePair; 46import org.apache.http.message.BasicHeaderValueFormatter; 47import org.apache.http.message.BufferedHeader; 48import org.apache.http.util.CharArrayBuffer; 49import org.apache.http.util.EncodingUtils; 50 51/** 52 * <p> 53 * Digest authentication scheme as defined in RFC 2617. 54 * Both MD5 (default) and MD5-sess are supported. 55 * Currently only qop=auth or no qop is supported. qop=auth-int 56 * is unsupported. If auth and auth-int are provided, auth is 57 * used. 58 * </p> 59 * <p> 60 * Credential charset is configured via the 61 * {@link org.apache.http.auth.params.AuthPNames#CREDENTIAL_CHARSET 62 * credential charset} parameter. 63 * Since the digest username is included as clear text in the generated 64 * Authentication header, the charset of the username must be compatible 65 * with the 66 * {@link org.apache.http.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET 67 * http element charset}. 68 * </p> 69 * 70 * @author <a href="mailto:remm@apache.org">Remy Maucherat</a> 71 * @author Rodney Waldhoff 72 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> 73 * @author Ortwin Glueck 74 * @author Sean C. Sullivan 75 * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a> 76 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> 77 * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> 78 * 79 * @since 4.0 80 */ 81 82public class DigestScheme extends RFC2617Scheme { 83 84 /** 85 * Hexa values used when creating 32 character long digest in HTTP DigestScheme 86 * in case of authentication. 87 * 88 * @see #encode(byte[]) 89 */ 90 private static final char[] HEXADECIMAL = { 91 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 92 'e', 'f' 93 }; 94 95 /** Whether the digest authentication process is complete */ 96 private boolean complete; 97 98 //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay 99 private static final String NC = "00000001"; //nonce-count is always 1 100 private static final int QOP_MISSING = 0; 101 private static final int QOP_AUTH_INT = 1; 102 private static final int QOP_AUTH = 2; 103 104 private int qopVariant = QOP_MISSING; 105 private String cnonce; 106 107 /** 108 * Default constructor for the digest authetication scheme. 109 */ 110 public DigestScheme() { 111 super(); 112 this.complete = false; 113 } 114 115 /** 116 * Processes the Digest challenge. 117 * 118 * @param header the challenge header 119 * 120 * @throws MalformedChallengeException is thrown if the authentication challenge 121 * is malformed 122 */ 123 @Override 124 public void processChallenge( 125 final Header header) throws MalformedChallengeException { 126 super.processChallenge(header); 127 128 if (getParameter("realm") == null) { 129 throw new MalformedChallengeException("missing realm in challange"); 130 } 131 if (getParameter("nonce") == null) { 132 throw new MalformedChallengeException("missing nonce in challange"); 133 } 134 135 boolean unsupportedQop = false; 136 // qop parsing 137 String qop = getParameter("qop"); 138 if (qop != null) { 139 StringTokenizer tok = new StringTokenizer(qop,","); 140 while (tok.hasMoreTokens()) { 141 String variant = tok.nextToken().trim(); 142 if (variant.equals("auth")) { 143 qopVariant = QOP_AUTH; 144 break; //that's our favourite, because auth-int is unsupported 145 } else if (variant.equals("auth-int")) { 146 qopVariant = QOP_AUTH_INT; 147 } else { 148 unsupportedQop = true; 149 } 150 } 151 } 152 153 if (unsupportedQop && (qopVariant == QOP_MISSING)) { 154 throw new MalformedChallengeException("None of the qop methods is supported"); 155 } 156 // Reset cnonce 157 this.cnonce = null; 158 this.complete = true; 159 } 160 161 /** 162 * Tests if the Digest authentication process has been completed. 163 * 164 * @return <tt>true</tt> if Digest authorization has been processed, 165 * <tt>false</tt> otherwise. 166 */ 167 public boolean isComplete() { 168 String s = getParameter("stale"); 169 if ("true".equalsIgnoreCase(s)) { 170 return false; 171 } else { 172 return this.complete; 173 } 174 } 175 176 /** 177 * Returns textual designation of the digest authentication scheme. 178 * 179 * @return <code>digest</code> 180 */ 181 public String getSchemeName() { 182 return "digest"; 183 } 184 185 /** 186 * Returns <tt>false</tt>. Digest authentication scheme is request based. 187 * 188 * @return <tt>false</tt>. 189 */ 190 public boolean isConnectionBased() { 191 return false; 192 } 193 194 public void overrideParamter(final String name, final String value) { 195 getParameters().put(name, value); 196 } 197 198 private String getCnonce() { 199 if (this.cnonce == null) { 200 this.cnonce = createCnonce(); 201 } 202 return this.cnonce; 203 } 204 205 /** 206 * Produces a digest authorization string for the given set of 207 * {@link Credentials}, method name and URI. 208 * 209 * @param credentials A set of credentials to be used for athentication 210 * @param request The request being authenticated 211 * 212 * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials 213 * are not valid or not applicable for this authentication scheme 214 * @throws AuthenticationException if authorization string cannot 215 * be generated due to an authentication failure 216 * 217 * @return a digest authorization string 218 */ 219 public Header authenticate( 220 final Credentials credentials, 221 final HttpRequest request) throws AuthenticationException { 222 223 if (credentials == null) { 224 throw new IllegalArgumentException("Credentials may not be null"); 225 } 226 if (request == null) { 227 throw new IllegalArgumentException("HTTP request may not be null"); 228 } 229 230 // Add method name and request-URI to the parameter map 231 getParameters().put("methodname", request.getRequestLine().getMethod()); 232 getParameters().put("uri", request.getRequestLine().getUri()); 233 String charset = getParameter("charset"); 234 if (charset == null) { 235 charset = AuthParams.getCredentialCharset(request.getParams()); 236 getParameters().put("charset", charset); 237 } 238 String digest = createDigest(credentials); 239 return createDigestHeader(credentials, digest); 240 } 241 242 private static MessageDigest createMessageDigest( 243 final String digAlg) throws UnsupportedDigestAlgorithmException { 244 try { 245 return MessageDigest.getInstance(digAlg); 246 } catch (Exception e) { 247 throw new UnsupportedDigestAlgorithmException( 248 "Unsupported algorithm in HTTP Digest authentication: " 249 + digAlg); 250 } 251 } 252 253 /** 254 * Creates an MD5 response digest. 255 * 256 * @return The created digest as string. This will be the response tag's 257 * value in the Authentication HTTP header. 258 * @throws AuthenticationException when MD5 is an unsupported algorithm 259 */ 260 private String createDigest(final Credentials credentials) throws AuthenticationException { 261 // Collecting required tokens 262 String uri = getParameter("uri"); 263 String realm = getParameter("realm"); 264 String nonce = getParameter("nonce"); 265 String method = getParameter("methodname"); 266 String algorithm = getParameter("algorithm"); 267 if (uri == null) { 268 throw new IllegalStateException("URI may not be null"); 269 } 270 if (realm == null) { 271 throw new IllegalStateException("Realm may not be null"); 272 } 273 if (nonce == null) { 274 throw new IllegalStateException("Nonce may not be null"); 275 } 276 // If an algorithm is not specified, default to MD5. 277 if (algorithm == null) { 278 algorithm = "MD5"; 279 } 280 // If an charset is not specified, default to ISO-8859-1. 281 String charset = getParameter("charset"); 282 if (charset == null) { 283 charset = "ISO-8859-1"; 284 } 285 286 if (qopVariant == QOP_AUTH_INT) { 287 throw new AuthenticationException( 288 "Unsupported qop in HTTP Digest authentication"); 289 } 290 291 MessageDigest md5Helper = createMessageDigest("MD5"); 292 293 String uname = credentials.getUserPrincipal().getName(); 294 String pwd = credentials.getPassword(); 295 296 // 3.2.2.2: Calculating digest 297 StringBuilder tmp = new StringBuilder(uname.length() + realm.length() + pwd.length() + 2); 298 tmp.append(uname); 299 tmp.append(':'); 300 tmp.append(realm); 301 tmp.append(':'); 302 tmp.append(pwd); 303 // unq(username-value) ":" unq(realm-value) ":" passwd 304 String a1 = tmp.toString(); 305 306 //a1 is suitable for MD5 algorithm 307 if(algorithm.equals("MD5-sess")) { 308 // H( unq(username-value) ":" unq(realm-value) ":" passwd ) 309 // ":" unq(nonce-value) 310 // ":" unq(cnonce-value) 311 312 String cnonce = getCnonce(); 313 314 String tmp2=encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset))); 315 StringBuilder tmp3 = new StringBuilder(tmp2.length() + nonce.length() + cnonce.length() + 2); 316 tmp3.append(tmp2); 317 tmp3.append(':'); 318 tmp3.append(nonce); 319 tmp3.append(':'); 320 tmp3.append(cnonce); 321 a1 = tmp3.toString(); 322 } else if (!algorithm.equals("MD5")) { 323 throw new AuthenticationException("Unhandled algorithm " + algorithm + " requested"); 324 } 325 String md5a1 = encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset))); 326 327 String a2 = null; 328 if (qopVariant == QOP_AUTH_INT) { 329 // Unhandled qop auth-int 330 //we do not have access to the entity-body or its hash 331 //TODO: add Method ":" digest-uri-value ":" H(entity-body) 332 } else { 333 a2 = method + ':' + uri; 334 } 335 String md5a2 = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(a2))); 336 337 // 3.2.2.1 338 String serverDigestValue; 339 if (qopVariant == QOP_MISSING) { 340 StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() + md5a2.length()); 341 tmp2.append(md5a1); 342 tmp2.append(':'); 343 tmp2.append(nonce); 344 tmp2.append(':'); 345 tmp2.append(md5a2); 346 serverDigestValue = tmp2.toString(); 347 } else { 348 String qopOption = getQopVariantString(); 349 String cnonce = getCnonce(); 350 351 StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() 352 + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5); 353 tmp2.append(md5a1); 354 tmp2.append(':'); 355 tmp2.append(nonce); 356 tmp2.append(':'); 357 tmp2.append(NC); 358 tmp2.append(':'); 359 tmp2.append(cnonce); 360 tmp2.append(':'); 361 tmp2.append(qopOption); 362 tmp2.append(':'); 363 tmp2.append(md5a2); 364 serverDigestValue = tmp2.toString(); 365 } 366 367 String serverDigest = 368 encode(md5Helper.digest(EncodingUtils.getAsciiBytes(serverDigestValue))); 369 370 return serverDigest; 371 } 372 373 /** 374 * Creates digest-response header as defined in RFC2617. 375 * 376 * @param credentials User credentials 377 * @param digest The response tag's value as String. 378 * 379 * @return The digest-response as String. 380 */ 381 private Header createDigestHeader( 382 final Credentials credentials, 383 final String digest) throws AuthenticationException { 384 385 CharArrayBuffer buffer = new CharArrayBuffer(128); 386 if (isProxy()) { 387 buffer.append(AUTH.PROXY_AUTH_RESP); 388 } else { 389 buffer.append(AUTH.WWW_AUTH_RESP); 390 } 391 buffer.append(": Digest "); 392 393 String uri = getParameter("uri"); 394 String realm = getParameter("realm"); 395 String nonce = getParameter("nonce"); 396 String opaque = getParameter("opaque"); 397 String response = digest; 398 String algorithm = getParameter("algorithm"); 399 400 String uname = credentials.getUserPrincipal().getName(); 401 402 List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20); 403 params.add(new BasicNameValuePair("username", uname)); 404 params.add(new BasicNameValuePair("realm", realm)); 405 params.add(new BasicNameValuePair("nonce", nonce)); 406 params.add(new BasicNameValuePair("uri", uri)); 407 params.add(new BasicNameValuePair("response", response)); 408 409 if (qopVariant != QOP_MISSING) { 410 params.add(new BasicNameValuePair("qop", getQopVariantString())); 411 params.add(new BasicNameValuePair("nc", NC)); 412 params.add(new BasicNameValuePair("cnonce", getCnonce())); 413 } 414 if (algorithm != null) { 415 params.add(new BasicNameValuePair("algorithm", algorithm)); 416 } 417 if (opaque != null) { 418 params.add(new BasicNameValuePair("opaque", opaque)); 419 } 420 421 for (int i = 0; i < params.size(); i++) { 422 BasicNameValuePair param = params.get(i); 423 if (i > 0) { 424 buffer.append(", "); 425 } 426 boolean noQuotes = "nc".equals(param.getName()) || 427 "qop".equals(param.getName()); 428 BasicHeaderValueFormatter.DEFAULT 429 .formatNameValuePair(buffer, param, !noQuotes); 430 } 431 return new BufferedHeader(buffer); 432 } 433 434 private String getQopVariantString() { 435 String qopOption; 436 if (qopVariant == QOP_AUTH_INT) { 437 qopOption = "auth-int"; 438 } else { 439 qopOption = "auth"; 440 } 441 return qopOption; 442 } 443 444 /** 445 * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long 446 * <CODE>String</CODE> according to RFC 2617. 447 * 448 * @param binaryData array containing the digest 449 * @return encoded MD5, or <CODE>null</CODE> if encoding failed 450 */ 451 private static String encode(byte[] binaryData) { 452 if (binaryData.length != 16) { 453 return null; 454 } 455 456 char[] buffer = new char[32]; 457 for (int i = 0; i < 16; i++) { 458 int low = (binaryData[i] & 0x0f); 459 int high = ((binaryData[i] & 0xf0) >> 4); 460 buffer[i * 2] = HEXADECIMAL[high]; 461 buffer[(i * 2) + 1] = HEXADECIMAL[low]; 462 } 463 464 return new String(buffer); 465 } 466 467 468 /** 469 * Creates a random cnonce value based on the current time. 470 * 471 * @return The cnonce value as String. 472 * @throws UnsupportedDigestAlgorithmException if MD5 algorithm is not supported. 473 */ 474 public static String createCnonce() { 475 String cnonce; 476 477 MessageDigest md5Helper = createMessageDigest("MD5"); 478 479 cnonce = Long.toString(System.currentTimeMillis()); 480 cnonce = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(cnonce))); 481 482 return cnonce; 483 } 484} 485