[ https://issues.apache.org/jira/browse/CXF-5366?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=13808893#comment-13808893 ]
Evgeny Shakin edited comment on CXF-5366 at 10/30/13 9:40 AM: -------------------------------------------------------------- Sure, here is the code of my customized DigestAuthSupplier, the changes vis-a-vis the original DigestAuthSupplier are hardly noticeable, please watch for //ES: comments. This auth supplier is set on the HTTPConduit as follows: conduit.setAuthSupplier(new CustomAuthSupplier()); public class CustomAuthSupplier implements HttpAuthSupplier { private static final char[] HEXADECIMAL = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; final MessageDigest md5Helper; Map<URI, DigestInfo> authInfo = new ConcurrentHashMap<URI, DigestInfo>(); public CustomAuthSupplier() { MessageDigest md = null; try { md = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { //ignore - set to null } md5Helper = md; } /** * {@inheritDoc} * With digest, the nonce could expire and thus a rechallenge will be issued. * Thus, we need requests cached to be able to handle that */ public boolean requiresRequestCaching() { return true; } public String getAuthorization(AuthorizationPolicy authPolicy, URI currentURI, Message message, String fullHeader) { if (authPolicy == null || (authPolicy.getUserName() == null && authPolicy.getPassword() == null)) { return null; } if (fullHeader == null) { DigestInfo di = authInfo.get(currentURI); if (di != null) { /* Preemptive authentication is only possible if we have a cached * challenge */ return di.generateAuth(currentURI.getPath(), authPolicy.getUserName(), authPolicy.getPassword()); } else { return null; } } HttpAuthHeader authHeader = new HttpAuthHeader(fullHeader); if (authHeader.authTypeIsDigest()) { Map<String, String> map = authHeader.getParams(); if ("auth".equals(map.get("qop")) \|| !map.containsKey("qop")) { DigestInfo di = new DigestInfo(); di.qop = map.get("qop"); di.realm = map.get("realm"); di.nonce = map.get("nonce"); di.opaque = map.get("opaque"); if (map.containsKey("algorithm")) { di.algorithm = map.get("algorithm"); } if (map.containsKey("charset")) { di.charset = map.get("charset"); } di.method = (String)message.get(Message.HTTP_REQUEST_METHOD); if (di.method == null) { di.method = "POST"; } authInfo.put(currentURI, di); return di.generateAuth(currentURI.getPath(), authPolicy.getUserName(), authPolicy.getPassword()); } } return null; } public String createCnonce() throws UnsupportedEncodingException { String cnonce = Long.toString(System.currentTimeMillis()); byte[] bytes = cnonce.getBytes("US-ASCII"); synchronized (md5Helper) { bytes = md5Helper.digest(bytes); } return encode(bytes); } class DigestInfo { String qop; String realm; String nonce; String opaque; int nc; String algorithm = "MD5"; String charset = "ISO-8859-1"; String method = "POST"; synchronized String generateAuth(String uri, String username, String password) { try { nc++; String ncstring = String.format("%08d", nc); String cnonce = createCnonce(); String digAlg = algorithm; if (digAlg.equalsIgnoreCase("MD5-sess")) { digAlg = "MD5"; } MessageDigest digester = MessageDigest.getInstance(digAlg); String a1 = username + ":" + realm + ":" + password; if ("MD5-sess".equalsIgnoreCase(algorithm)) { // ES:removed // algorithm = "MD5"; String tmp2 = encode(digester.digest(a1.getBytes(charset))); a1 = tmp2 + ':' + nonce + ':' + cnonce; } String hasha1 = encode(digester.digest(a1.getBytes(charset))); String a2 = method + ":" + uri; String hasha2 = encode(digester.digest(a2.getBytes("US-ASCII"))); String serverDigestValue = null; if (qop == null) { serverDigestValue = hasha1 + ":" + nonce + ":" + hasha2; } else { serverDigestValue = hasha1 + ":" + nonce + ":" + ncstring + ":" + cnonce + ":" + qop + ":" + hasha2; } String response = encode(digester.digest(serverDigestValue.getBytes("US-ASCII"))); Map<String, String> outParams = new HashMap<String, String>(); outParams.put("username", username); outParams.put("realm", realm); outParams.put("nonce", nonce); outParams.put("nc", ncstring); outParams.put("uri", uri); outParams.put("response", response); //ES: added outParams.put("algorithm", algorithm); outParams.put("cnonce", cnonce); if (qop != null) { outParams.put("qop", "auth"); } outParams.put("opaque", opaque); return new HttpAuthHeader(HttpAuthHeader.AUTH_TYPE_DIGEST, outParams).getFullHeader(); } catch (Exception ex) { throw new RuntimeException(ex); } } } /** * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long * <CODE>String</CODE> according to RFC 2617. * * @param binaryData array containing the digest * @return encoded MD5, or <CODE>null</CODE> if encoding failed */ private static String encode(byte[] binaryData) { int n = binaryData.length; char[] buffer = new char[n * 2]; for (int i = 0; i < n; i++) { int low = binaryData[i] & 0x0f; int high = (binaryData[i] & 0xf0) >> 4; buffer[i * 2] = HEXADECIMAL[high]; buffer[(i * 2) + 1] = HEXADECIMAL[low]; } return new String(buffer); } } was (Author: chakine): Sure, here is the code of my customized DigestAuthSupplier, the changes vis-a-vis the original DigestAuthSupplier are hardly noticeable, please watch for //ES: comments. This auth supplier is set on the HTTPConduit as follows: conduit.setAuthSupplier(new CustomAuthSupplier()); public class CustomAuthSupplier implements HttpAuthSupplier { private static final char[] HEXADECIMAL = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; final MessageDigest md5Helper; Map<URI, DigestInfo> authInfo = new ConcurrentHashMap<URI, DigestInfo>(); public CustomAuthSupplier() { MessageDigest md = null; try { md = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { //ignore - set to null } md5Helper = md; } /** * {@inheritDoc} * With digest, the nonce could expire and thus a rechallenge will be issued. * Thus, we need requests cached to be able to handle that */ public boolean requiresRequestCaching() { return true; } public String getAuthorization(AuthorizationPolicy authPolicy, URI currentURI, Message message, String fullHeader) { if (authPolicy == null || (authPolicy.getUserName() == null && authPolicy.getPassword() == null)) { return null; } if (fullHeader == null) { DigestInfo di = authInfo.get(currentURI); if (di != null) { /* Preemptive authentication is only possible if we have a cached * challenge */ return di.generateAuth(currentURI.getPath(), authPolicy.getUserName(), authPolicy.getPassword()); } else { return null; } } HttpAuthHeader authHeader = new HttpAuthHeader(fullHeader); if (authHeader.authTypeIsDigest()) { Map<String, String> map = authHeader.getParams(); if ("auth".equals(map.get("qop")) || !map.containsKey("qop")) { DigestInfo di = new DigestInfo(); di.qop = map.get("qop"); di.realm = map.get("realm"); di.nonce = map.get("nonce"); di.opaque = map.get("opaque"); if (map.containsKey("algorithm")) { di.algorithm = map.get("algorithm"); } if (map.containsKey("charset")) { di.charset = map.get("charset"); } di.method = (String)message.get(Message.HTTP_REQUEST_METHOD); if (di.method == null) { di.method = "POST"; } authInfo.put(currentURI, di); return di.generateAuth(currentURI.getPath(), authPolicy.getUserName(), authPolicy.getPassword()); } } return null; } public String createCnonce() throws UnsupportedEncodingException { String cnonce = Long.toString(System.currentTimeMillis()); byte[] bytes = cnonce.getBytes("US-ASCII"); synchronized (md5Helper) { bytes = md5Helper.digest(bytes); } return encode(bytes); } class DigestInfo { String qop; String realm; String nonce; String opaque; int nc; String algorithm = "MD5"; String charset = "ISO-8859-1"; String method = "POST"; synchronized String generateAuth(String uri, String username, String password) { try { nc++; String ncstring = String.format("%08d", nc); String cnonce = createCnonce(); String digAlg = algorithm; if (digAlg.equalsIgnoreCase("MD5-sess")) { digAlg = "MD5"; } MessageDigest digester = MessageDigest.getInstance(digAlg); String a1 = username + ":" + realm + ":" + password; if ("MD5-sess".equalsIgnoreCase(algorithm)) { // ES:removed // algorithm = "MD5"; String tmp2 = encode(digester.digest(a1.getBytes(charset))); a1 = tmp2 + ':' + nonce + ':' + cnonce; } String hasha1 = encode(digester.digest(a1.getBytes(charset))); String a2 = method + ":" + uri; String hasha2 = encode(digester.digest(a2.getBytes("US-ASCII"))); String serverDigestValue = null; if (qop == null) { serverDigestValue = hasha1 + ":" + nonce + ":" + hasha2; } else { serverDigestValue = hasha1 + ":" + nonce + ":" + ncstring + ":" + cnonce + ":" + qop + ":" + hasha2; } String response = encode(digester.digest(serverDigestValue.getBytes("US-ASCII"))); Map<String, String> outParams = new HashMap<String, String>(); outParams.put("username", username); outParams.put("realm", realm); outParams.put("nonce", nonce); outParams.put("nc", ncstring); outParams.put("uri", uri); outParams.put("response", response); //ES: added outParams.put("algorithm", algorithm); outParams.put("cnonce", cnonce); if (qop != null) { outParams.put("qop", "auth"); } outParams.put("opaque", opaque); return new HttpAuthHeader(HttpAuthHeader.AUTH_TYPE_DIGEST, outParams).getFullHeader(); } catch (Exception ex) { throw new RuntimeException(ex); } } } /** * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long * <CODE>String</CODE> according to RFC 2617. * * @param binaryData array containing the digest * @return encoded MD5, or <CODE>null</CODE> if encoding failed */ private static String encode(byte[] binaryData) { int n = binaryData.length; char[] buffer = new char[n * 2]; for (int i = 0; i < n; i++) { int low = binaryData[i] & 0x0f; int high = (binaryData[i] & 0xf0) >> 4; buffer[i * 2] = HEXADECIMAL[high]; buffer[(i * 2) + 1] = HEXADECIMAL[low]; } return new String(buffer); } } > Authorization header is not set correctly in CXF HTTP digest authentication > ---------------------------------------------------------------------------- > > Key: CXF-5366 > URL: https://issues.apache.org/jira/browse/CXF-5366 > Project: CXF > Issue Type: Bug > Components: Core > Affects Versions: 2.7.4, 2.7.5, 2.7.6, 2.7.7 > Environment: Windows 7 64 bit, Java 1.6.0_29, CXF 2.7.4, calling MS > Dynamics WS. > Reporter: Evgeny Shakin > > When performing the digest HTTP authentication the generated Authorization > header is missing the "algorithm" element. Also if the algorithm is > "MD5-sess" it should appear in the Authorization header as is and not as > "MD5". To get around the issue it is possible to use a customized > DigestAuthSupplier for the affected CXF versions. The result of WS invocation > without "algorithm" in the Authorization header is 400-Bad request. > The issue relates to versions of CXF 2.7.4 and later, earlier versions work > fine. > Sample request: > POST /XXXXXXX HTTP/1.1 > Content-Type: text/xml; charset=UTF-8 > Accept: */* > SOAPAction: "http://schemas.microsoft.com/dynamics/XXXXXXX" > User-Agent: Apache CXF 2.7.4 > Cache-Control: no-cache > Pragma: no-cache > Host: XXXXX > Connection: keep-alive > Content-Length: 542 > <soap:Envelope > xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body>XXXXX</soap:Body></soap:Envelope> > POST /XXXXX HTTP/1.1 > Content-Type: text/xml; charset=UTF-8 > Accept: */* > Authorization: Digest response="541f8d073f2be81deae8e2f1065725b2", > cnonce="46f26ffb6cf32b66873bf6e5e955bae8", username="XXXXX", nc="00000001", > nonce="+Upgraded+v126a0f6047dd70851ab2155a14d09d56aacd7cd4a87d1ce01d77d4709393a1585490f57bdd6026b2c339c1f27bc03f4e47400ad20e8208244", > realm="Digest", qop="auth", uri="/XXXXXXX" > SOAPAction: "http://schemas.microsoft.com/dynamics/XXXXXXX" > User-Agent: Apache CXF 2.7.4 > Cache-Control: no-cache > Pragma: no-cache > Host: localhost:8887 > Connection: keep-alive > Content-Length: 542 > <soap:Envelope > xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body>XXXXXX</soap:Body></soap:Envelope> > Sample response: > HTTP/1.1 401 Unauthorized > Content-Length: 0 > Server: Microsoft-HTTPAPI/2.0 > WWW-Authenticate: Digest > qop="auth",algorithm=MD5-sess,nonce="+Upgraded+v126a0f6047dd70851ab2155a14d09d56af26b5ad2f0d3ce0169267269a2cfa168709705665fd13f9adf81235595c672ec1623b17e470ccaef",charset=utf-8,realm="Digest" > Date: Mon, 28 Oct 2013 15:17:31 GMT > HTTP/1.1 400 Bad Request > Content-Length: 0 > Server: Microsoft-HTTPAPI/2.0 > Date: Mon, 28 Oct 2013 15:17:31 GMT -- This message was sent by Atlassian JIRA (v6.1#6144)