From c0cf0c76542e2cc0ff83d515de144c52086ba217 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Fri, 23 May 2008 16:47:14 +0000 Subject: [PATCH] HTTPCLIENT-424: Preemptive authentication no longer limited to BASIC scheme only. HttpClient can be customized to authenticate preemptively with DIGEST scheme git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@659595 13f79535-47bb-0310-9956-ffa450edef68 --- RELEASE_NOTES.txt | 5 + .../ClientPreemptiveBasicAuthentication.java | 137 +++++++++++++++ .../ClientPreemptiveDigestAuthentication.java | 164 ++++++++++++++++++ .../http/client/params/ClientPNames.java | 8 - .../http/client/params/ClientParamBean.java | 4 - .../http/client/params/HttpClientParams.java | 29 ---- .../apache/http/impl/auth/DigestScheme.java | 31 +++- .../apache/http/impl/auth/RFC2617Scheme.java | 3 + 8 files changed, 337 insertions(+), 44 deletions(-) create mode 100644 module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveBasicAuthentication.java create mode 100644 module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveDigestAuthentication.java diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt index d33ccaff4..ff4b7c227 100644 --- a/RELEASE_NOTES.txt +++ b/RELEASE_NOTES.txt @@ -1,6 +1,11 @@ Changes since 4.0 Alpha 4 ------------------- +* [HTTPCLIENT-424] Preemptive authentication no longer limited to BASIC + scheme only. HttpClient can be customized to authenticate preemptively + with DIGEST scheme. + Contributed by Oleg Kalnichevski + * [HTTPCLIENT-670] Pluggable hostname resolver. Contributed by Oleg Kalnichevski diff --git a/module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveBasicAuthentication.java b/module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveBasicAuthentication.java new file mode 100644 index 000000000..c222442e8 --- /dev/null +++ b/module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveBasicAuthentication.java @@ -0,0 +1,137 @@ +/* + * $HeadURL:$ + * $Revision:$ + * $Date:$ + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +package org.apache.http.examples.client; + +import java.io.IOException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScheme; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthState; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.ExecutionContext; +import org.apache.http.protocol.HttpContext; + +/** + * An example of HttpClient can be customized to authenticate + * preemptively using BASIC scheme. + * + * Generally, preemptive authentication can be considered less + * secure than a response to an authentication challenge + * and therefore discouraged. + * + * This code is NOT officially supported! Use at your risk. + */ +public class ClientPreemptiveBasicAuthentication { + + public static void main(String[] args) throws Exception { + + DefaultHttpClient httpclient = new DefaultHttpClient(); + + httpclient.getCredentialsProvider().setCredentials( + new AuthScope("localhost", 80), + new UsernamePasswordCredentials("username", "password")); + + BasicHttpContext localcontext = new BasicHttpContext(); + + // Generate BASIC scheme object and stick it to the local + // execution context + BasicScheme basicAuth = new BasicScheme(); + localcontext.setAttribute("preemptive-auth", basicAuth); + + // Add as the first request interceptor + httpclient.addRequestInterceptor(new PreemptiveAuth(), 0); + + HttpHost targetHost = new HttpHost("localhost", 80, "http"); + + HttpGet httpget = new HttpGet("/"); + + System.out.println("executing request: " + httpget.getRequestLine()); + System.out.println("to target: " + targetHost); + + for (int i = 0; i < 3; i++) { + HttpResponse response = httpclient.execute(targetHost, httpget, localcontext); + HttpEntity entity = response.getEntity(); + + System.out.println("----------------------------------------"); + System.out.println(response.getStatusLine()); + if (entity != null) { + System.out.println("Response content length: " + entity.getContentLength()); + System.out.println("Chunked?: " + entity.isChunked()); + entity.consumeContent(); + } + } + } + + static class PreemptiveAuth implements HttpRequestInterceptor { + + public void process( + final HttpRequest request, + final HttpContext context) throws HttpException, IOException { + + AuthState authState = (AuthState) context.getAttribute( + ClientContext.TARGET_AUTH_STATE); + + // If no auth scheme avaialble yet, try to initialize it preemptively + if (authState.getAuthScheme() == null) { + AuthScheme authScheme = (AuthScheme) context.getAttribute( + "preemptive-auth"); + CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute( + ClientContext.CREDS_PROVIDER); + HttpHost targetHost = (HttpHost) context.getAttribute( + ExecutionContext.HTTP_TARGET_HOST); + if (authScheme != null) { + Credentials creds = credsProvider.getCredentials( + new AuthScope( + targetHost.getHostName(), + targetHost.getPort())); + if (creds == null) { + throw new HttpException("No credentials for preemptive authentication"); + } + authState.setAuthScheme(authScheme); + authState.setCredentials(creds); + } + } + + } + + } +} diff --git a/module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveDigestAuthentication.java b/module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveDigestAuthentication.java new file mode 100644 index 000000000..18c279a80 --- /dev/null +++ b/module-client/src/examples/org/apache/http/examples/client/ClientPreemptiveDigestAuthentication.java @@ -0,0 +1,164 @@ +/* + * $HeadURL:$ + * $Revision:$ + * $Date:$ + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +package org.apache.http.examples.client; + +import java.io.IOException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.auth.AuthScheme; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthState; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.impl.auth.DigestScheme; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.ExecutionContext; +import org.apache.http.protocol.HttpContext; + +/** + * An example of HttpClient can be customized to authenticate + * preemptively using DIGEST scheme. + * + * Generally, preemptive authentication can be considered less + * secure than a response to an authentication challenge + * and therefore discouraged. + * + * This code is NOT officially supported! Use at your risk. + */ +public class ClientPreemptiveDigestAuthentication { + + public static void main(String[] args) throws Exception { + + DefaultHttpClient httpclient = new DefaultHttpClient(); + + httpclient.getCredentialsProvider().setCredentials( + new AuthScope("localhost", 80), + new UsernamePasswordCredentials("username", "password")); + + BasicHttpContext localcontext = new BasicHttpContext(); + // Generate DIGEST scheme object, initialize it and stick it to + // the local execution context + DigestScheme digestAuth = new DigestScheme(); + // Suppose we already know the realm name + digestAuth.overrideParamter("realm", "some realm"); + // Suppose we already know the expected nonce value + digestAuth.overrideParamter("nonce", "whatever"); + localcontext.setAttribute("preemptive-auth", digestAuth); + + // Add as the first request interceptor + httpclient.addRequestInterceptor(new PreemptiveAuth(), 0); + // Add as the last response interceptor + httpclient.addResponseInterceptor(new PersistentDigest()); + + HttpHost targetHost = new HttpHost("localhost", 80, "http"); + + HttpGet httpget = new HttpGet("/"); + + System.out.println("executing request: " + httpget.getRequestLine()); + System.out.println("to target: " + targetHost); + + for (int i = 0; i < 3; i++) { + HttpResponse response = httpclient.execute(targetHost, httpget, localcontext); + HttpEntity entity = response.getEntity(); + + System.out.println("----------------------------------------"); + System.out.println(response.getStatusLine()); + if (entity != null) { + System.out.println("Response content length: " + entity.getContentLength()); + System.out.println("Chunked?: " + entity.isChunked()); + entity.consumeContent(); + } + } + } + + static class PreemptiveAuth implements HttpRequestInterceptor { + + public void process( + final HttpRequest request, + final HttpContext context) throws HttpException, IOException { + + AuthState authState = (AuthState) context.getAttribute( + ClientContext.TARGET_AUTH_STATE); + + // If no auth scheme avaialble yet, try to initialize it preemptively + if (authState.getAuthScheme() == null) { + AuthScheme authScheme = (AuthScheme) context.getAttribute( + "preemptive-auth"); + CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute( + ClientContext.CREDS_PROVIDER); + HttpHost targetHost = (HttpHost) context.getAttribute( + ExecutionContext.HTTP_TARGET_HOST); + if (authScheme != null) { + Credentials creds = credsProvider.getCredentials( + new AuthScope( + targetHost.getHostName(), + targetHost.getPort())); + if (creds == null) { + throw new HttpException("No credentials for preemptive authentication"); + } + authState.setAuthScheme(authScheme); + authState.setCredentials(creds); + } + } + + } + + } + + static class PersistentDigest implements HttpResponseInterceptor { + + public void process( + final HttpResponse response, + final HttpContext context) throws HttpException, IOException { + AuthState authState = (AuthState) context.getAttribute( + ClientContext.TARGET_AUTH_STATE); + if (authState != null) { + AuthScheme authScheme = authState.getAuthScheme(); + // Stick the auth scheme to the local context, so + // we could try to authenticate subsequent requests + // preemptively + if (authScheme instanceof DigestScheme) { + context.setAttribute("preemptive-auth", authScheme); + } + } + } + + } + +} diff --git a/module-client/src/main/java/org/apache/http/client/params/ClientPNames.java b/module-client/src/main/java/org/apache/http/client/params/ClientPNames.java index 2729d2fbb..33c307b82 100644 --- a/module-client/src/main/java/org/apache/http/client/params/ClientPNames.java +++ b/module-client/src/main/java/org/apache/http/client/params/ClientPNames.java @@ -101,14 +101,6 @@ public interface ClientPNames { */ public static final String HANDLE_AUTHENTICATION = "http.protocol.handle-authentication"; - /** - * Defines whether authentication should be attempted preemptively. - *

- * This parameter expects a value of type {@link Boolean}. - *

- */ - public static final String PREEMPTIVE_AUTHENTICATION = "http.protocol.authentication-preemptive"; - /** * Defines the name of the cookie specification to be used for HTTP state management. *

diff --git a/module-client/src/main/java/org/apache/http/client/params/ClientParamBean.java b/module-client/src/main/java/org/apache/http/client/params/ClientParamBean.java index b7ba4dba0..c48f1b113 100644 --- a/module-client/src/main/java/org/apache/http/client/params/ClientParamBean.java +++ b/module-client/src/main/java/org/apache/http/client/params/ClientParamBean.java @@ -73,10 +73,6 @@ public void setHandleAuthentication (final boolean handle) { params.setBooleanParameter(ClientPNames.HANDLE_AUTHENTICATION, handle); } - public void setPreemptiveAuthentication (final boolean preemptive) { - params.setBooleanParameter(ClientPNames.PREEMPTIVE_AUTHENTICATION, preemptive); - } - public void setCookiePolicy (final String policy) { params.setParameter(ClientPNames.COOKIE_POLICY, policy); } diff --git a/module-client/src/main/java/org/apache/http/client/params/HttpClientParams.java b/module-client/src/main/java/org/apache/http/client/params/HttpClientParams.java index d96426bab..e454702ef 100644 --- a/module-client/src/main/java/org/apache/http/client/params/HttpClientParams.java +++ b/module-client/src/main/java/org/apache/http/client/params/HttpClientParams.java @@ -79,35 +79,6 @@ public static void setAuthenticating(final HttpParams params, boolean value) { (ClientPNames.HANDLE_AUTHENTICATION, value); } - /** - * Returns true if authentication should be attempted preemptively, - * false otherwise. - * - * @return true if authentication should be attempted preemptively, - * false otherwise. - */ - public static boolean isAuthenticationPreemptive(final HttpParams params) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - return params.getBooleanParameter - (ClientPNames.PREEMPTIVE_AUTHENTICATION, false); - } - - /** - * Sets whether authentication should be attempted preemptively. - * - * @param value true if authentication should be attempted preemptively, - * false otherwise. - */ - public static void setAuthenticationPreemptive(final HttpParams params, boolean value) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - params.setBooleanParameter - (ClientPNames.PREEMPTIVE_AUTHENTICATION, value); - } - public static String getCookiePolicy(final HttpParams params) { if (params == null) { throw new IllegalArgumentException("HTTP parameters may not be null"); diff --git a/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java b/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java index aa2aa1cf1..ad6561f73 100644 --- a/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java +++ b/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java @@ -153,8 +153,8 @@ public void processChallenge( if (unsupportedQop && (qopVariant == QOP_MISSING)) { throw new MalformedChallengeException("None of the qop methods is supported"); } - - cnonce = createCnonce(); + // Reset cnonce + this.cnonce = null; this.complete = true; } @@ -191,6 +191,17 @@ public boolean isConnectionBased() { return false; } + public void overrideParamter(final String name, final String value) { + getParameters().put(name, value); + } + + private String getCnonce() { + if (this.cnonce == null) { + this.cnonce = createCnonce(); + } + return this.cnonce; + } + /** * Produces a digest authorization string for the given set of * {@link Credentials}, method name and URI. @@ -253,6 +264,15 @@ private String createDigest(final Credentials credentials) throws Authentication String nonce = getParameter("nonce"); String method = getParameter("methodname"); String algorithm = getParameter("algorithm"); + if (uri == null) { + throw new IllegalStateException("URI may not be null"); + } + if (realm == null) { + throw new IllegalStateException("Realm may not be null"); + } + if (nonce == null) { + throw new IllegalStateException("Nonce may not be null"); + } // If an algorithm is not specified, default to MD5. if (algorithm == null) { algorithm = "MD5"; @@ -282,12 +302,15 @@ private String createDigest(final Credentials credentials) throws Authentication tmp.append(pwd); // unq(username-value) ":" unq(realm-value) ":" passwd String a1 = tmp.toString(); + //a1 is suitable for MD5 algorithm if(algorithm.equals("MD5-sess")) { // H( unq(username-value) ":" unq(realm-value) ":" passwd ) // ":" unq(nonce-value) // ":" unq(cnonce-value) + String cnonce = getCnonce(); + String tmp2=encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset))); StringBuilder tmp3 = new StringBuilder(tmp2.length() + nonce.length() + cnonce.length() + 2); tmp3.append(tmp2); @@ -323,6 +346,8 @@ private String createDigest(final Credentials credentials) throws Authentication serverDigestValue = tmp2.toString(); } else { String qopOption = getQopVariantString(); + String cnonce = getCnonce(); + StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5); tmp2.append(md5a1); @@ -384,7 +409,7 @@ private Header createDigestHeader( if (qopVariant != QOP_MISSING) { params.add(new BasicNameValuePair("qop", getQopVariantString())); params.add(new BasicNameValuePair("nc", NC)); - params.add(new BasicNameValuePair("cnonce", this.cnonce)); + params.add(new BasicNameValuePair("cnonce", getCnonce())); } if (algorithm != null) { params.add(new BasicNameValuePair("algorithm", algorithm)); diff --git a/module-client/src/main/java/org/apache/http/impl/auth/RFC2617Scheme.java b/module-client/src/main/java/org/apache/http/impl/auth/RFC2617Scheme.java index b5aebf83e..3d3e94bc4 100644 --- a/module-client/src/main/java/org/apache/http/impl/auth/RFC2617Scheme.java +++ b/module-client/src/main/java/org/apache/http/impl/auth/RFC2617Scheme.java @@ -84,6 +84,9 @@ protected void parseChallenge( * @return the map of authentication parameters */ protected Map getParameters() { + if (this.params == null) { + this.params = new HashMap(); + } return this.params; }