-
Notifications
You must be signed in to change notification settings - Fork 24
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
base: main
Are you sure you want to change the base?
Feat/auth handler #1562
Changes from 5 commits
958d1be
a19519b
a619021
9014de3
67047cd
293575b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
|
||
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 |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
There was a problem hiding this comment.
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?