getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Gets the token68 of the challenge.
+ *
+ * This may be null if the challenge does not have a token68.
+ *
+ * @return The token68 of the challenge, or null if the challenge does not have a token68.
+ */
+ public String getToken68() {
+ return token68;
+ }
+}
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java
index 05a662da4d22..e0d4bd605b52 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java
@@ -3,10 +3,12 @@
package com.azure.core.util;
+import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.policy.HttpLogOptions;
import com.azure.core.http.rest.PagedResponse;
import com.azure.core.implementation.ImplUtils;
+import com.azure.core.implementation.http.AuthenticateChallengeParser;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.logging.LogLevel;
import org.reactivestreams.Publisher;
@@ -798,4 +800,34 @@ public static OffsetDateTime parseBestOffsetDateTime(String dateString) {
return OffsetDateTime.from(temporal);
}
}
+
+ /**
+ * Processes an authenticate header, such as {@link HttpHeaderName#WWW_AUTHENTICATE} or
+ * {@link HttpHeaderName#PROXY_AUTHENTICATE}, into a list of {@link AuthenticateChallenge}.
+ *
+ * If the {@code authenticateHeader} is null or empty an empty list will be returned.
+ *
+ * This method will parse the authenticate header as plainly as possible, meaning no casing will be changed on the
+ * scheme and no decoding will be done on the parameters. The only processing done is removal of quotes around
+ * parameter values and backslashes escaping values. Ex, {@code "va\"lue"} will be parsed as {@code va"lue}.
+ *
+ * In addition to processing as plainly as possible, this method will not validate the authenticate header, it will
+ * only parse it. Though, if the authenticate header has syntax errors an {@link IllegalStateException} will be
+ * thrown.
+ *
+ * A list of {@link AuthenticateChallenge} will be returned as it is valid for multiple authenticate challenges to
+ * use the same scheme, therefore a map cannot be used as the scheme would be the key and only one challenge would
+ * be stored.
+ *
+ * @param authenticateHeader The authenticate header to be parsed.
+ * @return A list of authenticate challenges.
+ * @throws IllegalArgumentException If the {@code authenticateHeader} has syntax errors.
+ */
+ public static List parseAuthenticateHeader(String authenticateHeader) {
+ if (isNullOrEmpty(authenticateHeader)) {
+ return Collections.emptyList();
+ }
+
+ return new AuthenticateChallengeParser(authenticateHeader).parse();
+ }
}
diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/util/CoreUtilsTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/util/CoreUtilsTests.java
index 78fd2291c906..39548d4a2cff 100644
--- a/sdk/core/azure-core/src/test/java/com/azure/core/util/CoreUtilsTests.java
+++ b/sdk/core/azure-core/src/test/java/com/azure/core/util/CoreUtilsTests.java
@@ -21,10 +21,11 @@
import java.time.OffsetDateTime;
import java.util.AbstractMap;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -38,6 +39,9 @@
import java.util.function.Function;
import java.util.stream.Stream;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -132,7 +136,7 @@ public void isNullOrEmptyCollection(Collection> collection, boolean expected)
private static Stream isNullOrEmptyCollectionSupplier() {
return Stream.of(Arguments.of(null, true), Arguments.of(new ArrayList<>(), true),
- Arguments.of(Collections.singletonList(1), false));
+ Arguments.of(singletonList(1), false));
}
@ParameterizedTest
@@ -235,8 +239,8 @@ private static Stream createHttpHeadersFromClientOptionsSupplier() {
Arguments.of(new ClientOptions(), null),
// ClientOptions contains a single header value, a single header HttpHeaders is returned.
- Arguments.of(new ClientOptions().setHeaders(Collections.singletonList(new Header("a", "header"))),
- new HttpHeaders(Collections.singletonMap("a", "header"))),
+ Arguments.of(new ClientOptions().setHeaders(singletonList(new Header("a", "header"))),
+ new HttpHeaders(singletonMap("a", "header"))),
// ClientOptions contains multiple header values, a multi-header HttpHeaders is returned.
Arguments.of(new ClientOptions().setHeaders(multipleHeadersList), new HttpHeaders(multipleHeadersMap)));
@@ -626,4 +630,66 @@ public void parseBestNoColonInTimezoneOffset() {
assertEquals(5, parsed.getSecond());
assertEquals(0, parsed.getOffset().getTotalSeconds());
}
+
+ @ParameterizedTest
+ @MethodSource("validParseAuthenticateHeaderSupplier")
+ @Execution(ExecutionMode.SAME_THREAD)
+ public void validParseAuthenticateHeader(String authenticationHeader, List expected) {
+ List actual = CoreUtils.parseAuthenticateHeader(authenticationHeader);
+
+ assertEquals(expected.size(), actual.size());
+ for (int i = 0; i < expected.size(); i++) {
+ assertEquals(expected.get(i).getScheme(), actual.get(i).getScheme());
+ assertEquals(expected.get(i).getParameters(), actual.get(i).getParameters());
+ assertEquals(expected.get(i).getToken68(), actual.get(i).getToken68());
+ }
+ }
+
+ private static Stream validParseAuthenticateHeaderSupplier() {
+ Map digestParams = new LinkedHashMap<>();
+ digestParams.put("nonce", "123");
+ digestParams.put("opaque", "123");
+ digestParams.put("qop", "123");
+ digestParams.put("algorithm", "SHA-256");
+
+ return Stream.of(Arguments.of(null, emptyList()), Arguments.of("", emptyList()),
+ Arguments.of("Basic", singletonList(new AuthenticateChallenge("Basic"))),
+ Arguments.of("Basic realm=\"test\"",
+ singletonList(new AuthenticateChallenge("Basic", singletonMap("realm", "test")))),
+ Arguments.of("Custom ABkd856gkslw-._~+/=",
+ singletonList(new AuthenticateChallenge("Custom", "ABkd856gkslw-._~+/="))),
+ Arguments.of("Digest nonce = \"123\", opaque=\"123\", qop=\"123\", algorithm=SHA-256",
+ singletonList(new AuthenticateChallenge("Digest", digestParams))),
+ Arguments.of("Digest nonce = \"123\", opaque=\"123\", qop=\"123\", algorithm=SHA-256, Basic realm=\"test\"",
+ Arrays.asList(new AuthenticateChallenge("Digest", digestParams),
+ new AuthenticateChallenge("Basic", singletonMap("realm", "test")))),
+ Arguments.of("Basic realm=\"test\", Basic realm=\"test2\"",
+ Arrays.asList(new AuthenticateChallenge("Basic", singletonMap("realm", "test")),
+ new AuthenticateChallenge("Basic", singletonMap("realm", "test2")))),
+ Arguments.of(" , ,, ,, Basic realm=\"test\"",
+ singletonList(new AuthenticateChallenge("Basic", singletonMap("realm", "test")))),
+ Arguments.of("Custom1,Custom2",
+ Arrays.asList(new AuthenticateChallenge("Custom1"), new AuthenticateChallenge("Custom2"))));
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidParseAuthenticateHeaderSupplier")
+ @Execution(ExecutionMode.SAME_THREAD)
+ public void invalidParseAuthenticateHeader(String authenticationHeader) {
+ assertThrows(IllegalArgumentException.class, () -> CoreUtils.parseAuthenticateHeader(authenticationHeader));
+ }
+
+ private static Stream invalidParseAuthenticateHeaderSupplier() {
+ return Stream.of("ABkd856gkslw-._~+/=", // token68 without scheme
+ "realm=\"test\"", // auth-param without scheme
+ "Custom ABkd856gkslw-._~+/, ABkd856gkslw-._~+/", // multiple token68s
+ "Custom ABkd856gkslw-._~+/, realm=\"test\"", // token68 and auth-param
+ "Custom realm=\"test\", ABkd856gkslw-._~+/", // auth-param and token68
+ "Custom/", // scheme with invalid character
+ "Custom ABkd856gkslw-._~+/!", // token68 with invalid character
+ "Custom realm=test/", // auth-param with invalid character
+ "Custom realm=test realm2=test2", // missing comma between auth-params
+ "Custom realm=\"" // missing closing quote for auth-param
+ );
+ }
}