From 904321e70090af80d79292719b9f2e5638e20797 Mon Sep 17 00:00:00 2001
From: tracyboehrer
Date: Tue, 21 Apr 2020 09:43:44 -0500
Subject: [PATCH] BotFrameworkAdapter auth flow/CallerId
---
etc/botframework-java-formatter.xml | 2 +-
.../com/microsoft/bot/builder/BotAdapter.java | 70 +++-
.../bot/builder/BotFrameworkAdapter.java | 364 ++++++++++++------
.../bot/builder/BotFrameworkAdapterTests.java | 195 +++++++++-
.../bot/builder/MemoryConnectorClient.java | 76 ++++
.../bot/builder/MemoryConversations.java | 145 +++++++
.../authentication/JwtTokenValidation.java | 33 ++
.../bot/schema/TokenExchangeState.java | 38 +-
8 files changed, 787 insertions(+), 136 deletions(-)
create mode 100644 libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConnectorClient.java
create mode 100644 libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConversations.java
diff --git a/etc/botframework-java-formatter.xml b/etc/botframework-java-formatter.xml
index 577eb8d5e..11cd7eff7 100644
--- a/etc/botframework-java-formatter.xml
+++ b/etc/botframework-java-formatter.xml
@@ -131,7 +131,7 @@
-
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java
index 9678a0d20..7bd258210 100644
--- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotAdapter.java
@@ -3,6 +3,7 @@
package com.microsoft.bot.builder;
+import com.microsoft.bot.connector.authentication.ClaimsIdentity;
import com.microsoft.bot.schema.Activity;
import com.microsoft.bot.schema.ConversationReference;
import com.microsoft.bot.schema.ResourceResponse;
@@ -10,6 +11,7 @@
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
+import org.apache.commons.lang3.NotImplementedException;
/**
* Represents a bot adapter that can connect a bot to a service endpoint. This
@@ -32,6 +34,16 @@
* {@link TurnContext} {@link Activity} {@link Bot} {@link Middleware}
*/
public abstract class BotAdapter {
+ /**
+ * Key to store bot claims identity.
+ */
+ public static final String BOT_IDENTITY_KEY = "BotIdentity";
+
+ /**
+ * Key to store bot oauth scope.
+ */
+ public static final String OAUTH_SCOPE_KEY = "Microsoft.Bot.Builder.BotAdapter.OAuthScope";
+
/**
* The collection of middleware in the adapter's pipeline.
*/
@@ -213,13 +225,10 @@ public CompletableFuture continueConversation(
ConversationReference reference,
BotCallbackHandler callback
) {
-
CompletableFuture pipelineResult = new CompletableFuture<>();
- try (TurnContextImpl context = new TurnContextImpl(
- this,
- reference.getContinuationActivity()
- )) {
+ try (TurnContextImpl context =
+ new TurnContextImpl(this, reference.getContinuationActivity())) {
pipelineResult = runPipeline(context, callback);
} catch (Exception e) {
pipelineResult.completeExceptionally(e);
@@ -227,4 +236,55 @@ public CompletableFuture continueConversation(
return pipelineResult;
}
+
+ /**
+ * Sends a proactive message to a conversation.
+ *
+ *
+ * Call this method to proactively send a message to a conversation. Most
+ * channels require a user to initiate a conversation with a bot before the bot
+ * can send activities to the user.
+ *
+ *
+ * @param claimsIdentity A ClaimsIdentity reference for the conversation.
+ * @param reference A reference to the conversation to continue.
+ * @param callback The method to call for the result bot turn.
+ * @return A task that represents the work queued to execute.
+ */
+ public CompletableFuture continueConversation(
+ ClaimsIdentity claimsIdentity,
+ ConversationReference reference,
+ BotCallbackHandler callback
+ ) {
+ CompletableFuture result = new CompletableFuture<>();
+ result.completeExceptionally(new NotImplementedException("continueConversation"));
+ return result;
+ }
+
+ /**
+ * Sends a proactive message to a conversation.
+ *
+ *
+ * Call this method to proactively send a message to a conversation. Most
+ * channels require a user to initiate a conversation with a bot before the bot
+ * can send activities to the user.
+ *
+ *
+ * @param claimsIdentity A ClaimsIdentity reference for the conversation.
+ * @param reference A reference to the conversation to continue.
+ * @param audience A value signifying the recipient of the proactive
+ * message.
+ * @param callback The method to call for the result bot turn.
+ * @return A task that represents the work queued to execute.
+ */
+ public CompletableFuture continueConversation(
+ ClaimsIdentity claimsIdentity,
+ ConversationReference reference,
+ String audience,
+ BotCallbackHandler callback
+ ) {
+ CompletableFuture result = new CompletableFuture<>();
+ result.completeExceptionally(new NotImplementedException("continueConversation"));
+ return result;
+ }
}
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java
index 5e3d27812..da560d2e9 100644
--- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java
@@ -21,6 +21,7 @@
import com.microsoft.bot.connector.authentication.ChannelProvider;
import com.microsoft.bot.connector.authentication.ClaimsIdentity;
import com.microsoft.bot.connector.authentication.CredentialProvider;
+import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants;
import com.microsoft.bot.connector.authentication.JwtTokenValidation;
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
import com.microsoft.bot.connector.authentication.MicrosoftGovernmentAppCredentials;
@@ -37,11 +38,11 @@
import com.microsoft.bot.schema.ConversationReference;
import com.microsoft.bot.schema.ConversationsResult;
import com.microsoft.bot.schema.ResourceResponse;
-import com.microsoft.bot.schema.RoleTypes;
import com.microsoft.bot.schema.TokenExchangeState;
import com.microsoft.bot.schema.TokenResponse;
import com.microsoft.bot.schema.TokenStatus;
import com.microsoft.bot.rest.retry.RetryStrategy;
+import java.util.Collections;
import org.apache.commons.lang3.StringUtils;
import java.net.HttpURLConnection;
@@ -80,11 +81,6 @@ public class BotFrameworkAdapter extends BotAdapter
*/
public static final String INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse";
- /**
- * Key to store bot claims identity.
- */
- private static final String BOT_IDENTITY_KEY = "BotIdentity";
-
/**
* Key to store ConnectorClient.
*/
@@ -296,26 +292,101 @@ public CompletableFuture continueConversation(
throw new IllegalArgumentException("callback");
}
+ // Hand craft Claims Identity.
+ HashMap claims = new HashMap() {
+ {
+ // Adding claims for both Emulator and Channel.
+ put(AuthenticationConstants.AUDIENCE_CLAIM, botAppId);
+ put(AuthenticationConstants.APPID_CLAIM, botAppId);
+ }
+ };
+ ClaimsIdentity claimsIdentity = new ClaimsIdentity("ExternalBearer", claims);
+
+ String audience = getBotFrameworkOAuthScope();
+
+ return continueConversation(claimsIdentity, reference, audience, callback);
+ }
+
+ /**
+ * Sends a proactive message to a conversation.
+ *
+ *
+ * Call this method to proactively send a message to a conversation. Most
+ * channels require a user to initiate a conversation with a bot before the bot
+ * can send activities to the user.
+ *
+ *
+ * @param claimsIdentity A ClaimsIdentity reference for the conversation.
+ * @param reference A reference to the conversation to continue.
+ * @param callback The method to call for the result bot turn.
+ * @return A task that represents the work queued to execute.
+ */
+ public CompletableFuture continueConversation(
+ ClaimsIdentity claimsIdentity,
+ ConversationReference reference,
+ BotCallbackHandler callback
+ ) {
+ return continueConversation(
+ claimsIdentity,
+ reference,
+ getBotFrameworkOAuthScope(),
+ callback
+ );
+ }
+
+ /**
+ * Sends a proactive message to a conversation.
+ *
+ *
+ * Call this method to proactively send a message to a conversation. Most
+ * channels require a user to initiate a conversation with a bot before the bot
+ * can send activities to the user.
+ *
+ *
+ * @param claimsIdentity A ClaimsIdentity reference for the conversation.
+ * @param reference A reference to the conversation to continue.
+ * @param audience A value signifying the recipient of the proactive
+ * message.
+ * @param callback The method to call for the result bot turn.
+ * @return A task that represents the work queued to execute.
+ */
+ public CompletableFuture continueConversation(
+ ClaimsIdentity claimsIdentity,
+ ConversationReference reference,
+ String audience,
+ BotCallbackHandler callback
+ ) {
+ if (claimsIdentity == null) {
+ throw new IllegalArgumentException("claimsIdentity");
+ }
+
+ if (reference == null) {
+ throw new IllegalArgumentException("reference");
+ }
+
+ if (callback == null) {
+ throw new IllegalArgumentException("callback");
+ }
+
+ if (StringUtils.isEmpty(audience)) {
+ throw new IllegalArgumentException("audience cannot be null or empty");
+ }
+
CompletableFuture pipelineResult = new CompletableFuture<>();
try (TurnContextImpl context =
new TurnContextImpl(this, reference.getContinuationActivity())) {
- // Hand craft Claims Identity.
- HashMap claims = new HashMap() {
- {
- put(AuthenticationConstants.AUDIENCE_CLAIM, botAppId);
- put(AuthenticationConstants.APPID_CLAIM, botAppId);
- }
- };
- ClaimsIdentity claimsIdentity = new ClaimsIdentity("ExternalBearer", claims);
-
context.getTurnState().add(BOT_IDENTITY_KEY, claimsIdentity);
-
- pipelineResult = createConnectorClient(reference.getServiceUrl(), claimsIdentity)
- .thenCompose(connectorClient -> {
- context.getTurnState().add(CONNECTOR_CLIENT_KEY, connectorClient);
- return runPipeline(context, callback);
- });
+ context.getTurnState().add(OAUTH_SCOPE_KEY, audience);
+
+ pipelineResult = createConnectorClient(
+ reference.getServiceUrl(),
+ claimsIdentity,
+ audience
+ ).thenCompose(connectorClient -> {
+ context.getTurnState().add(CONNECTOR_CLIENT_KEY, connectorClient);
+ return runPipeline(context, callback);
+ });
} catch (Exception e) {
pipelineResult.completeExceptionally(e);
}
@@ -359,7 +430,11 @@ public CompletableFuture processActivity(
BotAssert.activityNotNull(activity);
return JwtTokenValidation.authenticateRequest(
- activity, authHeader, credentialProvider, channelProvider, authConfiguration
+ activity,
+ authHeader,
+ credentialProvider,
+ channelProvider,
+ authConfiguration
).thenCompose(claimsIdentity -> processActivity(claimsIdentity, activity, callback));
}
@@ -390,7 +465,12 @@ public CompletableFuture processActivity(
activity.setCallerId(generateCallerId(identity).join());
context.getTurnState().add(BOT_IDENTITY_KEY, identity);
- pipelineResult = createConnectorClient(activity.getServiceUrl(), identity)
+ // The OAuthScope is also stored on the TurnState to get the correct
+ // AppCredentials if fetching a token is required.
+ String scope = getBotFrameworkOAuthScope();
+ context.getTurnState().add(OAUTH_SCOPE_KEY, scope);
+
+ pipelineResult = createConnectorClient(activity.getServiceUrl(), identity, scope)
// run pipeline
.thenCompose(connectorClient -> {
@@ -399,8 +479,7 @@ public CompletableFuture processActivity(
})
// Handle Invoke scenarios, which deviate from the request/response model in
- // that
- // the Bot will return a specific body and return code.
+ // that the Bot will return a specific body and return code.
.thenCompose(result -> {
if (activity.isType(ActivityTypes.INVOKE)) {
Activity invokeResponse = context.getTurnState().get(INVOKE_RESPONSE_KEY);
@@ -429,9 +508,9 @@ public CompletableFuture processActivity(
private CompletableFuture generateCallerId(ClaimsIdentity claimsIdentity) {
return credentialProvider.isAuthenticationDisabled()
.thenApply(
- is_auth_enabled -> {
+ is_auth_disabled -> {
// Is the bot accepting all incoming messages?
- if (!is_auth_enabled) {
+ if (is_auth_disabled) {
return null;
}
@@ -463,7 +542,7 @@ private CompletableFuture generateCallerId(ClaimsIdentity claimsIdentity
*
* {@link TurnContext#onSendActivities(SendActivitiesHandler)}
*/
- @SuppressWarnings("checkstyle:EmptyBlock")
+ @SuppressWarnings("checkstyle:EmptyBlock, checkstyle:linelength")
@Override
public CompletableFuture sendActivities(
TurnContext context,
@@ -494,8 +573,11 @@ public CompletableFuture sendActivities(
*/
for (int index = 0; index < activities.size(); index++) {
Activity activity = activities.get(index);
- ResourceResponse response;
+ // Clients and bots SHOULD NOT include an id field in activities they generate.
+ activity.setId(null);
+
+ ResourceResponse response;
if (activity.isType(ActivityTypes.DELAY)) {
// The Activity Schema doesn't have a delay type build in, so it's simulated
// here in the Bot. This matches the behavior in the Node connector.
@@ -529,17 +611,14 @@ public CompletableFuture sendActivities(
}
// If No response is set, then default to a "simple" response. This can't really
- // be done
- // above, as there are cases where the ReplyTo/SendTo methods will also return
- // null
- // (See below) so the check has to happen here.
-
+ // be done above, as there are cases where the ReplyTo/SendTo methods will also
+ // return null (See below) so the check has to happen here.
+ //
// Note: In addition to the Invoke / Delay / Activity cases, this code also
- // applies
- // with Skype and Teams with regards to typing events. When sending a typing
- // event in
- // these channels they do not return a RequestResponse which causes the bot to
- // blow up.
+ // applies with Skype and Teams with regards to typing events. When sending a
+ // typing event in these channels they do not return a RequestResponse which
+ // causes the bot to blow up.
+ //
// https://github.com/Microsoft/botbuilder-dotnet/issues/460
// bug report : https://github.com/Microsoft/botbuilder-dotnet/issues/465
if (response == null) {
@@ -826,11 +905,13 @@ public CompletableFuture getUserToken(
throw new IllegalArgumentException("connectionName");
}
- return createOAuthClient(context).thenCompose(oAuthClient -> {
+ return createOAuthClient(context, null).thenCompose(oAuthClient -> {
return oAuthClient.getUserToken()
.getToken(
- context.getActivity().getFrom().getId(), connectionName,
- context.getActivity().getChannelId(), magicCode
+ context.getActivity().getFrom().getId(),
+ connectionName,
+ context.getActivity().getChannelId(),
+ magicCode
);
});
}
@@ -854,9 +935,10 @@ public CompletableFuture getOauthSignInLink(
throw new IllegalArgumentException("connectionName");
}
- return createOAuthClient(context).thenCompose(oAuthClient -> {
+ return createOAuthClient(context, null).thenCompose(oAuthClient -> {
try {
Activity activity = context.getActivity();
+ String appId = getBotAppId(context);
TokenExchangeState tokenExchangeState = new TokenExchangeState() {
{
@@ -871,6 +953,8 @@ public CompletableFuture getOauthSignInLink(
setUser(activity.getFrom());
}
});
+ setMsAppId(appId);
+ setRelatesTo(activity.getRelatesTo());
}
};
@@ -904,7 +988,6 @@ public CompletableFuture getOauthSignInLink(
String userId,
String finalRedirect
) {
-
BotAssert.contextNotNull(context);
if (StringUtils.isEmpty(connectionName)) {
throw new IllegalArgumentException("connectionName");
@@ -913,30 +996,26 @@ public CompletableFuture getOauthSignInLink(
throw new IllegalArgumentException("userId");
}
- return createOAuthClient(context).thenCompose(oAuthClient -> {
+ return createOAuthClient(context, null).thenCompose(oAuthClient -> {
try {
+ Activity activity = context.getActivity();
+ String appId = getBotAppId(context);
+
TokenExchangeState tokenExchangeState = new TokenExchangeState() {
{
setConnectionName(connectionName);
setConversation(new ConversationReference() {
{
- setActivityId(null);
- setBot(new ChannelAccount() {
- {
- setRole(RoleTypes.BOT);
- }
- });
- setChannelId(Channels.DIRECTLINE);
- setConversation(new ConversationAccount());
- setServiceUrl(null);
- setUser(new ChannelAccount() {
- {
- setRole(RoleTypes.USER);
- setId(userId);
- }
- });
+ setActivityId(activity.getId());
+ setBot(activity.getRecipient());
+ setChannelId(activity.getChannelId());
+ setConversation(activity.getConversation());
+ setServiceUrl(activity.getServiceUrl());
+ setUser(activity.getFrom());
}
});
+ setRelatesTo(activity.getRelatesTo());
+ setMsAppId(appId);
}
};
@@ -971,10 +1050,11 @@ public CompletableFuture signOutUser(
throw new IllegalArgumentException("connectionName");
}
- return createOAuthClient(context).thenCompose(oAuthClient -> {
+ return createOAuthClient(context, null).thenCompose(oAuthClient -> {
return oAuthClient.getUserToken()
.signOut(
- context.getActivity().getFrom().getId(), connectionName,
+ context.getActivity().getFrom().getId(),
+ connectionName,
context.getActivity().getChannelId()
);
}).thenApply(signOutResult -> null);
@@ -1002,7 +1082,7 @@ public CompletableFuture> getTokenStatus(
throw new IllegalArgumentException("userId");
}
- return createOAuthClient(context).thenCompose(oAuthClient -> {
+ return createOAuthClient(context, null).thenCompose(oAuthClient -> {
return oAuthClient.getUserToken()
.getTokenStatus(userId, context.getActivity().getChannelId(), includeFilter);
});
@@ -1038,13 +1118,12 @@ public CompletableFuture
*
- * @param turnContext The context object for the current turn.
+ * @param turnContext The context object for the current turn.
+ * @param oAuthAppCredentials The credentials to use when creating the client.
+ * If null, the default credentials will be used.
* @return An OAuth client for the bot.
*/
- protected CompletableFuture createOAuthClient(TurnContext turnContext) {
+ protected CompletableFuture createOAuthClient(
+ TurnContext turnContext,
+ AppCredentials oAuthAppCredentials
+ ) {
if (
!OAuthClientConfig.emulateOAuthCards
&& StringUtils
@@ -1216,34 +1304,23 @@ protected CompletableFuture createOAuthClient(TurnContext turnConte
OAuthClientConfig.emulateOAuthCards = true;
}
- ConnectorClient connectorClient = turnContext.getTurnState().get(CONNECTOR_CLIENT_KEY);
- if (connectorClient == null) {
- CompletableFuture result = new CompletableFuture<>();
- result.completeExceptionally(
- new RuntimeException(
- "An ConnectorClient is required in TurnState for this operation."
- )
- );
- return result;
- }
+ String appId = getBotAppId(turnContext);
+ String oAuthScope = getBotFrameworkOAuthScope();
+ AppCredentials credentials = oAuthAppCredentials != null
+ ? oAuthAppCredentials
+ : getAppCredentials(appId, oAuthScope).join();
if (OAuthClientConfig.emulateOAuthCards) {
// do not join task - we want this to run in the background.
- OAuthClient oAuthClient = new RestOAuthClient(
- turnContext.getActivity().getServiceUrl(),
- connectorClient.getRestClient().credentials()
- );
+ OAuthClient oAuthClient =
+ new RestOAuthClient(turnContext.getActivity().getServiceUrl(), credentials);
return OAuthClientConfig
.sendEmulateOAuthCards(oAuthClient, OAuthClientConfig.emulateOAuthCards)
.thenApply(result -> oAuthClient);
}
- return CompletableFuture.completedFuture(
- new RestOAuthClient(
- OAuthClientConfig.OAUTHENDPOINT,
- connectorClient.getRestClient().credentials()
- )
- );
+ return CompletableFuture
+ .completedFuture(new RestOAuthClient(OAuthClientConfig.OAUTHENDPOINT, credentials));
}
/**
@@ -1256,9 +1333,11 @@ protected CompletableFuture createOAuthClient(TurnContext turnConte
* Anonymous ClaimsIdentity if
* authentication is turned off.
*/
+ @SuppressWarnings(value = "PMD")
private CompletableFuture createConnectorClient(
String serviceUrl,
- ClaimsIdentity claimsIdentity
+ ClaimsIdentity claimsIdentity,
+ String audience
) {
if (claimsIdentity == null) {
throw new UnsupportedOperationException(
@@ -1277,32 +1356,19 @@ private CompletableFuture createConnectorClient(
// For Activities coming from Emulator AppId claim contains the Bot's AAD AppId.
// For anonymous requests (requests with no header) appId is not set in claims.
- Map.Entry botAppIdClaim = claimsIdentity.claims()
- .entrySet()
- .stream()
- .filter(
- claim -> StringUtils.equals(claim.getKey(), AuthenticationConstants.AUDIENCE_CLAIM)
- )
- .findFirst()
- .orElse(null);
+ String botAppIdClaim = claimsIdentity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM);
if (botAppIdClaim == null) {
- botAppIdClaim = claimsIdentity.claims()
- .entrySet()
- .stream()
- .filter(
- claim -> StringUtils.equals(claim.getKey(), AuthenticationConstants.APPID_CLAIM)
- )
- .findFirst()
- .orElse(null);
+ botAppIdClaim = claimsIdentity.claims().get(AuthenticationConstants.APPID_CLAIM);
}
- if (botAppIdClaim == null) {
- return getOrCreateConnectorClient(serviceUrl);
+ if (botAppIdClaim != null) {
+ String scope = getBotFrameworkOAuthScope();
+
+ return getAppCredentials(botAppIdClaim, scope)
+ .thenCompose(credentials -> getOrCreateConnectorClient(serviceUrl, credentials));
}
- String botId = botAppIdClaim.getValue();
- return getAppCredentials(botId)
- .thenCompose(credentials -> getOrCreateConnectorClient(serviceUrl, credentials));
+ return getOrCreateConnectorClient(serviceUrl);
}
private CompletableFuture getOrCreateConnectorClient(String serviceUrl) {
@@ -1327,7 +1393,11 @@ protected CompletableFuture getOrCreateConnectorClient(
) {
CompletableFuture result = new CompletableFuture<>();
- String clientKey = serviceUrl + (appCredentials != null ? appCredentials.getAppId() : "");
+ String clientKey = keyForConnectorClient(
+ serviceUrl,
+ usingAppCredentials != null ? usingAppCredentials.getAppId() : null,
+ usingAppCredentials != null ? usingAppCredentials.oAuthScope() : null
+ );
result.complete(connectorClients.computeIfAbsent(clientKey, key -> {
try {
@@ -1375,30 +1445,76 @@ protected CompletableFuture getOrCreateConnectorClient(
* @param appId The application identifier (AAD Id for the bot).
* @return App credentials.
*/
- private CompletableFuture getAppCredentials(String appId) {
+ private CompletableFuture getAppCredentials(String appId, String scope) {
if (appId == null) {
return CompletableFuture.completedFuture(MicrosoftAppCredentials.empty());
}
- if (appCredentialMap.containsKey(appId)) {
- return CompletableFuture.completedFuture(appCredentialMap.get(appId));
+ String cacheKey = keyForAppCredentials(appId, scope);
+ if (appCredentialMap.containsKey(cacheKey)) {
+ return CompletableFuture.completedFuture(appCredentialMap.get(cacheKey));
}
// If app credentials were provided, use them as they are the preferred choice
// moving forward
if (appCredentials != null) {
- appCredentialMap.put(appId, appCredentials);
+ appCredentialMap.put(cacheKey, appCredentials);
}
return credentialProvider.getAppPassword(appId).thenApply(appPassword -> {
AppCredentials credentials = channelProvider != null && channelProvider.isGovernment()
? new MicrosoftGovernmentAppCredentials(appId, appPassword)
: new MicrosoftAppCredentials(appId, appPassword);
- appCredentialMap.put(appId, credentials);
+ appCredentialMap.put(cacheKey, credentials);
return credentials;
});
}
+ private String getBotAppId(TurnContext turnContext) {
+ ClaimsIdentity botIdentity = turnContext.getTurnState().get(BOT_IDENTITY_KEY);
+ if (botIdentity == null) {
+ throw new IllegalStateException(
+ "An IIdentity is required in TurnState for this operation."
+ );
+ }
+
+ String appId = botIdentity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM);
+ if (StringUtils.isEmpty(appId)) {
+ throw new IllegalStateException("Unable to get the bot AppId from the audience claim.");
+ }
+
+ return appId;
+ }
+
+ private String getBotFrameworkOAuthScope() {
+ return channelProvider != null && channelProvider.isGovernment()
+ ? GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+ : AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE;
+ }
+
+ /**
+ * Generates the key for accessing the app credentials cache.
+ *
+ * @param appId The appId
+ * @param scope The scope.
+ * @return The cache key
+ */
+ protected static String keyForAppCredentials(String appId, String scope) {
+ return appId + (StringUtils.isEmpty(scope) ? "" : scope);
+ }
+
+ /**
+ * Generates the key for accessing the connector client cache.
+ *
+ * @param serviceUrl The service url
+ * @param appId The app did
+ * @param scope The scope
+ * @return The cache key.
+ */
+ protected static String keyForConnectorClient(String serviceUrl, String appId, String scope) {
+ return serviceUrl + (appId == null ? "" : appId) + (scope == null ? "" : scope);
+ }
+
/**
* Middleware to assign tenantId from channelData to Conversation.TenantId.
*
@@ -1435,4 +1551,20 @@ public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next
return next.next();
}
}
+
+ /**
+ * Get the AppCredentials cache. For unit testing.
+ * @return The AppCredentials cache.
+ */
+ protected Map getCredentialsCache() {
+ return Collections.unmodifiableMap(appCredentialMap);
+ }
+
+ /**
+ * Get the ConnectorClient cache. For unit testing.
+ * @return The ConnectorClient cache.
+ */
+ protected Map getConnectorClientCache() {
+ return Collections.unmodifiableMap(connectorClients);
+ }
}
diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java
index 1de3e55e0..af3ec53d8 100644
--- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java
+++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java
@@ -8,15 +8,21 @@
import com.microsoft.bot.connector.ConnectorClient;
import com.microsoft.bot.connector.Conversations;
import com.microsoft.bot.connector.authentication.AppCredentials;
+import com.microsoft.bot.connector.authentication.AuthenticationConstants;
import com.microsoft.bot.connector.authentication.ClaimsIdentity;
import com.microsoft.bot.connector.authentication.CredentialProvider;
+import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants;
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
+import com.microsoft.bot.connector.authentication.SimpleChannelProvider;
import com.microsoft.bot.connector.authentication.SimpleCredentialProvider;
import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.CallerIdConstants;
import com.microsoft.bot.schema.ConversationAccount;
import com.microsoft.bot.schema.ConversationParameters;
import com.microsoft.bot.schema.ConversationReference;
import com.microsoft.bot.schema.ConversationResourceResponse;
+import java.util.HashMap;
+import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -87,10 +93,8 @@ protected CompletableFuture getOrCreateConnectorClient(
ObjectNode channelData = JsonNodeFactory.instance.objectNode();
channelData.set(
"tenant",
- JsonNodeFactory.instance.objectNode().set(
- "id",
- JsonNodeFactory.instance.textNode(TenantIdValue)
- )
+ JsonNodeFactory.instance.objectNode()
+ .set("id", JsonNodeFactory.instance.textNode(TenantIdValue))
);
Activity activity = new Activity("Test") {
@@ -119,7 +123,7 @@ protected CompletableFuture getOrCreateConnectorClient(
ConversationReference reference = activity.getConversationReference();
MicrosoftAppCredentials credentials = new MicrosoftAppCredentials(null, null);
- Activity[] newActivity = new Activity[] { null };
+ Activity[] newActivity = new Activity[] {null};
BotCallbackHandler updateParameters = (turnContext -> {
newActivity[0] = turnContext.getActivity();
return CompletableFuture.completedFuture(null);
@@ -159,7 +163,7 @@ private Activity processActivity(
tenantId.put("id", channelDataTenantId);
channelData.set("tenant", tenantId);
- Activity[] activity = new Activity[] { null };
+ Activity[] activity = new Activity[] {null};
sut.processActivity(mockClaims, new Activity("test") {
{
setChannelId(channelId);
@@ -178,4 +182,183 @@ private Activity processActivity(
return activity[0];
}
+
+ @Test
+ public void OutgoingActivityIdNotSent() {
+ CredentialProvider mockCredentials = mock(CredentialProvider.class);
+ BotFrameworkAdapter adapter = new BotFrameworkAdapter(mockCredentials);
+
+ Activity incoming_activity = new Activity("test") {
+ {
+ setId("testid");
+ setChannelId(Channels.DIRECTLINE);
+ setServiceUrl("https://fake.service.url");
+ setConversation(new ConversationAccount("cid"));
+ }
+ };
+
+ Activity reply = MessageFactory.text("test");
+ reply.setId("TestReplyId");
+
+ MemoryConnectorClient mockConnector = new MemoryConnectorClient();
+ TurnContext turnContext = new TurnContextImpl(adapter, incoming_activity);
+ turnContext.getTurnState().add(BotFrameworkAdapter.CONNECTOR_CLIENT_KEY, mockConnector);
+ turnContext.sendActivity(reply).join();
+
+ Assert.assertEquals(
+ 1,
+ ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size()
+ );
+ Assert.assertNull(
+ ((MemoryConversations) mockConnector.getConversations()).getSentActivities().get(0).getId()
+ );
+ }
+
+ @Test
+ public void processActivityCreatesCorrectCredsAndClient_anon() {
+ processActivityCreatesCorrectCredsAndClient(
+ null,
+ null,
+ null,
+ AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+ 0,
+ 1
+ );
+ }
+
+ @Test
+ public void processActivityCreatesCorrectCredsAndClient_public() {
+ processActivityCreatesCorrectCredsAndClient(
+ "00000000-0000-0000-0000-000000000001",
+ CallerIdConstants.PUBLIC_AZURE_CHANNEL,
+ null,
+ AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+ 1,
+ 1
+ );
+ }
+
+ @Test
+ public void processActivityCreatesCorrectCredsAndClient_gov() {
+ processActivityCreatesCorrectCredsAndClient(
+ "00000000-0000-0000-0000-000000000001",
+ CallerIdConstants.US_GOV_CHANNEL,
+ GovernmentAuthenticationConstants.CHANNELSERVICE,
+ GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+ 1,
+ 1
+ );
+ }
+
+ private void processActivityCreatesCorrectCredsAndClient(
+ String botAppId,
+ String expectedCallerId,
+ String channelService,
+ String expectedScope,
+ int expectedAppCredentialsCount,
+ int expectedClientCredentialsCount
+ ) {
+ HashMap claims = new HashMap() {
+ {
+ put(AuthenticationConstants.AUDIENCE_CLAIM, botAppId);
+ put(AuthenticationConstants.APPID_CLAIM, botAppId);
+ put(AuthenticationConstants.VERSION_CLAIM, "1.0");
+ }
+ };
+ ClaimsIdentity identity = new ClaimsIdentity("anonymous", claims);
+
+ CredentialProvider credentialProvider = new SimpleCredentialProvider() {
+ {
+ setAppId(botAppId);
+ }
+ };
+ String serviceUrl = "https://smba.trafficmanager.net/amer/";
+
+ BotFrameworkAdapter sut = new BotFrameworkAdapter(
+ credentialProvider,
+ new SimpleChannelProvider(channelService),
+ null,
+ null
+ );
+
+ BotCallbackHandler callback = turnContext -> {
+ getAppCredentialsAndAssertValues(
+ turnContext,
+ botAppId,
+ expectedScope,
+ expectedAppCredentialsCount
+ );
+
+ getConnectorClientAndAssertValues(
+ turnContext,
+ botAppId,
+ expectedScope,
+ serviceUrl,
+ expectedClientCredentialsCount
+ );
+
+ Assert.assertEquals(expectedCallerId, turnContext.getActivity().getCallerId());
+
+ return CompletableFuture.completedFuture(null);
+ };
+
+ sut.processActivity(identity, new Activity("test") {
+ {
+ setChannelId(Channels.EMULATOR);
+ setServiceUrl(serviceUrl);
+ }
+ }, callback).join();
+ }
+
+ private static void getAppCredentialsAndAssertValues(
+ TurnContext turnContext,
+ String expectedAppId,
+ String expectedScope,
+ int credsCount
+ ) {
+ if (credsCount > 0) {
+ Map credsCache =
+ ((BotFrameworkAdapter) turnContext.getAdapter()).getCredentialsCache();
+ AppCredentials creds = credsCache
+ .get(BotFrameworkAdapter.keyForAppCredentials(expectedAppId, expectedScope));
+
+ Assert
+ .assertEquals("Unexpected credentials cache count", credsCount, credsCache.size());
+
+ Assert.assertNotNull("Credentials not found", creds);
+ Assert.assertEquals("Unexpected app id", expectedAppId, creds.getAppId());
+ Assert.assertEquals("Unexpected scope", expectedScope, creds.oAuthScope());
+ }
+ }
+
+ private static void getConnectorClientAndAssertValues(
+ TurnContext turnContext,
+ String expectedAppId,
+ String expectedScope,
+ String expectedUrl,
+ int clientCount
+ ) {
+ Map clientCache =
+ ((BotFrameworkAdapter) turnContext.getAdapter()).getConnectorClientCache();
+
+ String cacheKey = expectedAppId == null
+ ? BotFrameworkAdapter.keyForConnectorClient(expectedUrl, null, null)
+ : BotFrameworkAdapter.keyForConnectorClient(expectedUrl, expectedAppId, expectedScope);
+ ConnectorClient client = clientCache.get(cacheKey);
+
+ Assert.assertNotNull("ConnectorClient not in cache", client);
+ Assert.assertEquals("Unexpected credentials cache count", clientCount, clientCache.size());
+ AppCredentials creds = (AppCredentials) client.credentials();
+ Assert.assertEquals(
+ "Unexpected app id",
+ expectedAppId,
+ creds == null ? null : creds.getAppId()
+ );
+ Assert.assertEquals(
+ "Unexpected scope",
+ expectedScope,
+ creds == null ? null : creds.oAuthScope()
+ );
+ Assert.assertEquals("Unexpected base url", expectedUrl, client.baseUrl());
+ }
}
diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConnectorClient.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConnectorClient.java
new file mode 100644
index 000000000..6e0320648
--- /dev/null
+++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConnectorClient.java
@@ -0,0 +1,76 @@
+package com.microsoft.bot.builder;
+
+import com.microsoft.bot.connector.Attachments;
+import com.microsoft.bot.connector.ConnectorClient;
+import com.microsoft.bot.connector.Conversations;
+import com.microsoft.bot.rest.RestClient;
+import com.microsoft.bot.rest.credentials.ServiceClientCredentials;
+
+public class MemoryConnectorClient implements ConnectorClient {
+ private MemoryConversations conversations = new MemoryConversations();
+
+ @Override
+ public RestClient getRestClient() {
+ return null;
+ }
+
+ @Override
+ public String baseUrl() {
+ return null;
+ }
+
+ @Override
+ public ServiceClientCredentials credentials() {
+ return null;
+ }
+
+ @Override
+ public String getUserAgent() {
+ return null;
+ }
+
+ @Override
+ public String getAcceptLanguage() {
+ return null;
+ }
+
+ @Override
+ public void setAcceptLanguage(String acceptLanguage) {
+
+ }
+
+ @Override
+ public int getLongRunningOperationRetryTimeout() {
+ return 0;
+ }
+
+ @Override
+ public void setLongRunningOperationRetryTimeout(int timeout) {
+
+ }
+
+ @Override
+ public boolean getGenerateClientRequestId() {
+ return false;
+ }
+
+ @Override
+ public void setGenerateClientRequestId(boolean generateClientRequestId) {
+
+ }
+
+ @Override
+ public Attachments getAttachments() {
+ return null;
+ }
+
+ @Override
+ public Conversations getConversations() {
+ return conversations;
+ }
+
+ @Override
+ public void close() throws Exception {
+
+ }
+}
diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConversations.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConversations.java
new file mode 100644
index 000000000..32da8e319
--- /dev/null
+++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/MemoryConversations.java
@@ -0,0 +1,145 @@
+package com.microsoft.bot.builder;
+
+import com.microsoft.bot.connector.Conversations;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.AttachmentData;
+import com.microsoft.bot.schema.ChannelAccount;
+import com.microsoft.bot.schema.ConversationParameters;
+import com.microsoft.bot.schema.ConversationResourceResponse;
+import com.microsoft.bot.schema.ConversationsResult;
+import com.microsoft.bot.schema.PagedMembersResult;
+import com.microsoft.bot.schema.ResourceResponse;
+import com.microsoft.bot.schema.Transcript;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import org.apache.commons.lang3.NotImplementedException;
+
+public class MemoryConversations implements Conversations {
+ private List sentActivities = new ArrayList<>();
+
+ public List getSentActivities() {
+ return sentActivities;
+ }
+
+ @Override
+ public CompletableFuture getConversations() {
+ return notImplemented("getConversations");
+ }
+
+ @Override
+ public CompletableFuture getConversations(String continuationToken) {
+ return notImplemented("getConversations");
+ }
+
+ @Override
+ public CompletableFuture createConversation(
+ ConversationParameters parameters
+ ) {
+ return notImplemented("createConversation");
+ }
+
+ @Override
+ public CompletableFuture sendToConversation(
+ String conversationId,
+ Activity activity
+ ) {
+ sentActivities.add(activity);
+ return CompletableFuture.completedFuture(new ResourceResponse() {
+ {
+ setId(activity.getId());
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture updateActivity(
+ String conversationId, String activityId,
+ Activity activity
+ ) {
+ return notImplemented("updateActivity");
+ }
+
+ @Override
+ public CompletableFuture replyToActivity(
+ String conversationId, String activityId,
+ Activity activity
+ ) {
+ sentActivities.add(activity);
+ return CompletableFuture.completedFuture(new ResourceResponse() {
+ {
+ setId(activity.getId());
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture deleteActivity(String conversationId, String activityId) {
+ return notImplemented("deleteActivity");
+ }
+
+ @Override
+ public CompletableFuture> getConversationMembers(
+ String conversationId
+ ) {
+ return notImplemented("getConversationMembers");
+ }
+
+ @Override
+ public CompletableFuture getConversationMember(
+ String userId, String conversationId
+ ) {
+ return notImplemented("getConversationMember");
+ }
+
+ @Override
+ public CompletableFuture deleteConversationMember(
+ String conversationId, String memberId
+ ) {
+ return notImplemented("deleteConversationMember");
+ }
+
+ @Override
+ public CompletableFuture> getActivityMembers(
+ String conversationId, String activityId
+ ) {
+ return notImplemented("getActivityMembers");
+ }
+
+ @Override
+ public CompletableFuture uploadAttachment(
+ String conversationId, AttachmentData attachmentUpload
+ ) {
+ return notImplemented("uploadAttachment");
+ }
+
+ @Override
+ public CompletableFuture sendConversationHistory(
+ String conversationId, Transcript history
+ ) {
+ return notImplemented("sendConversationHistory");
+ }
+
+ @Override
+ public CompletableFuture getConversationPagedMembers(
+ String conversationId
+ ) {
+ return notImplemented("getConversationPagedMembers");
+ }
+
+ @Override
+ public CompletableFuture getConversationPagedMembers(
+ String conversationId,
+ String continuationToken
+ ) {
+ return notImplemented("getConversationPagedMembers");
+ }
+
+ protected CompletableFuture notImplemented(String message) {
+ CompletableFuture result = new CompletableFuture<>();
+ result.completeExceptionally(
+ new NotImplementedException(message)
+ );
+ return result;
+ }
+}
diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java
index 59f4c1a89..3b6e5303e 100644
--- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java
+++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java
@@ -4,6 +4,7 @@
package com.microsoft.bot.connector.authentication;
import com.microsoft.bot.schema.Activity;
+import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.CompletableFuture;
@@ -170,4 +171,36 @@ public static CompletableFuture validateAuthHeader(
);
}
}
+
+ /**
+ * Gets the AppId from claims.
+ *
+ *
+ * In v1 tokens the AppId is in the the AppIdClaim claim. In v2 tokens the AppId
+ * is in the AuthorizedParty claim.
+ *
+ *
+ * @param claims The map of claims.
+ * @return The value of the appId claim if found (null if it can't find a
+ * suitable claim).
+ */
+ public static String getAppIdFromClaims(Map claims) {
+ if (claims == null) {
+ throw new IllegalArgumentException("claims");
+ }
+
+ String appId = null;
+
+ String tokenVersion = claims.get(AuthenticationConstants.VERSION_CLAIM);
+ if (StringUtils.isEmpty(tokenVersion) || tokenVersion.equalsIgnoreCase("1.0")) {
+ // either no Version or a version of "1.0" means we should look for the claim in
+ // the "appid" claim.
+ appId = claims.get(AuthenticationConstants.APPID_CLAIM);
+ } else {
+ // "2.0" puts the AppId in the "azp" claim.
+ appId = claims.get(AuthenticationConstants.AUTHORIZED_PARTY);
+ }
+
+ return appId;
+ }
}
diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TokenExchangeState.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TokenExchangeState.java
index fe781a265..3546a9db8 100644
--- a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TokenExchangeState.java
+++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/TokenExchangeState.java
@@ -26,9 +26,13 @@ public class TokenExchangeState {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private String botUrl;
+ @JsonProperty("relatesTo")
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private ConversationReference relatesTo;
+
/**
* The connection name that was used.
- *
+ *
* @return The connection name.
*/
public String getConnectionName() {
@@ -37,7 +41,7 @@ public String getConnectionName() {
/**
* The connection name that was used.
- *
+ *
* @param withConnectionName The connection name.
*/
public void setConnectionName(String withConnectionName) {
@@ -46,7 +50,7 @@ public void setConnectionName(String withConnectionName) {
/**
* A reference to the conversation.
- *
+ *
* @return The conversation reference.
*/
public ConversationReference getConversation() {
@@ -55,7 +59,7 @@ public ConversationReference getConversation() {
/**
* A reference to the conversation.
- *
+ *
* @param withConversation The conversation reference.
*/
public void setConversation(ConversationReference withConversation) {
@@ -64,7 +68,7 @@ public void setConversation(ConversationReference withConversation) {
/**
* The URL of the bot messaging endpoint.
- *
+ *
* @return The messaging endpoint.
*/
public String getBotUrl() {
@@ -73,7 +77,7 @@ public String getBotUrl() {
/**
* The URL of the bot messaging endpoint.
- *
+ *
* @param withBotUrl The messaging endpoint.
*/
public void setBotUrl(String withBotUrl) {
@@ -82,7 +86,7 @@ public void setBotUrl(String withBotUrl) {
/**
* The bot's registered application ID.
- *
+ *
* @return The app id.
*/
public String getMsAppId() {
@@ -91,10 +95,28 @@ public String getMsAppId() {
/**
* The bot's registered application ID.
- *
+ *
* @param withMsAppId The app id.
*/
public void setMsAppId(String withMsAppId) {
this.msAppId = withMsAppId;
}
+
+ /**
+ * Gets the reference to a related parent conversation for this token exchange.
+ *
+ * @return A reference to a related parent conversation.
+ */
+ public ConversationReference getRelatesTo() {
+ return relatesTo;
+ }
+
+ /**
+ * Sets the reference to a related parent conversation for this token exchange.
+ *
+ * @param withRelatesTo A reference to a related parent conversation.
+ */
+ public void setRelatesTo(ConversationReference withRelatesTo) {
+ relatesTo = withRelatesTo;
+ }
}