Skip to content
This repository was archived by the owner on Dec 4, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -7,6 +7,7 @@
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import com.microsoft.aad.msal4j.MsalServiceException;

import java.net.MalformedURLException;
import java.util.Collections;
Expand All @@ -28,16 +29,12 @@ public class CredentialsAuthenticator implements Authenticator {
* @throws MalformedURLException Invalid endpoint.
*/
CredentialsAuthenticator(String appId, String appPassword, OAuthConfiguration configuration)
throws MalformedURLException {
throws MalformedURLException {

app = ConfidentialClientApplication
.builder(appId, ClientCredentialFactory.createFromSecret(appPassword))
.authority(configuration.getAuthority())
.build();
app = ConfidentialClientApplication.builder(appId, ClientCredentialFactory.createFromSecret(appPassword))
.authority(configuration.getAuthority()).build();

parameters = ClientCredentialParameters
.builder(Collections.singleton(configuration.getScope()))
.build();
parameters = ClientCredentialParameters.builder(Collections.singleton(configuration.getScope())).build();
}

/**
Expand All @@ -47,12 +44,19 @@ public class CredentialsAuthenticator implements Authenticator {
*/
@Override
public CompletableFuture<IAuthenticationResult> acquireToken() {
return app.acquireToken(parameters)
.exceptionally(
exception -> {
// wrapping whatever msal throws into our own exception
throw new AuthenticationException(exception);
return Retry.run(() -> app.acquireToken(parameters).exceptionally(exception -> {
// wrapping whatever msal throws into our own exception
throw new AuthenticationException(exception);
}), (exception, count) -> {
if (exception instanceof RetryException && exception.getCause() instanceof MsalServiceException) {
MsalServiceException serviceException = (MsalServiceException) exception.getCause();
if (serviceException.headers().containsKey("Retry-After")) {
return RetryAfterHelper.processRetry(serviceException.headers().get("Retry-After"), count);
} else {
return RetryParams.defaultBackOff(++count);
}
);
}
return RetryParams.stopRetrying();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.microsoft.bot.connector.authentication;

import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

/**
* Class that contains a helper function to process HTTP 429 Retry-After headers
* for the CredentialsAuthenticator. The reason to extract this was
* CredentialsAuthenticator is an internal class that isn't exposed except
* through other Authentication classes and we wanted a way to test the
* processing of 429 headers without building complicated test harnesses.
*/
public final class RetryAfterHelper {

private RetryAfterHelper() {

}

/**
* Process a RetryException and see if we should wait for a requested amount of
* time before retrying to call the authentication service again.
*
* @param header The header values to process.
* @param count The count of how many times we have retried.
* @return A RetryParams with instructions of when or how many more times to
* retry.
*/
public static RetryParams processRetry(List<String> header, Integer count) {
if (header == null || header.size() == 0) {
return RetryParams.defaultBackOff(++count);
} else {
String headerString = header.get(0);
if (StringUtils.isNotBlank(headerString)) {
// see if it matches a numeric value
if (headerString.matches("^[0-9]+\\.?0*$")) {
headerString = headerString.replaceAll("\\.0*$", "");
Duration delay = Duration.ofSeconds(Long.parseLong(headerString));
return new RetryParams(delay.toMillis());
} else {
// check to see if it's a RFC_1123 format Date/Time
DateTimeFormatter gmtFormat = DateTimeFormatter.RFC_1123_DATE_TIME;
try {
ZonedDateTime zoned = ZonedDateTime.parse(headerString, gmtFormat);
if (zoned != null) {
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
long waitMillis = zoned.toInstant().toEpochMilli() - now.toInstant().toEpochMilli();
if (waitMillis > 0) {
return new RetryParams(waitMillis);
} else {
return RetryParams.defaultBackOff(++count);
}
}
} catch (DateTimeParseException ex) {
return RetryParams.defaultBackOff(++count);
}
}
}
}
return RetryParams.defaultBackOff(++count);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.microsoft.bot.connector;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.microsoft.aad.msal4j.MsalException;
import com.microsoft.aad.msal4j.MsalServiceException;
import com.microsoft.bot.connector.authentication.RetryAfterHelper;
import com.microsoft.bot.connector.authentication.RetryException;
import com.microsoft.bot.connector.authentication.RetryParams;

import org.junit.Assert;
import org.junit.Test;

public class RetryAfterHelperTests {

@Test
public void TestRetryIncrement() {
RetryParams result = RetryAfterHelper.processRetry(new ArrayList<String>(), 8);
Assert.assertTrue(result.getShouldRetry());
result = RetryAfterHelper.processRetry(new ArrayList<String>(), 9);
Assert.assertFalse(result.getShouldRetry());
}

@Test
public void TestRetryDelaySeconds() {
List<String> headers = new ArrayList<String>();
headers.add("10");
RetryParams result = RetryAfterHelper.processRetry(headers, 1);
Assert.assertEquals(result.getRetryAfter(), 10000);
}

@Test
public void TestRetryDelayRFC1123Date() {
Instant instant = Instant.now().plusSeconds(5);
ZonedDateTime dateTime = instant.atZone(ZoneId.of("UTC"));
String dateTimeString = dateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);
List<String> headers = new ArrayList<String>();
headers.add(dateTimeString);
RetryParams result = RetryAfterHelper.processRetry(headers, 1);
Assert.assertTrue(result.getShouldRetry());
Assert.assertTrue(result.getRetryAfter() > 0);
}

@Test
public void TestRetryDelayRFC1123DateInPast() {
Instant instant = Instant.now().plusSeconds(-5);
ZonedDateTime dateTime = instant.atZone(ZoneId.of("UTC"));
String dateTimeString = dateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);
List<String> headers = new ArrayList<String>();
headers.add(dateTimeString);
RetryParams result = RetryAfterHelper.processRetry(headers, 1);
Assert.assertTrue(result.getShouldRetry());
// default is 50, so since the time was in the past we should be seeing the default 50 here.
Assert.assertTrue(result.getRetryAfter() == 50);
}


@Test
public void TestRetryDelayRFC1123DateEmpty() {
List<String> headers = new ArrayList<String>();
headers.add("");
RetryParams result = RetryAfterHelper.processRetry(headers, 1);
Assert.assertTrue(result.getShouldRetry());
// default is 50, so since the time was in the past we should be seeing the default 50 here.
Assert.assertTrue(result.getRetryAfter() == 50);
}

@Test
public void TestRetryDelayRFC1123DateNull() {
List<String> headers = new ArrayList<String>();
headers.add(null);
RetryParams result = RetryAfterHelper.processRetry(headers, 1);
Assert.assertTrue(result.getShouldRetry());
// default is 50, so since the time was in the past we should be seeing the default 50 here.
Assert.assertTrue(result.getRetryAfter() == 50);
}

@Test
public void TestRetryDelayRFC1123NeaderNull() {
RetryParams result = RetryAfterHelper.processRetry(null, 1);
Assert.assertTrue(result.getShouldRetry());
// default is 50, so since the time was in the past we should be seeing the default 50 here.
Assert.assertTrue(result.getRetryAfter() == 50);
}

}