diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdaa6be84..1faa2086e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.5.0] - 2024-09-30
+
+### Added
+- Adds an `AuthorizationHandler` that authenticates requests using a provided `BaseBearerTokenAuthenticationProvider`. Opting in to use this middleware can be done
+via `KiotaClientFactory.create(authProvider)`.
+
+
## [1.4.0] - 2024-09-11
### Changed
diff --git a/README.md b/README.md
index 960a94716..06e7245c8 100644
--- a/README.md
+++ b/README.md
@@ -21,14 +21,14 @@ Read more about Kiota [here](https://github.com/microsoft/kiota/blob/main/README
In `build.gradle` in the `dependencies` section:
```Groovy
-implementation 'com.microsoft.kiota:microsoft-kiota-abstractions:1.3.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-authentication-azure:1.3.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-http-okHttp:1.3.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-serialization-json:1.3.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-serialization-text:1.3.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-serialization-form:1.3.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-serialization-multipart:1.3.0'
-implementation 'com.microsoft.kiota:microsoft-kiota-bundle:1.3.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-abstractions:1.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-authentication-azure:1.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-http-okHttp:1.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-json:1.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-text:1.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-form:1.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-serialization-multipart:1.5.0'
+implementation 'com.microsoft.kiota:microsoft-kiota-bundle:1.5.0'
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
```
@@ -40,37 +40,37 @@ In `pom.xml` in the `dependencies` section:
com.microsoft.kiota
microsoft-kiota-abstractions
- 1.3.0
+ 1.5.0
com.microsoft.kiota
microsoft-kiota-authentication-azure
- 1.3.0
+ 1.5.0
com.microsoft.kiota
microsoft-kiota-http-okHttp
- 1.3.0
+ 1.5.0
com.microsoft.kiota
microsoft-kiota-serialization-json
- 1.3.0
+ 1.5.0
com.microsoft.kiota
microsoft-kiota-serialization-text
- 1.3.0
+ 1.5.0
com.microsoft.kiota
microsoft-kiota-serialization-form
- 1.3.0
+ 1.5.0
com.microsoft.kiota
microsoft-kiota-serialization-multipart
- 1.3.0
+ 1.5.0
jakarta.annotation
diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/authentication/BaseBearerTokenAuthenticationProvider.java b/components/abstractions/src/main/java/com/microsoft/kiota/authentication/BaseBearerTokenAuthenticationProvider.java
index d8dda1c47..819c63bf9 100644
--- a/components/abstractions/src/main/java/com/microsoft/kiota/authentication/BaseBearerTokenAuthenticationProvider.java
+++ b/components/abstractions/src/main/java/com/microsoft/kiota/authentication/BaseBearerTokenAuthenticationProvider.java
@@ -50,4 +50,8 @@ public void authenticateRequest(
}
}
}
+
+ public @Nonnull AccessTokenProvider getAccessTokenProvider() {
+ return this.accessTokenProvider;
+ }
}
diff --git a/components/http/okHttp/spotBugsExcludeFilter.xml b/components/http/okHttp/spotBugsExcludeFilter.xml
index 1ab81c79f..a242f453f 100644
--- a/components/http/okHttp/spotBugsExcludeFilter.xml
+++ b/components/http/okHttp/spotBugsExcludeFilter.xml
@@ -29,7 +29,7 @@
-
+
@@ -37,6 +37,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/ContinuousAccessEvaluationClaims.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/ContinuousAccessEvaluationClaims.java
new file mode 100644
index 000000000..96295ed84
--- /dev/null
+++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/ContinuousAccessEvaluationClaims.java
@@ -0,0 +1,60 @@
+package com.microsoft.kiota.http;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+
+import okhttp3.Response;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to extract the claims from the WWW-Authenticate header in a response.
+ * https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-continuous-access-evaluation
+ */
+public final class ContinuousAccessEvaluationClaims {
+
+ private static final Pattern bearerPattern =
+ Pattern.compile("^Bearer\\s.*", Pattern.CASE_INSENSITIVE);
+ private static final Pattern claimsPattern =
+ Pattern.compile("\\s?claims=\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
+
+ private static final String WWW_AUTHENTICATE_HEADER = "WWW-Authenticate";
+
+ private ContinuousAccessEvaluationClaims() {}
+
+ /**
+ * Extracts the claims from the WWW-Authenticate header in a response.
+ * @param response the response to extract the claims from.
+ * @return the claims
+ */
+ public static @Nullable String getClaimsFromResponse(@Nonnull Response response) {
+ Objects.requireNonNull(response, "parameter response cannot be null");
+ if (response.code() != 401) {
+ return null;
+ }
+ final List authenticateHeader = response.headers(WWW_AUTHENTICATE_HEADER);
+ if (!authenticateHeader.isEmpty()) {
+ String rawHeaderValue = null;
+ for (final String authenticateEntry : authenticateHeader) {
+ final Matcher matcher = bearerPattern.matcher(authenticateEntry);
+ if (matcher.matches()) {
+ rawHeaderValue = authenticateEntry.replaceFirst("^Bearer\\s", "");
+ break;
+ }
+ }
+ if (rawHeaderValue != null) {
+ final String[] parameters = rawHeaderValue.split(",");
+ for (final String parameter : parameters) {
+ final Matcher matcher = claimsPattern.matcher(parameter);
+ if (matcher.matches()) {
+ return matcher.group(1);
+ }
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java
index cb81973c8..5f86565e4 100644
--- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java
+++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java
@@ -1,5 +1,7 @@
package com.microsoft.kiota.http;
+import com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider;
+import com.microsoft.kiota.http.middleware.AuthorizationHandler;
import com.microsoft.kiota.http.middleware.HeadersInspectionHandler;
import com.microsoft.kiota.http.middleware.ParametersNameDecodingHandler;
import com.microsoft.kiota.http.middleware.RedirectHandler;
@@ -13,6 +15,9 @@
import okhttp3.OkHttpClient;
import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
/** This class is used to build the HttpClient instance used by the core service. */
public class KiotaClientFactory {
@@ -23,7 +28,7 @@ private KiotaClientFactory() {}
* @return an OkHttpClient Builder instance.
*/
@Nonnull public static OkHttpClient.Builder create() {
- return create(null);
+ return create(createDefaultInterceptors());
}
/**
@@ -48,6 +53,31 @@ private KiotaClientFactory() {}
return builder;
}
+ /**
+ * Creates an OkHttpClient Builder with the default configuration and middleware.
+ * @param interceptors The interceptors to add to the client. Will default to createDefaultInterceptors() if null.
+ * @return an OkHttpClient Builder instance.
+ */
+ @Nonnull public static OkHttpClient.Builder create(@Nullable final List interceptors) {
+ if (interceptors == null) {
+ return create();
+ }
+ return create(
+ (new ArrayList<>(interceptors)).toArray(new Interceptor[interceptors.size()]));
+ }
+
+ /**
+ * Creates an OkHttpClient Builder with the default configuration and middleware including the AuthorizationHandler.
+ * @param authenticationProvider authentication provider to use for the AuthorizationHandler.
+ * @return an OkHttpClient Builder instance.
+ */
+ @Nonnull public static OkHttpClient.Builder create(
+ @Nonnull final BaseBearerTokenAuthenticationProvider authenticationProvider) {
+ ArrayList interceptors = new ArrayList<>(createDefaultInterceptorsAsList());
+ interceptors.add(new AuthorizationHandler(authenticationProvider));
+ return create(interceptors);
+ }
+
/**
* Creates the default interceptors for the client.
* @return an array of interceptors.
@@ -61,4 +91,12 @@ private KiotaClientFactory() {}
new HeadersInspectionHandler()
};
}
+
+ /**
+ * Creates the default interceptors for the client.
+ * @return an array of interceptors.
+ */
+ @Nonnull public static List createDefaultInterceptorsAsList() {
+ return new ArrayList<>(Arrays.asList(createDefaultInterceptors()));
+ }
}
diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java
index 028afe64c..b634cf7da 100644
--- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java
+++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java
@@ -48,7 +48,6 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** RequestAdapter implementation for OkHttp */
@@ -753,11 +752,6 @@ private String getHeaderValue(final Response response, String key) {
return null;
}
- private static final Pattern bearerPattern =
- Pattern.compile("^Bearer\\s.*", Pattern.CASE_INSENSITIVE);
- private static final Pattern claimsPattern =
- Pattern.compile("\\s?claims=\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
-
/** Key used for events when an authentication challenge is returned by the API */
@Nonnull public static final String authenticateChallengedEventKey =
"com.microsoft.kiota.authenticate_challenge_received";
@@ -804,26 +798,7 @@ String getClaimsFromResponse(
&& (claims == null || claims.isEmpty())
&& // we avoid infinite loops and retry only once
(requestInfo.content == null || requestInfo.content.markSupported())) {
- final List authenticateHeader = response.headers("WWW-Authenticate");
- if (!authenticateHeader.isEmpty()) {
- String rawHeaderValue = null;
- for (final String authenticateEntry : authenticateHeader) {
- final Matcher matcher = bearerPattern.matcher(authenticateEntry);
- if (matcher.matches()) {
- rawHeaderValue = authenticateEntry.replaceFirst("^Bearer\\s", "");
- break;
- }
- }
- if (rawHeaderValue != null) {
- final String[] parameters = rawHeaderValue.split(",");
- for (final String parameter : parameters) {
- final Matcher matcher = claimsPattern.matcher(parameter);
- if (matcher.matches()) {
- return matcher.group(1);
- }
- }
- }
- }
+ return ContinuousAccessEvaluationClaims.getClaimsFromResponse(response);
}
return null;
}
diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/TelemetrySemanticConventions.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/TelemetrySemanticConventions.java
index d5546409a..6fdb412fc 100644
--- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/TelemetrySemanticConventions.java
+++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/TelemetrySemanticConventions.java
@@ -5,30 +5,79 @@
import io.opentelemetry.api.common.AttributeKey;
+/**
+ * This class contains the telemetry attribute keys used by this library.
+ */
public final class TelemetrySemanticConventions {
private TelemetrySemanticConventions() {}
// https://opentelemetry.io/docs/specs/semconv/attributes-registry/
+
+ /**
+ * HTTP Response status code
+ */
public static final AttributeKey HTTP_RESPONSE_STATUS_CODE =
longKey("http.response.status_code"); // stable
+
+ /**
+ * HTTP Request resend count
+ */
public static final AttributeKey HTTP_REQUEST_RESEND_COUNT =
longKey("http.request.resend_count"); // stable
+
+ /**
+ * HTTP Request method
+ */
public static final AttributeKey HTTP_REQUEST_METHOD =
stringKey("http.request.method"); // stable
+
+ /**
+ * Network connection protocol version
+ */
public static final AttributeKey NETWORK_PROTOCOL_VERSION =
stringKey("network.protocol.version"); // stable
+
+ /**
+ * Full HTTP request URL
+ */
public static final AttributeKey URL_FULL = stringKey("url.full"); // stable
+
+ /**
+ * HTTP request URL scheme
+ */
public static final AttributeKey URL_SCHEME = stringKey("url.scheme"); // stable
+
+ /**
+ * HTTP request destination server address
+ */
public static final AttributeKey SERVER_ADDRESS = stringKey("server.address"); // stable
+
+ /**
+ * HTTP request destination server port
+ */
public static final AttributeKey SERVER_PORT = longKey("server.port"); // stable
+ /**
+ * HTTP response body size
+ */
public static final AttributeKey EXPERIMENTAL_HTTP_RESPONSE_BODY_SIZE =
longKey("http.response.body.size"); // experimental
+
+ /**
+ * HTTP request body size
+ */
public static final AttributeKey EXPERIMENTAL_HTTP_REQUEST_BODY_SIZE =
longKey("http.request.body.size"); // experimental
+ /**
+ * HTTP response content type
+ */
public static final AttributeKey CUSTOM_HTTP_RESPONSE_CONTENT_TYPE =
stringKey("http.response_content_type"); // custom
+
+ /**
+ * HTTP request content type
+ */
public static final AttributeKey CUSTOM_HTTP_REQUEST_CONTENT_TYPE =
stringKey("http.request_content_type"); // custom
}
diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/AuthorizationHandler.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/AuthorizationHandler.java
new file mode 100644
index 000000000..c80b16177
--- /dev/null
+++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/AuthorizationHandler.java
@@ -0,0 +1,140 @@
+package com.microsoft.kiota.http.middleware;
+
+import static com.microsoft.kiota.http.TelemetrySemanticConventions.HTTP_REQUEST_RESEND_COUNT;
+
+import com.microsoft.kiota.authentication.AccessTokenProvider;
+import com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider;
+import com.microsoft.kiota.http.ContinuousAccessEvaluationClaims;
+import com.microsoft.kiota.http.ObservabilityOptions;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Scope;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This interceptor is responsible for adding the Authorization header to the request
+ * if the header is not already present. It also handles Continuous Access Evaluation (CAE) claims
+ * challenges if the token request was made using this interceptor. It does this using the provided AuthenticationProvider
+ */
+public class AuthorizationHandler implements Interceptor {
+
+ @Nonnull private final BaseBearerTokenAuthenticationProvider authenticationProvider;
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+
+ /**
+ * Instantiates a new AuthorizationHandler.
+ * @param authenticationProvider the authentication provider.
+ */
+ public AuthorizationHandler(
+ @Nonnull final BaseBearerTokenAuthenticationProvider authenticationProvider) {
+ this.authenticationProvider =
+ Objects.requireNonNull(
+ authenticationProvider, "AuthenticationProvider cannot be null");
+ }
+
+ @Override
+ @SuppressWarnings("UnknownNullness")
+ public @Nonnull Response intercept(final Chain chain) throws IOException {
+ Objects.requireNonNull(chain, "parameter chain cannot be null");
+ final Request request = chain.request();
+
+ final Span span =
+ GlobalOpenTelemetry.getTracer(
+ (new ObservabilityOptions()).getTracerInstrumentationName())
+ .spanBuilder("AuthorizationHandler_Intercept")
+ .startSpan();
+ Scope scope = null;
+ if (span != null) {
+ scope = span.makeCurrent();
+ span.setAttribute("com.microsoft.kiota.handler.authorization.enable", true);
+ }
+
+ try {
+ // Auth provider already added auth header
+ if (request.headers().names().contains(AUTHORIZATION_HEADER)) {
+ if (span != null)
+ span.setAttribute(
+ "com.microsoft.kiota.handler.authorization.token_present", true);
+ return chain.proceed(request);
+ }
+
+ final HashMap additionalContext = new HashMap<>();
+ additionalContext.put("parent-span", span);
+ final Request authenticatedRequest =
+ authenticateRequest(request, additionalContext, span);
+ final Response response = chain.proceed(authenticatedRequest);
+
+ if (response != null && response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
+ return response;
+ }
+
+ // Attempt CAE claims challenge
+ final String claims = ContinuousAccessEvaluationClaims.getClaimsFromResponse(response);
+ if (claims == null || claims.isEmpty()) {
+ return response;
+ }
+
+ if (span != null)
+ span.addEvent("com.microsoft.kiota.handler.authorization.challenge_received");
+
+ // We cannot replay one-shot requests after claims challenge
+ final RequestBody requestBody = request.body();
+ if (requestBody != null && requestBody.isOneShot()) {
+ return response;
+ }
+
+ response.close();
+ additionalContext.put("claims", claims);
+ // Retry claims challenge only once
+ if (span != null) {
+ span.setAttribute(HTTP_REQUEST_RESEND_COUNT, 1);
+ }
+ final Request authenticatedRequestAfterCAE =
+ authenticateRequest(request, additionalContext, span);
+ return chain.proceed(authenticatedRequestAfterCAE);
+ } finally {
+ if (scope != null) {
+ scope.close();
+ }
+ if (span != null) {
+ span.end();
+ }
+ }
+ }
+
+ private @Nonnull Request authenticateRequest(
+ @Nonnull final Request request,
+ @Nullable final Map additionalAuthenticationContext,
+ final Span span) {
+
+ final AccessTokenProvider accessTokenProvider =
+ authenticationProvider.getAccessTokenProvider();
+ if (!accessTokenProvider.getAllowedHostsValidator().isUrlHostValid(request.url().uri())) {
+ return request;
+ }
+ final String accessToken =
+ accessTokenProvider.getAuthorizationToken(
+ request.url().uri(), additionalAuthenticationContext);
+ if (accessToken != null && !accessToken.isEmpty()) {
+ span.setAttribute("com.microsoft.kiota.handler.authorization.token_obtained", true);
+ final Request.Builder requestBuilder = request.newBuilder();
+ requestBuilder.addHeader(AUTHORIZATION_HEADER, "Bearer " + accessToken);
+ return requestBuilder.build();
+ }
+ return request;
+ }
+}
diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java
index c8dba6011..910546e9a 100644
--- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java
+++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java
@@ -13,7 +13,7 @@ public UserAgentHandlerOption() {}
private boolean enabled = true;
@Nonnull private String productName = "kiota-java";
- @Nonnull private String productVersion = "1.4.0";
+ @Nonnull private String productVersion = "1.5.0";
/**
* Gets the product name to be used in the user agent header
diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java
index 636a97111..b14d32c84 100644
--- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java
+++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java
@@ -9,6 +9,7 @@
import com.microsoft.kiota.NativeResponseHandler;
import com.microsoft.kiota.RequestInformation;
import com.microsoft.kiota.authentication.AuthenticationProvider;
+import com.microsoft.kiota.http.middleware.MockResponseHandler;
import com.microsoft.kiota.serialization.Parsable;
import com.microsoft.kiota.serialization.ParsableFactory;
import com.microsoft.kiota.serialization.ParseNode;
@@ -20,7 +21,6 @@
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Dispatcher;
-import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
@@ -574,33 +574,4 @@ public ParseNodeFactory creatMockParseNodeFactory(
when(mockFactory.getValidContentType()).thenReturn(validContentType);
return mockFactory;
}
-
- // Returns request body as response body
- static class MockResponseHandler implements Interceptor {
- @Override
- public Response intercept(Chain chain) throws IOException {
- final var request = chain.request();
- final var requestBody = request.body();
- if (request != null && requestBody != null) {
- final var buffer = new Buffer();
- requestBody.writeTo(buffer);
- return new Response.Builder()
- .code(200)
- .message("OK")
- .protocol(Protocol.HTTP_1_1)
- .request(request)
- .body(
- ResponseBody.create(
- buffer.readByteArray(),
- MediaType.parse("application/json")))
- .build();
- }
- return new Response.Builder()
- .code(200)
- .message("OK")
- .protocol(Protocol.HTTP_1_1)
- .request(request)
- .build();
- }
- }
}
diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/AuthorizationHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/AuthorizationHandlerTest.java
new file mode 100644
index 000000000..605aaf69d
--- /dev/null
+++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/AuthorizationHandlerTest.java
@@ -0,0 +1,213 @@
+package com.microsoft.kiota.http.middleware;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.microsoft.kiota.authentication.AccessTokenProvider;
+import com.microsoft.kiota.authentication.AllowedHostsValidator;
+import com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider;
+import com.microsoft.kiota.http.KiotaClientFactory;
+
+import okhttp3.Interceptor.Chain;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Arrays;
+
+class AuthorizationHandlerTest {
+
+ private static final String ACCESS_TOKEN_STRING = "token";
+ private static final String TOKEN_AFTER_CAE = "TOKEN_AFTER_CAE";
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private static final String PREV_AUTHORIZATION_HEADER_VALUE = "Bearer 123";
+ private static final String NEW_AUTHORIZATION_HEADER_VALUE = "Bearer " + ACCESS_TOKEN_STRING;
+ private static final String CLAIMS_CHALLENGE_HEADER_VALUE =
+ "Bearer authorization_uri=\"https://login.windows.net/common/oauth2/authorize\","
+ + "error=\"insufficient_claims\","
+ + "claims=\"eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwNDEwNjY1MSJ9fX0=\"";
+
+ @Test
+ void testDoesNotAddAuthorizationHeaderIfAlreadyPresent() throws IOException {
+ final Request request =
+ new Request.Builder()
+ .url("https://graph.microsoft.com/v1.0/me")
+ .addHeader("Authorization", "Bearer 123")
+ .build();
+ final Chain mockChain = getMockChain(request, mock(Response.class));
+ final AuthorizationHandler handler =
+ new AuthorizationHandler(getMockAuthenticationProvider());
+ Response response = handler.intercept(mockChain);
+
+ assertTrue(response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ assertEquals(
+ PREV_AUTHORIZATION_HEADER_VALUE, response.request().header(AUTHORIZATION_HEADER));
+ }
+
+ @Test
+ void testAddsAuthorizationHeaderIfNotPresent() throws IOException {
+ final Request request =
+ new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build();
+ final Chain mockChain = getMockChain(request, mock(Response.class));
+ final AuthorizationHandler handler =
+ new AuthorizationHandler(getMockAuthenticationProvider());
+ Response response = handler.intercept(mockChain);
+
+ assertTrue(response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ assertEquals(
+ NEW_AUTHORIZATION_HEADER_VALUE, response.request().header(AUTHORIZATION_HEADER));
+ }
+
+ @Test
+ void testAddsAuthHeaderOnlyToAllowedHosts() throws IOException {
+ final Request request =
+ new Request.Builder().url("https://canary.graph.microsoft.com/v1.0/me").build();
+ final Chain mockChain = getMockChain(request, mock(Response.class));
+ final BaseBearerTokenAuthenticationProvider authProvider = getMockAuthenticationProvider();
+ final AuthorizationHandler handler = new AuthorizationHandler(authProvider);
+ Response response = handler.intercept(mockChain);
+
+ assertTrue(!response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ }
+
+ @Test
+ void testAttemptsCAEChallenge() throws IOException {
+ final Request request =
+ new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build();
+ final Chain mockChain =
+ getMockChain(request, getMockResponseWithClaimsChallengeHeader(request));
+ final BaseBearerTokenAuthenticationProvider authProvider = getMockAuthenticationProvider();
+ final AuthorizationHandler handler = new AuthorizationHandler(authProvider);
+ Response response = handler.intercept(mockChain);
+
+ assertTrue(response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ assertEquals("Bearer " + TOKEN_AFTER_CAE, response.request().header(AUTHORIZATION_HEADER));
+ }
+
+ @Test
+ void testOtherRequestPropertiesAreNotAltered() throws IOException {
+ final Request request =
+ new Request.Builder()
+ .url("https://graph.microsoft.com/v1.0/me")
+ .addHeader("content-type", "application/json")
+ .get()
+ .build();
+ final Chain mockChain = getMockChain(request, mock(Response.class));
+ final AuthorizationHandler handler =
+ new AuthorizationHandler(getMockAuthenticationProvider());
+ Response response = handler.intercept(mockChain);
+
+ assertEquals(request.url(), response.request().url());
+ assertEquals(request.method(), response.request().method());
+ assertTrue(response.request().headers().names().contains("content-type"));
+ assertEquals("application/json", response.request().header("content-type"));
+ assertTrue(response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ assertEquals(
+ NEW_AUTHORIZATION_HEADER_VALUE, response.request().header(AUTHORIZATION_HEADER));
+ }
+
+ @Test
+ void testDoesNotRetryCAEChallengeForOneShotBodyRequests() throws IOException {
+ final RequestBody mockRequestBody = mock(RequestBody.class);
+ when(mockRequestBody.isOneShot()).thenReturn(true);
+ final Request request =
+ new Request.Builder()
+ .url("https://graph.microsoft.com/v1.0/me")
+ .post(mockRequestBody)
+ .build();
+ final Chain mockChain =
+ getMockChain(request, getMockResponseWithClaimsChallengeHeader(request));
+ final BaseBearerTokenAuthenticationProvider authProvider = getMockAuthenticationProvider();
+ final AuthorizationHandler handler = new AuthorizationHandler(authProvider);
+ Response response = handler.intercept(mockChain);
+
+ assertTrue(response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ assertEquals(
+ NEW_AUTHORIZATION_HEADER_VALUE, response.request().header(AUTHORIZATION_HEADER));
+ }
+
+ @Test
+ void testDoesNotAttemptCAEChallengeIfNoClaimsPresent() throws IOException {
+ final Request request =
+ new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build();
+ final Response mockResponse = mock(Response.class);
+ when(mockResponse.code()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
+ final Chain mockChain = getMockChain(request, mockResponse);
+ final BaseBearerTokenAuthenticationProvider authProvider = getMockAuthenticationProvider();
+ final AuthorizationHandler handler = new AuthorizationHandler(authProvider);
+ Response response = handler.intercept(mockChain);
+
+ assertTrue(response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ assertEquals(
+ NEW_AUTHORIZATION_HEADER_VALUE, response.request().header(AUTHORIZATION_HEADER));
+ assertEquals(401, response.code());
+ }
+
+ @Test
+ void testAuthorizationHandlerAddedByClientFactory() throws IOException {
+ final BaseBearerTokenAuthenticationProvider authProvider = getMockAuthenticationProvider();
+ OkHttpClient okHttpClient =
+ KiotaClientFactory.create(authProvider)
+ .addInterceptor(new MockResponseHandler())
+ .build();
+
+ final Request request =
+ new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build();
+ Response response = okHttpClient.newCall(request).execute();
+
+ assertTrue(response.request().headers().names().contains(AUTHORIZATION_HEADER));
+ assertEquals(
+ NEW_AUTHORIZATION_HEADER_VALUE, response.request().header(AUTHORIZATION_HEADER));
+ }
+
+ private Chain getMockChain(Request mockRequest, Response mockResponse) throws IOException {
+ Chain mockChain = mock(Chain.class);
+ when(mockChain.request()).thenReturn(mockRequest);
+ when(mockChain.proceed(any(Request.class)))
+ .thenAnswer(
+ new Answer() {
+ public Response answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ Request request = (Request) args[0];
+ when(mockResponse.request()).thenReturn(request);
+ return mockResponse;
+ }
+ });
+ return mockChain;
+ }
+
+ private BaseBearerTokenAuthenticationProvider getMockAuthenticationProvider() {
+ final AccessTokenProvider mockAccessTokenProvider = mock(AccessTokenProvider.class);
+ final AllowedHostsValidator allowedHostsValidator =
+ new AllowedHostsValidator("graph.microsoft.com");
+ when(mockAccessTokenProvider.getAllowedHostsValidator()).thenReturn(allowedHostsValidator);
+ when(mockAccessTokenProvider.getAuthorizationToken(any(URI.class), anyMap()))
+ .thenReturn(ACCESS_TOKEN_STRING, TOKEN_AFTER_CAE);
+ final BaseBearerTokenAuthenticationProvider mockAuthenticationProvider =
+ mock(BaseBearerTokenAuthenticationProvider.class);
+ when(mockAuthenticationProvider.getAccessTokenProvider())
+ .thenReturn(mockAccessTokenProvider);
+ return mockAuthenticationProvider;
+ }
+
+ private Response getMockResponseWithClaimsChallengeHeader(Request request) {
+ final Response mockResponse = mock(Response.class);
+ when(mockResponse.code()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
+ when(mockResponse.headers("WWW-Authenticate"))
+ .thenReturn(Arrays.asList(CLAIMS_CHALLENGE_HEADER_VALUE));
+ when(mockResponse.request()).thenReturn(request);
+ return mockResponse;
+ }
+}
diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/HeadersInspectionHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/HeadersInspectionHandlerTest.java
similarity index 97%
rename from components/http/okHttp/src/test/java/com/microsoft/kiota/http/HeadersInspectionHandlerTest.java
rename to components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/HeadersInspectionHandlerTest.java
index 4dfa1db88..a358fe724 100644
--- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/HeadersInspectionHandlerTest.java
+++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/HeadersInspectionHandlerTest.java
@@ -1,4 +1,4 @@
-package com.microsoft.kiota.http;
+package com.microsoft.kiota.http.middleware;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -6,7 +6,6 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import com.microsoft.kiota.http.middleware.HeadersInspectionHandler;
import com.microsoft.kiota.http.middleware.options.HeadersInspectionOption;
import okhttp3.Headers;
diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/MockResponseHandler.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/MockResponseHandler.java
new file mode 100644
index 000000000..456597668
--- /dev/null
+++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/MockResponseHandler.java
@@ -0,0 +1,52 @@
+package com.microsoft.kiota.http.middleware;
+
+import okhttp3.Interceptor;
+import okhttp3.MediaType;
+import okhttp3.Protocol;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import okio.Buffer;
+
+import java.io.IOException;
+
+/**
+ * Returns the request body as the response body
+ */
+public class MockResponseHandler implements Interceptor {
+ private int statusCode;
+
+ public MockResponseHandler(int statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ public MockResponseHandler() {
+ this.statusCode = 200;
+ }
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ final var request = chain.request();
+ final var requestBody = request.body();
+ if (request != null && requestBody != null) {
+ final var buffer = new Buffer();
+ requestBody.writeTo(buffer);
+ return new Response.Builder()
+ .code(this.statusCode)
+ .message("OK")
+ .protocol(Protocol.HTTP_1_1)
+ .request(request)
+ .body(
+ ResponseBody.create(
+ buffer.readByteArray(), MediaType.parse("application/json")))
+ .build();
+ }
+ return new Response.Builder()
+ .code(this.statusCode)
+ .message("OK")
+ .protocol(Protocol.HTTP_1_1)
+ .request(request)
+ .body(ResponseBody.create("", MediaType.parse("application/json")))
+ .build();
+ }
+}
diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/ParametersNameDecodingHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/ParametersNameDecodingHandlerTest.java
similarity index 95%
rename from components/http/okHttp/src/test/java/com/microsoft/kiota/http/ParametersNameDecodingHandlerTest.java
rename to components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/ParametersNameDecodingHandlerTest.java
index f555ff457..2452c0526 100644
--- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/ParametersNameDecodingHandlerTest.java
+++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/ParametersNameDecodingHandlerTest.java
@@ -1,9 +1,9 @@
-package com.microsoft.kiota.http;
+package com.microsoft.kiota.http.middleware;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import com.microsoft.kiota.http.middleware.ParametersNameDecodingHandler;
+import com.microsoft.kiota.http.KiotaClientFactory;
import com.microsoft.kiota.http.middleware.options.ParametersNameDecodingOption;
import okhttp3.Interceptor;
diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UrlReplaceHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java
similarity index 91%
rename from components/http/okHttp/src/test/java/com/microsoft/kiota/http/UrlReplaceHandlerTest.java
rename to components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java
index 21b49281c..0c04f014e 100644
--- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UrlReplaceHandlerTest.java
+++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java
@@ -1,9 +1,9 @@
-package com.microsoft.kiota.http;
+package com.microsoft.kiota.http.middleware;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import com.microsoft.kiota.http.middleware.UrlReplaceHandler;
+import com.microsoft.kiota.http.KiotaClientFactory;
import com.microsoft.kiota.http.middleware.options.UrlReplaceHandlerOption;
import okhttp3.Interceptor;
@@ -18,7 +18,7 @@
class UrlReplaceHandlerTest {
- private static final String defaultUsersWithTokenUrl =
+ private static final String DEFAULT_URL_WITH_TOKEN =
"https://graph.microsoft.com/v1.0/users/TokenToReplace";
private static final HashMap defaultReplacementPairs = new HashMap<>();
@@ -27,12 +27,12 @@ void testUrlReplaceHandler_no_replacementPairs() throws IOException {
Interceptor[] interceptors =
new Interceptor[] {new UrlReplaceHandler(new UrlReplaceHandlerOption())};
final OkHttpClient client = KiotaClientFactory.create(interceptors).build();
- final Request request = new Request.Builder().url(defaultUsersWithTokenUrl).build();
+ final Request request = new Request.Builder().url(DEFAULT_URL_WITH_TOKEN).build();
final Response response = client.newCall(request).execute();
assertNotNull(response);
assertEquals(
- defaultUsersWithTokenUrl,
+ DEFAULT_URL_WITH_TOKEN,
response.request()
.url()
.toString()); // url should remain the same without replacement pairs
@@ -46,7 +46,7 @@ void testUrlReplaceHandler_default_url() throws IOException {
new UrlReplaceHandler(new UrlReplaceHandlerOption(defaultReplacementPairs))
};
final OkHttpClient client = KiotaClientFactory.create(interceptors).build();
- final Request request = new Request.Builder().url(defaultUsersWithTokenUrl).build();
+ final Request request = new Request.Builder().url(DEFAULT_URL_WITH_TOKEN).build();
final Response response = client.newCall(request).execute();
final String expectedNewUrl = "https://graph.microsoft.com/v1.0/me";
diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UserAgentHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java
similarity index 95%
rename from components/http/okHttp/src/test/java/com/microsoft/kiota/http/UserAgentHandlerTest.java
rename to components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java
index bcfca8fba..1b8fe4498 100644
--- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/UserAgentHandlerTest.java
+++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java
@@ -1,11 +1,10 @@
-package com.microsoft.kiota.http;
+package com.microsoft.kiota.http.middleware;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.*;
-import com.microsoft.kiota.http.middleware.UserAgentHandler;
import com.microsoft.kiota.http.middleware.options.UserAgentHandlerOption;
import okhttp3.Headers;
@@ -60,8 +59,8 @@ void addsTheProductOnce() throws IOException {
final UserAgentHandler handler = new UserAgentHandler();
final Request request = new Request.Builder().url("http://localhost").build();
when(mockChain.request()).thenReturn(request);
+ handler.intercept(mockChain);
Response response = handler.intercept(mockChain);
- response = handler.intercept(mockChain);
final Request result = response.request();
assertNotNull(response);
assertNotNull(result);
diff --git a/gradle.properties b/gradle.properties
index 434f746a4..d61e8397a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -25,7 +25,7 @@ org.gradle.caching=true
mavenGroupId = com.microsoft.kiota
mavenMajorVersion = 1
-mavenMinorVersion = 4
+mavenMinorVersion = 5
mavenPatchVersion = 0
mavenArtifactSuffix =