You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@cxf.apache.org by "Evgeny Shakin (JIRA)" <ji...@apache.org> on 2013/10/30 10:40:26 UTC

[jira] [Comment Edited] (CXF-5366) Authorization header is not set correctly in CXF HTTP digest authentication

    [ 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)