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