Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/auth handler #1562

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.microsoft.kiota.http;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

import okhttp3.Response;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ContinuousAccessEvaluationClaims {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we restrict this to package instead?


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 wwwAuthenticateHeader = "WWW-Authenticate";

public static @Nullable String getClaimsFromResponse(@Nonnull Response response) {
if (response == null || response.code() != 401) {
return null;
}
final List<String> authenticateHeader = response.headers(wwwAuthenticateHeader);
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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,6 +15,8 @@
import okhttp3.OkHttpClient;

import java.time.Duration;
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 {
Expand All @@ -23,7 +27,7 @@ private KiotaClientFactory() {}
* @return an OkHttpClient Builder instance.
*/
@Nonnull public static OkHttpClient.Builder create() {
return create(null);
return create(createDefaultInterceptors());
}

/**
Expand All @@ -48,6 +52,13 @@ private KiotaClientFactory() {}
return builder;
}

@Nonnull public static OkHttpClient.Builder create(
@Nonnull BaseBearerTokenAuthenticationProvider authenticationProvider) {
List<Interceptor> interceptors = Arrays.asList(createDefaultInterceptors());
interceptors.add(new AuthorizationHandler(authenticationProvider));
return create((Interceptor[]) interceptors.toArray());
}

/**
* Creates the default interceptors for the client.
* @return an array of interceptors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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<String> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.microsoft.kiota.http.middleware;

import static com.microsoft.kiota.http.TelemetrySemanticConventions.HTTP_REQUEST_RESEND_COUNT;

import com.microsoft.kiota.RequestInformation;
import com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider;
import com.microsoft.kiota.http.ContinuousAccessEvaluationClaims;

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.Response;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class AuthorizationHandler implements Interceptor {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing doc comment


private final BaseBearerTokenAuthenticationProvider authenticationProvider;
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
private static final String authorizationHeaderKey = "Authorization";

/**
* Instantiates a new AuthorizationHandler.
* @param authenticationProvider the authentication provider.
*/
public AuthorizationHandler(BaseBearerTokenAuthenticationProvider authenticationProvider) {
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
this.authenticationProvider = authenticationProvider;
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public Response intercept(final Chain chain) throws IOException {
Objects.requireNonNull(chain, "parameter chain cannot be null");
final Request request = chain.request();

final Span span =
ObservabilityHelper.getSpanForRequest(request, "AuthorizationHandler_Intercept");
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(authorizationHeaderKey)) {
span.setAttribute("com.microsoft.kiota.handler.authorization.token_present", true);
return chain.proceed(request);
}

authenticateRequest(request, null, span);
Response response = chain.proceed(chain.request());

if (response != null && response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
return response;
}

// Attempt CAE claims challenge
String claims = ContinuousAccessEvaluationClaims.getClaimsFromResponse(response);
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
if (claims == null || claims.isEmpty()) {
return response;
}

span.addEvent("com.microsoft.kiota.handler.authorization.challenge_received");

// We cannot replay one-shot requests after claims challenge
boolean isRequestBodyOneShot =
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
request != null && request.body() != null && request.body().isOneShot();
if (isRequestBodyOneShot) {
return response;
}

response.close();
final HashMap<String, Object> additionalContext = new HashMap<>();
additionalContext.put("claims", claims);
// Retry claims challenge only once
authenticateRequest(request, additionalContext, span);
span.setAttribute(HTTP_REQUEST_RESEND_COUNT, 1);
return chain.proceed(request);
} finally {
if (scope != null) {
scope.close();
}
if (span != null) {
span.end();
}
}
}

private void authenticateRequest(
@Nonnull Request request,
@Nullable Map<String, Object> additionalAuthenticationContext,
@Nonnull Span span) {
Ndiritu marked this conversation as resolved.
Show resolved Hide resolved
final RequestInformation requestInformation = getRequestInformation(request);
authenticationProvider.authenticateRequest(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should get the access token provider from the base authentication provider instead so we don't have to create an intermediate request information object.

requestInformation, additionalAuthenticationContext);
// Update native request with headers added to requestInformation
if (requestInformation.headers.containsKey(authorizationHeaderKey)) {
span.setAttribute("com.microsoft.kiota.handler.authorization.token_obtained", true);
Set<String> authorizationHeaderValues =
requestInformation.headers.get(authorizationHeaderKey);
if (!authorizationHeaderValues.isEmpty()) {
Request.Builder requestBuilder = request.newBuilder();
for (String value : authorizationHeaderValues) {
requestBuilder.addHeader(authorizationHeaderKey, value);
}
}
}
}

private RequestInformation getRequestInformation(final Request request) {
RequestInformation requestInformation = new RequestInformation();
requestInformation.setUri(request.url().uri());
for (String headerName : request.headers().names()) {
requestInformation.headers.add(headerName, request.header(headerName));
}
return requestInformation;
}
}
Loading