You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ab...@apache.org on 2024/11/03 14:50:14 UTC
(httpcomponents-client) branch master updated: Added NextNonceInterceptor to check the `Authentication-Info` header in HTTP Digest Access Authentication, extracting the `nextnonce` parameter when present and storing it in `HttpClientContext`. This supports compliance with RFC 7616, enhancing client authentication continuity. (#596)
This is an automated email from the ASF dual-hosted git repository.
abernal pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git
The following commit(s) were added to refs/heads/master by this push:
new 40d6ba4ee Added NextNonceInterceptor to check the `Authentication-Info` header in HTTP Digest Access Authentication, extracting the `nextnonce` parameter when present and storing it in `HttpClientContext`. This supports compliance with RFC 7616, enhancing client authentication continuity. (#596)
40d6ba4ee is described below
commit 40d6ba4ee5e39a00461a30503ff9aa98a9c8f0a2
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Sun Nov 3 15:50:09 2024 +0100
Added NextNonceInterceptor to check the `Authentication-Info` header in HTTP Digest Access Authentication, extracting the `nextnonce` parameter when present and storing it in `HttpClientContext`. This supports compliance with RFC 7616, enhancing client authentication continuity. (#596)
---
.../hc/client5/http/impl/auth/DigestScheme.java | 11 ++
.../client5/http/protocol/HttpClientContext.java | 39 ++++++
.../http/protocol/NextNonceInterceptor.java | 142 +++++++++++++++++++++
.../client5/http/impl/auth/TestDigestScheme.java | 59 +++++++++
.../http/protocol/TestNextNonceInterceptor.java | 106 +++++++++++++++
5 files changed, 357 insertions(+)
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
index ec9ea048e..85ed712d2 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
@@ -68,6 +68,7 @@ import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.PercentCodec;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.CharArrayBuffer;
+import org.apache.hc.core5.util.TextUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -253,6 +254,16 @@ public class DigestScheme implements AuthScheme, Serializable {
if (this.paramMap.get("nonce") == null) {
throw new AuthenticationException("missing nonce");
}
+
+ if (context != null) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String nextNonce = clientContext.getNextNonce();
+ if (!TextUtils.isBlank(nextNonce)) {
+ this.paramMap.put("nonce", nextNonce);
+ clientContext.setNextNonce(null);
+ }
+ }
+
return createDigestResponse(request);
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java
index f58a8042b..575f67cd6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/HttpClientContext.java
@@ -202,6 +202,23 @@ public class HttpClientContext extends HttpCoreContext {
private Object userToken;
private RequestConfig requestConfig;
+ /**
+ * Stores the {@code nextnonce} value provided by the server in an HTTP response.
+ * <p>
+ * In the context of HTTP Digest Access Authentication, the {@code nextnonce} parameter
+ * is used by the client in subsequent requests to ensure one-time or session-bound usage
+ * of nonce values, enhancing security by preventing replay attacks.
+ * </p>
+ * <p>
+ * This field is set by an interceptor or other component that processes the server's
+ * response containing the {@code Authentication-Info} header. Once used, this value
+ * may be cleared from the context to avoid reuse.
+ * </p>
+ *
+ * @since 5.5
+ */
+ private String nextNonce;
+
public HttpClientContext(final HttpContext context) {
super(context);
}
@@ -449,6 +466,28 @@ public class HttpClientContext extends HttpCoreContext {
this.exchangeId = exchangeId;
}
+ /**
+ * Retrieves the stored {@code nextnonce} value.
+ *
+ * @return the {@code nextnonce} parameter value, or {@code null} if not set
+ * @since 5.5
+ */
+ @Internal
+ public String getNextNonce() {
+ return nextNonce;
+ }
+
+ /**
+ * Sets the {@code nextnonce} value directly as an instance attribute.
+ *
+ * @param nextNonce the nonce value to set
+ * @since 5.5
+ */
+ @Internal
+ public void setNextNonce(final String nextNonce) {
+ this.nextNonce = nextNonce;
+ }
+
/**
* Internal adaptor class that delegates all its method calls to a plain {@link HttpContext}.
* To be removed in the future.
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/NextNonceInterceptor.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/NextNonceInterceptor.java
new file mode 100644
index 000000000..a5bb2a471
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/NextNonceInterceptor.java
@@ -0,0 +1,142 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.protocol;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.FormattedHeader;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.message.ParserCursor;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.CharArrayBuffer;
+import org.apache.hc.core5.util.TextUtils;
+import org.apache.hc.core5.util.Tokenizer;
+
+/**
+ * {@code NextNonceInterceptor} is an HTTP response interceptor that extracts the {@code nextnonce}
+ * parameter from the {@code Authentication-Info} header of an HTTP response. This parameter is used
+ * in HTTP Digest Access Authentication to provide an additional nonce value that the client is expected
+ * to use in subsequent authentication requests. By retrieving and storing this {@code nextnonce} value,
+ * the interceptor facilitates one-time nonce implementations and prevents replay attacks by ensuring that
+ * each request/response interaction includes a fresh nonce.
+ * <p>
+ * If present, the extracted {@code nextnonce} value is stored in the {@link HttpContext} under the attribute
+ * {@code auth-nextnonce}, allowing it to be accessed in subsequent requests. If the header does not contain
+ * the {@code nextnonce} parameter, no context attribute is set.
+ * </p>
+ *
+ * <p>This implementation adheres to the HTTP/1.1 specification, particularly focusing on the {@code Digest}
+ * scheme as defined in HTTP Digest Authentication, and parses header tokens using the {@link Tokenizer}
+ * utility class for robust token parsing.</p>
+ *
+ * <p>In the context of HTTP Digest Access Authentication, the {@code nextnonce} parameter is
+ * a critical part of the security mechanism, designed to mitigate replay attacks and enhance mutual
+ * authentication security. It provides the server with the ability to set and enforce single-use or
+ * session-bound nonces, prompting the client to use the provided {@code nextnonce} in its next request.
+ * This setup helps secure communication by forcing new cryptographic material in each transaction.
+ * </p>
+ *
+ * <p>This interceptor is stateless and thread-safe, making it suitable for use across multiple
+ * threads and HTTP requests. It should be registered with the HTTP client to enable support
+ * for advanced authentication mechanisms that require tracking of nonce values.</p>
+ *
+ * @since 5.5
+ */
+
+@Contract(threading = ThreadingBehavior.STATELESS)
+public class NextNonceInterceptor implements HttpResponseInterceptor {
+
+ public static final HttpResponseInterceptor INSTANCE = new NextNonceInterceptor();
+
+ private final Tokenizer tokenParser = Tokenizer.INSTANCE;
+
+
+ private static final String AUTHENTICATION_INFO_HEADER = "Authentication-Info";
+
+ private static final Tokenizer.Delimiter TOKEN_DELIMS = Tokenizer.delimiters('=', ',');
+ private static final Tokenizer.Delimiter VALUE_DELIMS = Tokenizer.delimiters(',');
+
+ /**
+ * Processes the HTTP response and extracts the {@code nextnonce} parameter from the
+ * {@code Authentication-Info} header if available, storing it in the provided {@code context}.
+ *
+ * @param response the HTTP response containing the {@code Authentication-Info} header
+ * @param entity the response entity, ignored by this interceptor
+ * @param context the HTTP context in which to store the {@code nextnonce} parameter
+ * @throws NullPointerException if either {@code response} or {@code context} is null
+ */
+ @Override
+ public void process(final HttpResponse response, final EntityDetails entity, final HttpContext context) {
+ Args.notNull(response, "HTTP response");
+ Args.notNull(context, "HTTP context");
+
+ final Header header = response.getFirstHeader(AUTHENTICATION_INFO_HEADER);
+ if (header != null) {
+ final String nextNonce;
+ if (header instanceof FormattedHeader) {
+ final CharSequence buf = ((FormattedHeader) header).getBuffer();
+ final ParserCursor cursor = new ParserCursor(((FormattedHeader) header).getValuePos(), buf.length());
+ nextNonce = parseNextNonce(buf, cursor);
+ } else {
+ final CharSequence headerValue = header.getValue();
+ final ParserCursor cursor = new ParserCursor(0, headerValue.length());
+ nextNonce = parseNextNonce(headerValue, cursor);
+ }
+ if (!TextUtils.isBlank(nextNonce)) {
+ HttpClientContext.castOrCreate(context).setNextNonce(nextNonce);
+ }
+ }
+ }
+
+ /**
+ * Parses the {@code Authentication-Info} header content represented by a {@link CharArrayBuffer}
+ * to extract the {@code nextnonce} parameter.
+ *
+ * @param buffer the {@link CharArrayBuffer} containing the value of the {@code Authentication-Info} header
+ * @param cursor the {@link ParserCursor} used to navigate through the buffer content
+ * @return the extracted {@code nextnonce} parameter value, or {@code null} if the parameter is not found
+ */
+ private String parseNextNonce(final CharSequence buffer, final ParserCursor cursor) {
+ while (!cursor.atEnd()) {
+ final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
+ if ("nextnonce".equals(name)) {
+ cursor.updatePos(cursor.getPos() + 1);
+ return tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
+ }
+ if (!cursor.atEnd()) {
+ cursor.updatePos(cursor.getPos() + 1);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
index 603d8ccc4..af8204b85 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
@@ -45,6 +45,7 @@ import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.auth.MalformedChallengeException;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HeaderElement;
@@ -928,4 +929,62 @@ class TestDigestScheme {
Assertions.assertNotNull(table.get("rspauth"));
}
+ @Test
+ void testNextNonceUsageFromContext() throws Exception {
+ final HttpRequest request = new BasicHttpRequest("GET", "/");
+ final HttpHost host = new HttpHost("somehost", 80);
+ final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
+ .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
+ .build();
+
+ final HttpClientContext context = HttpClientContext.create();
+ context.setNextNonce("sampleNextNonce"); // Set `nextNonce` in the context
+
+ final DigestScheme authscheme = new DigestScheme();
+ final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"initialNonce\"";
+ final AuthChallenge authChallenge = parse(challenge);
+ authscheme.processChallenge(authChallenge, context);
+
+ // Simulate credentials validation and generate auth response
+ authscheme.isResponseReady(host, credentialsProvider, context);
+ final String authResponse = authscheme.generateAuthResponse(host, request, context);
+
+ // Validate that `sampleNextNonce` (from context) is used in place of the initial nonce
+ final Map<String, String> paramMap = parseAuthResponse(authResponse);
+ Assertions.assertEquals("sampleNextNonce", paramMap.get("nonce"), "The nonce should match 'auth-nextnonce' from the context.");
+
+ // Verify that `auth-nextnonce` was removed from context after use
+ Assertions.assertNull(context.getAttribute("auth-nextnonce"), "The next nonce should be removed from the context after use.");
+ }
+
+
+ @Test
+ void testNoNextNonceUsageFromContext() throws Exception {
+ final HttpRequest request = new BasicHttpRequest("GET", "/");
+ final HttpHost host = new HttpHost("somehost", 80);
+ final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
+ .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
+ .build();
+
+ final HttpClientContext context = HttpClientContext.create();
+
+ // Initialize DigestScheme without setting any `nextNonce`
+ final DigestScheme authscheme = new DigestScheme();
+ final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"initialNonce\"";
+ final AuthChallenge authChallenge = parse(challenge);
+ authscheme.processChallenge(authChallenge, context);
+
+ // Simulate credentials validation and generate auth response
+ authscheme.isResponseReady(host, credentialsProvider, context);
+ final String authResponse = authscheme.generateAuthResponse(host, request, context);
+
+ // Validate that the nonce in the response matches the initial nonce, not a nextNonce
+ final Map<String, String> paramMap = parseAuthResponse(authResponse);
+ Assertions.assertEquals("initialNonce", paramMap.get("nonce"), "The nonce should match the initial nonce provided in the challenge.");
+
+ // Verify that the context does not contain any `nextNonce` value set
+ Assertions.assertNull(context.getAttribute("auth-nextnonce"), "The context should not contain a nextNonce attribute.");
+ }
+
+
}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestNextNonceInterceptor.java b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestNextNonceInterceptor.java
new file mode 100644
index 000000000..c6f3b0868
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestNextNonceInterceptor.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.protocol;
+
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestNextNonceInterceptor {
+
+ private static final String AUTHENTICATION_INFO_HEADER = "Authentication-Info";
+
+ private NextNonceInterceptor interceptor;
+ private HttpClientContext context;
+
+ @BeforeEach
+ void setUp() {
+ interceptor = new NextNonceInterceptor();
+ context = HttpClientContext.create();
+ }
+
+ @Test
+ void testNoAuthenticationInfoHeader() {
+ final HttpResponse response = new BasicHttpResponse(200);
+
+ interceptor.process(response, null, context);
+
+ Assertions.assertNull(context.getNextNonce(),
+ "Context should not contain nextnonce when the header is missing");
+ }
+
+ @Test
+ void testAuthenticationInfoHeaderWithoutNextNonce() {
+ final HttpResponse response = new BasicHttpResponse(200);
+ response.addHeader(new BasicHeader(AUTHENTICATION_INFO_HEADER, "auth-param=value"));
+
+ interceptor.process(response, null, context);
+
+ Assertions.assertNull(context.getNextNonce(),
+ "Context should not contain nextnonce when it is missing in the header value");
+ }
+
+ @Test
+ void testAuthenticationInfoHeaderWithNextNonce() {
+ final HttpResponse response = new BasicHttpResponse(200);
+ response.addHeader(new BasicHeader(AUTHENTICATION_INFO_HEADER, "nextnonce=\"10024b2308596a55d02699c0a0400fb4\",qop=auth,rspauth=\"0386df3cb9effdf08c9e00ab955827f3\",cnonce=\"21558090\",nc=00000001"));
+
+ interceptor.process(response, null, context);
+
+ Assertions.assertEquals("10024b2308596a55d02699c0a0400fb4", context.getNextNonce(),
+ "Context should contain the correct nextnonce value when it is present in the header");
+ }
+
+ @Test
+ void testMultipleAuthenticationInfoHeaders() {
+ final HttpResponse response = new BasicHttpResponse(200);
+ response.addHeader(new BasicHeader(AUTHENTICATION_INFO_HEADER, "auth-param=value")); // First header without nextnonce
+ response.addHeader(new BasicHeader(AUTHENTICATION_INFO_HEADER, "nextnonce=\"10024b2308596a55d02699c0a0400fb4\",qop=auth,rspauth=\"0386df3cb9effdf08c9e00ab955827f3\",cnonce=\"21558090\",nc=00000001")); // Second header with nextnonce
+
+ interceptor.process(response, null, context);
+
+ // Since only the first header is processed, `auth-nextnonce` should not be set in the context
+ Assertions.assertNull(context.getNextNonce(),
+ "Context should not contain nextnonce if it's not in the first Authentication-Info header");
+ }
+
+ @Test
+ void testAuthenticationInfoHeaderWithEmptyNextNonce() {
+ final HttpResponse response = new BasicHttpResponse(200);
+ response.addHeader(new BasicHeader(AUTHENTICATION_INFO_HEADER, "nextnonce=\"\",qop=auth,rspauth=\"0386df3cb9effdf08c9e00ab955827f3\",cnonce=\"21558090\",nc=00000001"));
+
+ interceptor.process(response, null, context);
+
+ Assertions.assertNull(context.getNextNonce(),
+ "Context should not contain nextnonce if it is empty in the Authentication-Info header");
+ }
+
+}