Merge pull request #97 from microsoft/tb-auth
Updated Connector Authorization
This commit is contained in:
Коммит
3014338782
|
@ -10,7 +10,7 @@ import com.microsoft.bot.connector.customizations.CredentialProvider;
|
|||
import com.microsoft.bot.connector.customizations.CredentialProviderImpl;
|
||||
import com.microsoft.bot.connector.customizations.JwtTokenValidation;
|
||||
import com.microsoft.bot.connector.customizations.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.rest.ConnectorClientImpl;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import com.microsoft.bot.schema.models.ActivityTypes;
|
||||
import com.microsoft.bot.schema.models.ResourceResponse;
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
|
||||
<!--
|
||||
|
||||
Checkstyle configuration that checks the sun coding conventions from:
|
||||
|
||||
- the Java Language Specification at
|
||||
https://docs.oracle.com/javase/specs/jls/se11/html/index.html
|
||||
|
||||
- the Sun Code Conventions at https://www.oracle.com/technetwork/java/codeconvtoc-136057.html
|
||||
|
||||
- the Javadoc guidelines at
|
||||
https://www.oracle.com/technetwork/java/javase/documentation/index-137868.html
|
||||
|
||||
- the JDK Api documentation https://docs.oracle.com/en/java/javase/11/
|
||||
|
||||
- some best practices
|
||||
|
||||
Checkstyle is very configurable. Be sure to read the documentation at
|
||||
http://checkstyle.sourceforge.net (or in your downloaded distribution).
|
||||
|
||||
Most Checks are configurable, be sure to consult the documentation.
|
||||
|
||||
To completely disable a check, just comment it out or delete it from the file.
|
||||
|
||||
Finally, it is worth reading the documentation.
|
||||
|
||||
-->
|
||||
|
||||
<module name="Checker">
|
||||
<!--
|
||||
If you set the basedir property below, then all reported file
|
||||
names will be relative to the specified directory. See
|
||||
https://checkstyle.org/5.x/config.html#Checker
|
||||
|
||||
<property name="basedir" value="${basedir}"/>
|
||||
-->
|
||||
|
||||
<property name="fileExtensions" value="java, properties, xml"/>
|
||||
|
||||
<!-- Excludes all 'module-info.java' files -->
|
||||
<!-- See https://checkstyle.org/config_filefilters.html -->
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||
</module>
|
||||
|
||||
<!-- Checks that a package-info.java file exists for each package. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_javadoc.html#JavadocPackage -->
|
||||
<module name="JavadocPackage"/>
|
||||
|
||||
<!-- Checks whether files end with a new line. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html#NewlineAtEndOfFile -->
|
||||
<module name="NewlineAtEndOfFile"/>
|
||||
|
||||
<!-- Checks that property files contain the same keys. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html#Translation -->
|
||||
<module name="Translation"/>
|
||||
|
||||
<!-- Checks for Size Violations. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_sizes.html -->
|
||||
<module name="FileLength"/>
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_whitespace.html -->
|
||||
<module name="FileTabCharacter"/>
|
||||
|
||||
<!-- Miscellaneous other checks. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html -->
|
||||
<module name="RegexpSingleline">
|
||||
<property name="format" value="\s+$"/>
|
||||
<property name="minimum" value="0"/>
|
||||
<property name="maximum" value="0"/>
|
||||
<property name="message" value="Line has trailing spaces."/>
|
||||
</module>
|
||||
|
||||
<!-- Checks for Headers -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_header.html -->
|
||||
<!-- <module name="Header"> -->
|
||||
<!-- <property name="headerFile" value="${checkstyle.header.file}"/> -->
|
||||
<!-- <property name="fileExtensions" value="java"/> -->
|
||||
<!-- </module> -->
|
||||
|
||||
<module name="TreeWalker">
|
||||
|
||||
<!-- Checks for Javadoc comments. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_javadoc.html -->
|
||||
<module name="JavadocMethod"/>
|
||||
<module name="JavadocType"/>
|
||||
<module name="JavadocVariable"/>
|
||||
<module name="JavadocStyle"/>
|
||||
|
||||
<!-- Checks for Naming Conventions. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_naming.html -->
|
||||
<module name="ConstantName"/>
|
||||
<module name="LocalFinalVariableName"/>
|
||||
<module name="LocalVariableName"/>
|
||||
<module name="MemberName"/>
|
||||
<module name="MethodName"/>
|
||||
<module name="PackageName"/>
|
||||
<module name="ParameterName"/>
|
||||
<module name="StaticVariableName"/>
|
||||
<module name="TypeName"/>
|
||||
|
||||
<!-- Checks for imports -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_import.html -->
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="IllegalImport"/> <!-- defaults to sun.* packages -->
|
||||
<module name="RedundantImport"/>
|
||||
<module name="UnusedImports">
|
||||
<property name="processJavadoc" value="false"/>
|
||||
</module>
|
||||
|
||||
<!-- Checks for Size Violations. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_sizes.html -->
|
||||
<module name="LineLength">
|
||||
<property name="max" value="120"/>
|
||||
</module>
|
||||
<module name="MethodLength"/>
|
||||
<module name="ParameterNumber"/>
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_whitespace.html -->
|
||||
<module name="EmptyForIteratorPad"/>
|
||||
<module name="GenericWhitespace"/>
|
||||
<module name="MethodParamPad"/>
|
||||
<module name="NoWhitespaceAfter"/>
|
||||
<module name="NoWhitespaceBefore"/>
|
||||
<module name="OperatorWrap"/>
|
||||
<module name="ParenPad"/>
|
||||
<module name="TypecastParenPad"/>
|
||||
<module name="WhitespaceAfter"/>
|
||||
<module name="WhitespaceAround"/>
|
||||
|
||||
<!-- Modifier Checks -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_modifiers.html -->
|
||||
<module name="ModifierOrder"/>
|
||||
<module name="RedundantModifier"/>
|
||||
|
||||
<!-- Checks for blocks. You know, those {}'s -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_blocks.html -->
|
||||
<module name="AvoidNestedBlocks"/>
|
||||
<module name="EmptyBlock"/>
|
||||
<module name="LeftCurly"/>
|
||||
<module name="NeedBraces"/>
|
||||
<module name="RightCurly"/>
|
||||
|
||||
<!-- Checks for common coding problems -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_coding.html -->
|
||||
<module name="EmptyStatement"/>
|
||||
<module name="EqualsHashCode"/>
|
||||
<module name="HiddenField"/>
|
||||
<module name="IllegalInstantiation"/>
|
||||
<module name="InnerAssignment"/>
|
||||
<module name="MagicNumber"/>
|
||||
<module name="MissingSwitchDefault"/>
|
||||
<module name="SimplifyBooleanExpression"/>
|
||||
<module name="SimplifyBooleanReturn"/>
|
||||
|
||||
<!-- Checks for class design -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_design.html -->
|
||||
<module name="DesignForExtension"/>
|
||||
<module name="FinalClass"/>
|
||||
<module name="HideUtilityClassConstructor"/>
|
||||
<module name="InterfaceIsType"/>
|
||||
<module name="VisibilityModifier"/>
|
||||
|
||||
<!-- Miscellaneous other checks. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html -->
|
||||
<module name="ArrayTypeStyle"/>
|
||||
<module name="FinalParameters"/>
|
||||
<module name="TodoComment"/>
|
||||
<module name="UpperEll"/>
|
||||
|
||||
</module>
|
||||
|
||||
</module>
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,310 +1,300 @@
|
|||
package com.microsoft.bot.builder;
|
||||
|
||||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The memory transcript store stores transcripts in volatile memory in a Dictionary.
|
||||
* <p>
|
||||
* <p>
|
||||
* Because this uses an unbounded volitile dictionary this should only be used for unit tests or non-production environments.
|
||||
*/
|
||||
public class MemoryTranscriptStore implements TranscriptStore {
|
||||
private HashMap<String, HashMap<String, ArrayList<Activity>>> channels = new HashMap<String, HashMap<String, ArrayList<Activity>>>();
|
||||
final ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory() {
|
||||
@Override
|
||||
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
|
||||
final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
|
||||
worker.setName("TestFlow-" + worker.getPoolIndex());
|
||||
return worker;
|
||||
}
|
||||
};
|
||||
|
||||
final ExecutorService executor = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), factory, null, false);
|
||||
|
||||
|
||||
/**
|
||||
* Logs an activity to the transcript.
|
||||
*
|
||||
* @param activity The activity to log.
|
||||
* @return A CompletableFuture that represents the work queued to execute.
|
||||
*/
|
||||
public final void LogActivityAsync(Activity activity) {
|
||||
if (activity == null) {
|
||||
throw new NullPointerException("activity cannot be null for LogActivity()");
|
||||
}
|
||||
|
||||
synchronized (this.channels) {
|
||||
HashMap<String, ArrayList<Activity>> channel;
|
||||
if (!this.channels.containsKey(activity.channelId())) {
|
||||
channel = new HashMap<String, ArrayList<Activity>>();
|
||||
this.channels.put(activity.channelId(), channel);
|
||||
} else {
|
||||
channel = this.channels.get(activity.channelId());
|
||||
}
|
||||
|
||||
ArrayList<Activity> transcript = null;
|
||||
|
||||
|
||||
if (!channel.containsKey(activity.conversation().id())) {
|
||||
transcript = new ArrayList<Activity>();
|
||||
channel.put(activity.conversation().id(), transcript);
|
||||
} else {
|
||||
transcript = channel.get(activity.conversation().id());
|
||||
}
|
||||
|
||||
transcript.add(activity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets from the store activities that match a set of criteria.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation.
|
||||
* @param continuationToken
|
||||
* @return A task that represents the work queued to execute.
|
||||
* If the task completes successfully, the result contains the matching activities.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Activity>> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken) {
|
||||
return GetTranscriptActivitiesAsync(channelId, conversationId, continuationToken, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets from the store activities that match a set of criteria.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* If the task completes successfully, the result contains the matching activities.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Activity>> GetTranscriptActivitiesAsync(String channelId, String conversationId) {
|
||||
return GetTranscriptActivitiesAsync(channelId, conversationId, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets from the store activities that match a set of criteria.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation.
|
||||
* @param continuationToken
|
||||
* @param startDate A cutoff date. Activities older than this date are not included.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* If the task completes successfully, the result contains the matching activities.
|
||||
*/
|
||||
public final CompletableFuture<PagedResult<Activity>> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken, DateTime startDate) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (channelId == null) {
|
||||
throw new NullPointerException(String.format("missing %1$s", "channelId"));
|
||||
}
|
||||
|
||||
if (conversationId == null) {
|
||||
throw new NullPointerException(String.format("missing %1$s", "conversationId"));
|
||||
}
|
||||
|
||||
PagedResult<Activity> pagedResult = new PagedResult<Activity>();
|
||||
synchronized (channels) {
|
||||
HashMap<String, ArrayList<Activity>> channel;
|
||||
if (!channels.containsKey(channelId)) {
|
||||
return pagedResult;
|
||||
}
|
||||
channel = channels.get(channelId);
|
||||
ArrayList<Activity> transcript;
|
||||
|
||||
if (!channel.containsKey(conversationId)) {
|
||||
return pagedResult;
|
||||
}
|
||||
transcript = channel.get(conversationId);
|
||||
if (continuationToken != null) {
|
||||
List<Activity> items = transcript.stream()
|
||||
.sorted(Comparator.comparing(Activity::timestamp))
|
||||
.filter(a -> a.timestamp().compareTo(startDate) >= 0)
|
||||
.filter(skipwhile(a -> !a.id().equals(continuationToken)))
|
||||
.skip(1)
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
pagedResult.items(items.toArray(new Activity[items.size()]));
|
||||
|
||||
if (pagedResult.getItems().length == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).id());
|
||||
}
|
||||
} else {
|
||||
List<Activity> items = transcript.stream()
|
||||
.sorted(Comparator.comparing(Activity::timestamp))
|
||||
.filter(a -> a.timestamp().compareTo((startDate == null) ? new DateTime(Long.MIN_VALUE) : startDate) >= 0)
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
pagedResult.items(items.toArray(new Activity[items.size()]));
|
||||
if (items.size() == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pagedResult;
|
||||
|
||||
}, this.executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes conversation data from the store.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation to delete.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
public final CompletableFuture DeleteTranscriptAsync(String channelId, String conversationId) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
if (channelId == null) {
|
||||
throw new NullPointerException(String.format("%1$s should not be null", "channelId"));
|
||||
}
|
||||
|
||||
if (conversationId == null) {
|
||||
throw new NullPointerException(String.format("%1$s should not be null", "conversationId"));
|
||||
}
|
||||
|
||||
synchronized (this.channels) {
|
||||
if (!this.channels.containsKey(channelId)) {
|
||||
return;
|
||||
}
|
||||
HashMap<String, ArrayList<Activity>> channel = this.channels.get(channelId);
|
||||
if (channel.containsKey(conversationId)) {
|
||||
channel.remove(conversationId);
|
||||
}
|
||||
}
|
||||
}, this.executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conversations on a channel from the store.
|
||||
*
|
||||
* @param channelId The ID of the channel.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Transcript>> ListTranscriptsAsync(String channelId) {
|
||||
return ListTranscriptsAsync(channelId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conversations on a channel from the store.
|
||||
*
|
||||
* @param channelId The ID of the channel.
|
||||
* @param continuationToken
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Transcript>> ListTranscriptsAsync(String channelId, String continuationToken) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (channelId == null) {
|
||||
throw new NullPointerException(String.format("missing %1$s", "channelId"));
|
||||
}
|
||||
|
||||
PagedResult<Transcript> pagedResult = new PagedResult<Transcript>();
|
||||
synchronized (channels) {
|
||||
|
||||
if (!channels.containsKey(channelId)) {
|
||||
return pagedResult;
|
||||
}
|
||||
|
||||
HashMap<String, ArrayList<Activity>> channel = channels.get(channelId);
|
||||
if (continuationToken != null) {
|
||||
List<Transcript> items = channel.entrySet().stream()
|
||||
.map(c -> {
|
||||
OffsetDateTime offsetDateTime = null;
|
||||
if (c.getValue().stream().findFirst().isPresent()) {
|
||||
DateTime dt = c.getValue().stream().findFirst().get().timestamp();
|
||||
// convert to DateTime to OffsetDateTime
|
||||
Instant instant = Instant.ofEpochMilli(dt.getMillis());
|
||||
ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant);
|
||||
offsetDateTime = instant.atOffset(offset);
|
||||
} else {
|
||||
offsetDateTime = OffsetDateTime.now();
|
||||
}
|
||||
return new Transcript()
|
||||
.withChannelId(channelId)
|
||||
.withId(c.getKey())
|
||||
.withCreated(offsetDateTime);
|
||||
}
|
||||
)
|
||||
.sorted(Comparator.comparing(Transcript::getCreated))
|
||||
.filter(skipwhile(c -> !c.getId().equals(continuationToken)))
|
||||
.skip(1)
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
pagedResult.items(items.toArray(new Transcript[items.size()]));
|
||||
if (items.size() == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).getId());
|
||||
}
|
||||
} else {
|
||||
|
||||
List<Transcript> items = channel.entrySet().stream()
|
||||
.map(c -> {
|
||||
OffsetDateTime offsetDateTime = null;
|
||||
if (c.getValue().stream().findFirst().isPresent()) {
|
||||
DateTime dt = c.getValue().stream().findFirst().get().timestamp();
|
||||
// convert to DateTime to OffsetDateTime
|
||||
Instant instant = Instant.ofEpochMilli(dt.getMillis());
|
||||
ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant);
|
||||
offsetDateTime = instant.atOffset(offset);
|
||||
} else {
|
||||
offsetDateTime = OffsetDateTime.now();
|
||||
}
|
||||
return new Transcript()
|
||||
.withChannelId(channelId)
|
||||
.withId(c.getKey())
|
||||
.withCreated(offsetDateTime);
|
||||
}
|
||||
)
|
||||
.sorted(Comparator.comparing(Transcript::getCreated))
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
pagedResult.items(items.toArray(new Transcript[items.size()]));
|
||||
if (items.size() == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
return pagedResult;
|
||||
}, this.executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulate C# SkipWhile.
|
||||
* Stateful
|
||||
*
|
||||
* @param func1 predicate to apply
|
||||
* @param <T> type
|
||||
* @return if the predicate condition is true
|
||||
*/
|
||||
public static <T> Predicate<T> skipwhile(Function<? super T, Object> func1) {
|
||||
final boolean[] started = {false};
|
||||
return t -> started[0] || (started[0] = (boolean) func1.apply(t));
|
||||
}
|
||||
|
||||
}
|
||||
package com.microsoft.bot.builder;
|
||||
|
||||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The memory transcript store stores transcripts in volatile memory in a Dictionary.
|
||||
* <p>
|
||||
* <p>
|
||||
* Because this uses an unbounded volitile dictionary this should only be used for unit tests or non-production environments.
|
||||
*/
|
||||
public class MemoryTranscriptStore implements TranscriptStore {
|
||||
private HashMap<String, HashMap<String, ArrayList<Activity>>> channels = new HashMap<String, HashMap<String, ArrayList<Activity>>>();
|
||||
|
||||
/**
|
||||
* Logs an activity to the transcript.
|
||||
*
|
||||
* @param activity The activity to log.
|
||||
* @return A CompletableFuture that represents the work queued to execute.
|
||||
*/
|
||||
public final void LogActivityAsync(Activity activity) {
|
||||
if (activity == null) {
|
||||
throw new NullPointerException("activity cannot be null for LogActivity()");
|
||||
}
|
||||
|
||||
synchronized (this.channels) {
|
||||
HashMap<String, ArrayList<Activity>> channel;
|
||||
if (!this.channels.containsKey(activity.channelId())) {
|
||||
channel = new HashMap<String, ArrayList<Activity>>();
|
||||
this.channels.put(activity.channelId(), channel);
|
||||
} else {
|
||||
channel = this.channels.get(activity.channelId());
|
||||
}
|
||||
|
||||
ArrayList<Activity> transcript = null;
|
||||
|
||||
|
||||
if (!channel.containsKey(activity.conversation().id())) {
|
||||
transcript = new ArrayList<Activity>();
|
||||
channel.put(activity.conversation().id(), transcript);
|
||||
} else {
|
||||
transcript = channel.get(activity.conversation().id());
|
||||
}
|
||||
|
||||
transcript.add(activity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets from the store activities that match a set of criteria.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation.
|
||||
* @param continuationToken
|
||||
* @return A task that represents the work queued to execute.
|
||||
* If the task completes successfully, the result contains the matching activities.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Activity>> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken) {
|
||||
return GetTranscriptActivitiesAsync(channelId, conversationId, continuationToken, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets from the store activities that match a set of criteria.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* If the task completes successfully, the result contains the matching activities.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Activity>> GetTranscriptActivitiesAsync(String channelId, String conversationId) {
|
||||
return GetTranscriptActivitiesAsync(channelId, conversationId, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets from the store activities that match a set of criteria.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation.
|
||||
* @param continuationToken
|
||||
* @param startDate A cutoff date. Activities older than this date are not included.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* If the task completes successfully, the result contains the matching activities.
|
||||
*/
|
||||
public final CompletableFuture<PagedResult<Activity>> GetTranscriptActivitiesAsync(String channelId, String conversationId, String continuationToken, DateTime startDate) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (channelId == null) {
|
||||
throw new NullPointerException(String.format("missing %1$s", "channelId"));
|
||||
}
|
||||
|
||||
if (conversationId == null) {
|
||||
throw new NullPointerException(String.format("missing %1$s", "conversationId"));
|
||||
}
|
||||
|
||||
PagedResult<Activity> pagedResult = new PagedResult<Activity>();
|
||||
synchronized (channels) {
|
||||
HashMap<String, ArrayList<Activity>> channel;
|
||||
if (!channels.containsKey(channelId)) {
|
||||
return pagedResult;
|
||||
}
|
||||
channel = channels.get(channelId);
|
||||
ArrayList<Activity> transcript;
|
||||
|
||||
if (!channel.containsKey(conversationId)) {
|
||||
return pagedResult;
|
||||
}
|
||||
transcript = channel.get(conversationId);
|
||||
if (continuationToken != null) {
|
||||
List<Activity> items = transcript.stream()
|
||||
.sorted(Comparator.comparing(Activity::timestamp))
|
||||
.filter(a -> a.timestamp().compareTo(startDate) >= 0)
|
||||
.filter(skipwhile(a -> !a.id().equals(continuationToken)))
|
||||
.skip(1)
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
pagedResult.items(items.toArray(new Activity[items.size()]));
|
||||
|
||||
if (pagedResult.getItems().length == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).id());
|
||||
}
|
||||
} else {
|
||||
List<Activity> items = transcript.stream()
|
||||
.sorted(Comparator.comparing(Activity::timestamp))
|
||||
.filter(a -> a.timestamp().compareTo((startDate == null) ? new DateTime(Long.MIN_VALUE) : startDate) >= 0)
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
pagedResult.items(items.toArray(new Activity[items.size()]));
|
||||
if (items.size() == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pagedResult;
|
||||
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes conversation data from the store.
|
||||
*
|
||||
* @param channelId The ID of the channel the conversation is in.
|
||||
* @param conversationId The ID of the conversation to delete.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
public final CompletableFuture DeleteTranscriptAsync(String channelId, String conversationId) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
if (channelId == null) {
|
||||
throw new NullPointerException(String.format("%1$s should not be null", "channelId"));
|
||||
}
|
||||
|
||||
if (conversationId == null) {
|
||||
throw new NullPointerException(String.format("%1$s should not be null", "conversationId"));
|
||||
}
|
||||
|
||||
synchronized (this.channels) {
|
||||
if (!this.channels.containsKey(channelId)) {
|
||||
return;
|
||||
}
|
||||
HashMap<String, ArrayList<Activity>> channel = this.channels.get(channelId);
|
||||
if (channel.containsKey(conversationId)) {
|
||||
channel.remove(conversationId);
|
||||
}
|
||||
}
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conversations on a channel from the store.
|
||||
*
|
||||
* @param channelId The ID of the channel.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Transcript>> ListTranscriptsAsync(String channelId) {
|
||||
return ListTranscriptsAsync(channelId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conversations on a channel from the store.
|
||||
*
|
||||
* @param channelId The ID of the channel.
|
||||
* @param continuationToken
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
|
||||
public final CompletableFuture<PagedResult<Transcript>> ListTranscriptsAsync(String channelId, String continuationToken) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (channelId == null) {
|
||||
throw new NullPointerException(String.format("missing %1$s", "channelId"));
|
||||
}
|
||||
|
||||
PagedResult<Transcript> pagedResult = new PagedResult<Transcript>();
|
||||
synchronized (channels) {
|
||||
|
||||
if (!channels.containsKey(channelId)) {
|
||||
return pagedResult;
|
||||
}
|
||||
|
||||
HashMap<String, ArrayList<Activity>> channel = channels.get(channelId);
|
||||
if (continuationToken != null) {
|
||||
List<Transcript> items = channel.entrySet().stream()
|
||||
.map(c -> {
|
||||
OffsetDateTime offsetDateTime = null;
|
||||
if (c.getValue().stream().findFirst().isPresent()) {
|
||||
DateTime dt = c.getValue().stream().findFirst().get().timestamp();
|
||||
// convert to DateTime to OffsetDateTime
|
||||
Instant instant = Instant.ofEpochMilli(dt.getMillis());
|
||||
ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant);
|
||||
offsetDateTime = instant.atOffset(offset);
|
||||
} else {
|
||||
offsetDateTime = OffsetDateTime.now();
|
||||
}
|
||||
return new Transcript()
|
||||
.withChannelId(channelId)
|
||||
.withId(c.getKey())
|
||||
.withCreated(offsetDateTime);
|
||||
}
|
||||
)
|
||||
.sorted(Comparator.comparing(Transcript::getCreated))
|
||||
.filter(skipwhile(c -> !c.getId().equals(continuationToken)))
|
||||
.skip(1)
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
pagedResult.items(items.toArray(new Transcript[items.size()]));
|
||||
if (items.size() == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).getId());
|
||||
}
|
||||
} else {
|
||||
|
||||
List<Transcript> items = channel.entrySet().stream()
|
||||
.map(c -> {
|
||||
OffsetDateTime offsetDateTime = null;
|
||||
if (c.getValue().stream().findFirst().isPresent()) {
|
||||
DateTime dt = c.getValue().stream().findFirst().get().timestamp();
|
||||
// convert to DateTime to OffsetDateTime
|
||||
Instant instant = Instant.ofEpochMilli(dt.getMillis());
|
||||
ZoneOffset offset = ZoneId.of(dt.getZone().getID()).getRules().getOffset(instant);
|
||||
offsetDateTime = instant.atOffset(offset);
|
||||
} else {
|
||||
offsetDateTime = OffsetDateTime.now();
|
||||
}
|
||||
return new Transcript()
|
||||
.withChannelId(channelId)
|
||||
.withId(c.getKey())
|
||||
.withCreated(offsetDateTime);
|
||||
}
|
||||
)
|
||||
.sorted(Comparator.comparing(Transcript::getCreated))
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
pagedResult.items(items.toArray(new Transcript[items.size()]));
|
||||
if (items.size() == 20) {
|
||||
pagedResult.withContinuationToken(items.get(items.size() - 1).getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
return pagedResult;
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulate C# SkipWhile.
|
||||
* Stateful
|
||||
*
|
||||
* @param func1 predicate to apply
|
||||
* @param <T> type
|
||||
* @return if the predicate condition is true
|
||||
*/
|
||||
public static <T> Predicate<T> skipwhile(Function<? super T, Object> func1) {
|
||||
final boolean[] started = {false};
|
||||
return t -> started[0] || (started[0] = (boolean) func1.apply(t));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,379 +1,379 @@
|
|||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.builder;
|
||||
|
||||
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.microsoft.bot.builder.adapters.TestAdapter;
|
||||
import com.microsoft.bot.builder.adapters.TestFlow;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.schema.models.ChannelAccount;
|
||||
import com.microsoft.bot.schema.models.ResourceResponse;
|
||||
import com.microsoft.rest.RestClient;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
||||
// [TestClass]
|
||||
// [TestCategory("State Management")]
|
||||
public class BotStateTest {
|
||||
protected ConnectorClientImpl connector;
|
||||
protected ChannelAccount bot;
|
||||
protected ChannelAccount user;
|
||||
|
||||
|
||||
protected void initializeClients(RestClient restClient, String botId, String userId) {
|
||||
|
||||
connector = new ConnectorClientImpl(restClient);
|
||||
bot = new ChannelAccount().withId(botId);
|
||||
user = new ChannelAccount().withId(userId);
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected void cleanUpResources() {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_DoNOTRememberContextState() throws ExecutionException, InterruptedException {
|
||||
|
||||
TestAdapter adapter = new TestAdapter();
|
||||
|
||||
new TestFlow(adapter, (context) -> {
|
||||
TestPocoState obj = StateTurnContextExtensions.<TestPocoState>GetConversationState(context);
|
||||
Assert.assertNull("context.state should not exist", obj); }
|
||||
)
|
||||
.Send("set value")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void State_RememberIStoreItemUserState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new UserState<TestState>(new MemoryStorage(), TestState::new));
|
||||
|
||||
|
||||
Consumer<TurnContext> callback = (context) -> {
|
||||
System.out.print(String.format("State_RememberIStoreItemUserState CALLBACK called.."));
|
||||
System.out.flush();
|
||||
TestState userState = StateTurnContextExtensions.<TestState>GetUserState(context);
|
||||
Assert.assertNotNull("user state should exist", userState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
userState.withValue("test");
|
||||
try {
|
||||
((TurnContextImpl)context).SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(userState.value()));
|
||||
((TurnContextImpl)context).SendActivity(userState.value());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
new TestFlow(adapter, callback)
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_RememberPocoUserState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new UserState<TestPocoState>(new MemoryStorage(), TestPocoState::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TestPocoState userState = StateTurnContextExtensions.<TestPocoState>GetUserState(context);
|
||||
|
||||
Assert.assertNotNull("user state should exist", userState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
userState.setValue("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(userState.getValue()));
|
||||
context.SendActivity(userState.getValue());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void State_RememberIStoreItemConversationState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TestState>(new MemoryStorage(), TestState::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TestState conversationState = StateTurnContextExtensions.<TestState>GetConversationState(context);
|
||||
Assert.assertNotNull("state.conversation should exist", conversationState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversationState.withValue("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(conversationState.value()));
|
||||
context.SendActivity(conversationState.value());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void State_RememberPocoConversationState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TestPocoState>(new MemoryStorage(), TestPocoState::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TestPocoState conversationState = StateTurnContextExtensions.<TestPocoState>GetConversationState(context);
|
||||
Assert.assertNotNull("state.conversation should exist", conversationState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversationState.setValue("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(conversationState.getValue()));
|
||||
context.SendActivity(conversationState.getValue());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_CustomStateManagerTest() throws ExecutionException, InterruptedException {
|
||||
|
||||
String testGuid = UUID.randomUUID().toString();
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new CustomKeyState(new MemoryStorage()));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
CustomState customState = CustomKeyState.Get(context);
|
||||
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
customState.setCustomString(testGuid);
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(customState.getCustomString()));
|
||||
context.SendActivity(customState.getCustomString());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", testGuid.toString())
|
||||
.StartTest();
|
||||
}
|
||||
@Test
|
||||
public void State_RoundTripTypedObjectwTrace() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TypedObject>(new MemoryStorage(), TypedObject::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): STARTING : %s", Thread.currentThread().getId(), context.getActivity().text()));
|
||||
System.out.flush();
|
||||
TypedObject conversation = StateTurnContextExtensions.<TypedObject>GetConversationState(context);
|
||||
Assert.assertNotNull("conversationstate should exist", conversation);
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Text is : %s", Thread.currentThread().getId(), context.getActivity().text()));
|
||||
System.out.flush();
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversation.withName("test");
|
||||
try {
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(),
|
||||
"value saved"));
|
||||
System.out.flush();
|
||||
ResourceResponse response = context.SendActivity("value saved");
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Response Id: %s", Thread.currentThread().getId(),
|
||||
response.id()));
|
||||
System.out.flush();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(),
|
||||
"TypedObject"));
|
||||
System.out.flush();
|
||||
context.SendActivity("TypedObject");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Turn("set value", "value saved", "Description", 50000)
|
||||
.Turn("get value", "TypedObject", "Description", 50000)
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void State_RoundTripTypedObject() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TypedObject>(new MemoryStorage(), TypedObject::new));
|
||||
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TypedObject conversation = StateTurnContextExtensions.<TypedObject>GetConversationState(context);
|
||||
Assert.assertNotNull("conversationstate should exist", conversation);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversation.withName("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
context.SendActivity("TypedObject");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "TypedObject")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_UseBotStateDirectly() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter();
|
||||
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
BotState botStateManager = new BotState<CustomState>(new MemoryStorage(), "BotState:com.microsoft.bot.builder.core.extensions.BotState<CustomState>",
|
||||
(ctx) -> String.format("botstate/%s/%s/com.microsoft.bot.builder.core.extensions.BotState<CustomState>",
|
||||
ctx.getActivity().channelId(), ctx.getActivity().conversation().id()), CustomState::new);
|
||||
|
||||
// read initial state object
|
||||
CustomState customState = null;
|
||||
try {
|
||||
customState = (CustomState) botStateManager.<CustomState>Read(context).join();
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail("Error reading custom state");
|
||||
}
|
||||
|
||||
// this should be a 'new CustomState' as nothing is currently stored in storage
|
||||
Assert.assertEquals(customState, new CustomState());
|
||||
|
||||
// amend property and write to storage
|
||||
customState.setCustomString("test");
|
||||
try {
|
||||
botStateManager.Write(context, customState).join();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail("Could not write customstate");
|
||||
}
|
||||
|
||||
// set customState to null before reading from storage
|
||||
customState = null;
|
||||
try {
|
||||
customState = (CustomState) botStateManager.<CustomState>Read(context).join();
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail("Could not read customstate back");
|
||||
}
|
||||
|
||||
// check object read from value has the correct value for CustomString
|
||||
Assert.assertEquals(customState.getCustomString(), "test");
|
||||
}
|
||||
)
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.builder;
|
||||
|
||||
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.microsoft.bot.builder.adapters.TestAdapter;
|
||||
import com.microsoft.bot.builder.adapters.TestFlow;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import com.microsoft.bot.schema.models.ChannelAccount;
|
||||
import com.microsoft.bot.schema.models.ResourceResponse;
|
||||
import com.microsoft.rest.RestClient;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
||||
// [TestClass]
|
||||
// [TestCategory("State Management")]
|
||||
public class BotStateTest {
|
||||
protected RestConnectorClient connector;
|
||||
protected ChannelAccount bot;
|
||||
protected ChannelAccount user;
|
||||
|
||||
|
||||
protected void initializeClients(RestClient restClient, String botId, String userId) {
|
||||
|
||||
connector = new RestConnectorClient(restClient);
|
||||
bot = new ChannelAccount().withId(botId);
|
||||
user = new ChannelAccount().withId(userId);
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected void cleanUpResources() {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_DoNOTRememberContextState() throws ExecutionException, InterruptedException {
|
||||
|
||||
TestAdapter adapter = new TestAdapter();
|
||||
|
||||
new TestFlow(adapter, (context) -> {
|
||||
TestPocoState obj = StateTurnContextExtensions.<TestPocoState>GetConversationState(context);
|
||||
Assert.assertNull("context.state should not exist", obj); }
|
||||
)
|
||||
.Send("set value")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void State_RememberIStoreItemUserState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new UserState<TestState>(new MemoryStorage(), TestState::new));
|
||||
|
||||
|
||||
Consumer<TurnContext> callback = (context) -> {
|
||||
System.out.print(String.format("State_RememberIStoreItemUserState CALLBACK called.."));
|
||||
System.out.flush();
|
||||
TestState userState = StateTurnContextExtensions.<TestState>GetUserState(context);
|
||||
Assert.assertNotNull("user state should exist", userState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
userState.withValue("test");
|
||||
try {
|
||||
((TurnContextImpl)context).SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(userState.value()));
|
||||
((TurnContextImpl)context).SendActivity(userState.value());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
new TestFlow(adapter, callback)
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_RememberPocoUserState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new UserState<TestPocoState>(new MemoryStorage(), TestPocoState::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TestPocoState userState = StateTurnContextExtensions.<TestPocoState>GetUserState(context);
|
||||
|
||||
Assert.assertNotNull("user state should exist", userState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
userState.setValue("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(userState.getValue()));
|
||||
context.SendActivity(userState.getValue());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void State_RememberIStoreItemConversationState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TestState>(new MemoryStorage(), TestState::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TestState conversationState = StateTurnContextExtensions.<TestState>GetConversationState(context);
|
||||
Assert.assertNotNull("state.conversation should exist", conversationState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversationState.withValue("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(conversationState.value()));
|
||||
context.SendActivity(conversationState.value());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void State_RememberPocoConversationState() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TestPocoState>(new MemoryStorage(), TestPocoState::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TestPocoState conversationState = StateTurnContextExtensions.<TestPocoState>GetConversationState(context);
|
||||
Assert.assertNotNull("state.conversation should exist", conversationState);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversationState.setValue("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(conversationState.getValue()));
|
||||
context.SendActivity(conversationState.getValue());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "test")
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_CustomStateManagerTest() throws ExecutionException, InterruptedException {
|
||||
|
||||
String testGuid = UUID.randomUUID().toString();
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new CustomKeyState(new MemoryStorage()));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
CustomState customState = CustomKeyState.Get(context);
|
||||
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
customState.setCustomString(testGuid);
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
Assert.assertFalse(StringUtils.isBlank(customState.getCustomString()));
|
||||
context.SendActivity(customState.getCustomString());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", testGuid.toString())
|
||||
.StartTest();
|
||||
}
|
||||
@Test
|
||||
public void State_RoundTripTypedObjectwTrace() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TypedObject>(new MemoryStorage(), TypedObject::new));
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): STARTING : %s", Thread.currentThread().getId(), context.getActivity().text()));
|
||||
System.out.flush();
|
||||
TypedObject conversation = StateTurnContextExtensions.<TypedObject>GetConversationState(context);
|
||||
Assert.assertNotNull("conversationstate should exist", conversation);
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Text is : %s", Thread.currentThread().getId(), context.getActivity().text()));
|
||||
System.out.flush();
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversation.withName("test");
|
||||
try {
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(),
|
||||
"value saved"));
|
||||
System.out.flush();
|
||||
ResourceResponse response = context.SendActivity("value saved");
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Response Id: %s", Thread.currentThread().getId(),
|
||||
response.id()));
|
||||
System.out.flush();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
System.out.println(String.format(">>Test Callback(tid:%s): Send activity : %s", Thread.currentThread().getId(),
|
||||
"TypedObject"));
|
||||
System.out.flush();
|
||||
context.SendActivity("TypedObject");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Turn("set value", "value saved", "Description", 50000)
|
||||
.Turn("get value", "TypedObject", "Description", 50000)
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void State_RoundTripTypedObject() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new ConversationState<TypedObject>(new MemoryStorage(), TypedObject::new));
|
||||
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
TypedObject conversation = StateTurnContextExtensions.<TypedObject>GetConversationState(context);
|
||||
Assert.assertNotNull("conversationstate should exist", conversation);
|
||||
switch (context.getActivity().text()) {
|
||||
case "set value":
|
||||
conversation.withName("test");
|
||||
try {
|
||||
context.SendActivity("value saved");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - set value"));
|
||||
}
|
||||
break;
|
||||
case "get value":
|
||||
try {
|
||||
context.SendActivity("TypedObject");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail(String.format("Error sending activity! - get value"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.Test("set value", "value saved")
|
||||
.Test("get value", "TypedObject")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void State_UseBotStateDirectly() throws ExecutionException, InterruptedException {
|
||||
TestAdapter adapter = new TestAdapter();
|
||||
|
||||
new TestFlow(adapter,
|
||||
(context) ->
|
||||
{
|
||||
BotState botStateManager = new BotState<CustomState>(new MemoryStorage(), "BotState:com.microsoft.bot.builder.core.extensions.BotState<CustomState>",
|
||||
(ctx) -> String.format("botstate/%s/%s/com.microsoft.bot.builder.core.extensions.BotState<CustomState>",
|
||||
ctx.getActivity().channelId(), ctx.getActivity().conversation().id()), CustomState::new);
|
||||
|
||||
// read initial state object
|
||||
CustomState customState = null;
|
||||
try {
|
||||
customState = (CustomState) botStateManager.<CustomState>Read(context).join();
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail("Error reading custom state");
|
||||
}
|
||||
|
||||
// this should be a 'new CustomState' as nothing is currently stored in storage
|
||||
Assert.assertEquals(customState, new CustomState());
|
||||
|
||||
// amend property and write to storage
|
||||
customState.setCustomString("test");
|
||||
try {
|
||||
botStateManager.Write(context, customState).join();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail("Could not write customstate");
|
||||
}
|
||||
|
||||
// set customState to null before reading from storage
|
||||
customState = null;
|
||||
try {
|
||||
customState = (CustomState) botStateManager.<CustomState>Read(context).join();
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
Assert.fail("Could not read customstate back");
|
||||
}
|
||||
|
||||
// check object read from value has the correct value for CustomString
|
||||
Assert.assertEquals(customState.getCustomString(), "test");
|
||||
}
|
||||
)
|
||||
.StartTest();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,109 +1,110 @@
|
|||
package com.microsoft.bot.builder;
|
||||
|
||||
import com.microsoft.bot.builder.adapters.TestAdapter;
|
||||
import com.microsoft.bot.builder.adapters.TestFlow;
|
||||
import com.microsoft.bot.schema.ActivityImpl;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class CatchException_MiddlewareTest {
|
||||
|
||||
@Test
|
||||
public void CatchException_TestMiddleware_TestStackedErrorMiddleware() throws ExecutionException, InterruptedException {
|
||||
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new CatchExceptionMiddleware<Exception>(new CallOnException() {
|
||||
@Override
|
||||
public <T> CompletableFuture apply(TurnContext context, T t) throws Exception {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
Activity activity = context.getActivity();
|
||||
if (activity instanceof ActivityImpl) {
|
||||
try {
|
||||
context.SendActivity(((ActivityImpl) activity).CreateReply(t.toString()));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(String.format("CatchException_TestMiddleware_TestStackedErrorMiddleware:SendActivity failed %s", e.toString()));
|
||||
}
|
||||
} else
|
||||
Assert.assertTrue("Test was built for ActivityImpl", false);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}, Exception.class))
|
||||
// Add middleware to catch NullReferenceExceptions before throwing up to the general exception instance
|
||||
.Use(new CatchExceptionMiddleware<NullPointerException>(new CallOnException() {
|
||||
@Override
|
||||
public <T> CompletableFuture apply(TurnContext context, T t) throws Exception {
|
||||
context.SendActivity("Sorry - Null Reference Exception");
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}, NullPointerException.class));
|
||||
|
||||
|
||||
new TestFlow(adapter, (context) ->
|
||||
{
|
||||
|
||||
if (context.getActivity().text() == "foo") {
|
||||
try {
|
||||
context.SendActivity(context.getActivity().text());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (context.getActivity().text() == "UnsupportedOperationException") {
|
||||
throw new UnsupportedOperationException("Test");
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
.Send("foo")
|
||||
.AssertReply("foo", "passthrough")
|
||||
.Send("UnsupportedOperationException")
|
||||
.AssertReply("Test")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
/* @Test
|
||||
// [TestCategory("Middleware")]
|
||||
public void CatchException_TestMiddleware_SpecificExceptionType()
|
||||
{
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new CatchExceptionMiddleware<Exception>((context, exception) =>
|
||||
{
|
||||
context.SendActivity("Generic Exception Caught");
|
||||
return CompletableFuture.CompletedTask;
|
||||
}))
|
||||
.Use(new CatchExceptionMiddleware<NullReferenceException>((context, exception) =>
|
||||
{
|
||||
context.SendActivity(exception.Message);
|
||||
return CompletableFuture.CompletedTask;
|
||||
}));
|
||||
|
||||
|
||||
await new TestFlow(adapter, (context) =>
|
||||
{
|
||||
if (context.Activity.AsMessageActivity().Text == "foo")
|
||||
{
|
||||
context.SendActivity(context.Activity.AsMessageActivity().Text);
|
||||
}
|
||||
|
||||
if (context.Activity.AsMessageActivity().Text == "NullReferenceException")
|
||||
{
|
||||
throw new NullReferenceException("Test");
|
||||
}
|
||||
|
||||
return CompletableFuture.CompletedTask;
|
||||
})
|
||||
.Send("foo")
|
||||
.AssertReply("foo", "passthrough")
|
||||
.Send("NullReferenceException")
|
||||
.AssertReply("Test")
|
||||
.StartTest();
|
||||
}*/
|
||||
}
|
||||
package com.microsoft.bot.builder;
|
||||
|
||||
import com.microsoft.bot.builder.adapters.TestAdapter;
|
||||
import com.microsoft.bot.builder.adapters.TestFlow;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import com.microsoft.bot.schema.ActivityImpl;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class CatchException_MiddlewareTest {
|
||||
|
||||
@Test
|
||||
public void CatchException_TestMiddleware_TestStackedErrorMiddleware() throws ExecutionException, InterruptedException {
|
||||
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new CatchExceptionMiddleware<Exception>(new CallOnException() {
|
||||
@Override
|
||||
public <T> CompletableFuture apply(TurnContext context, T t) throws Exception {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
Activity activity = context.getActivity();
|
||||
if (activity instanceof ActivityImpl) {
|
||||
try {
|
||||
context.SendActivity(((ActivityImpl) activity).CreateReply(t.toString()));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(String.format("CatchException_TestMiddleware_TestStackedErrorMiddleware:SendActivity failed %s", e.toString()));
|
||||
}
|
||||
} else
|
||||
Assert.assertTrue("Test was built for ActivityImpl", false);
|
||||
|
||||
}, ExecutorFactory.getExecutor());
|
||||
|
||||
}
|
||||
}, Exception.class))
|
||||
// Add middleware to catch NullReferenceExceptions before throwing up to the general exception instance
|
||||
.Use(new CatchExceptionMiddleware<NullPointerException>(new CallOnException() {
|
||||
@Override
|
||||
public <T> CompletableFuture apply(TurnContext context, T t) throws Exception {
|
||||
context.SendActivity("Sorry - Null Reference Exception");
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}, NullPointerException.class));
|
||||
|
||||
|
||||
new TestFlow(adapter, (context) ->
|
||||
{
|
||||
|
||||
if (context.getActivity().text() == "foo") {
|
||||
try {
|
||||
context.SendActivity(context.getActivity().text());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (context.getActivity().text() == "UnsupportedOperationException") {
|
||||
throw new UnsupportedOperationException("Test");
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
.Send("foo")
|
||||
.AssertReply("foo", "passthrough")
|
||||
.Send("UnsupportedOperationException")
|
||||
.AssertReply("Test")
|
||||
.StartTest();
|
||||
|
||||
}
|
||||
|
||||
/* @Test
|
||||
// [TestCategory("Middleware")]
|
||||
public void CatchException_TestMiddleware_SpecificExceptionType()
|
||||
{
|
||||
TestAdapter adapter = new TestAdapter()
|
||||
.Use(new CatchExceptionMiddleware<Exception>((context, exception) =>
|
||||
{
|
||||
context.SendActivity("Generic Exception Caught");
|
||||
return CompletableFuture.CompletedTask;
|
||||
}))
|
||||
.Use(new CatchExceptionMiddleware<NullReferenceException>((context, exception) =>
|
||||
{
|
||||
context.SendActivity(exception.Message);
|
||||
return CompletableFuture.CompletedTask;
|
||||
}));
|
||||
|
||||
|
||||
await new TestFlow(adapter, (context) =>
|
||||
{
|
||||
if (context.Activity.AsMessageActivity().Text == "foo")
|
||||
{
|
||||
context.SendActivity(context.Activity.AsMessageActivity().Text);
|
||||
}
|
||||
|
||||
if (context.Activity.AsMessageActivity().Text == "NullReferenceException")
|
||||
{
|
||||
throw new NullReferenceException("Test");
|
||||
}
|
||||
|
||||
return CompletableFuture.CompletedTask;
|
||||
})
|
||||
.Send("foo")
|
||||
.AssertReply("foo", "passthrough")
|
||||
.Send("NullReferenceException")
|
||||
.AssertReply("Test")
|
||||
.StartTest();
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -1,129 +1,130 @@
|
|||
package com.microsoft.bot.builder;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
|
||||
/**
|
||||
* Models IStorage around a dictionary
|
||||
*/
|
||||
public class DictionaryStorage implements Storage {
|
||||
private static ObjectMapper objectMapper;
|
||||
|
||||
// TODO: Object needs to be defined
|
||||
private final Map<String, Object> memory;
|
||||
private final Object syncroot = new Object();
|
||||
private int _eTag = 0;
|
||||
private final String typeNameForNonEntity = "__type_name_";
|
||||
|
||||
public DictionaryStorage() {
|
||||
this(null);
|
||||
}
|
||||
public DictionaryStorage(Map<String, Object> dictionary ) {
|
||||
DictionaryStorage.objectMapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.findAndRegisterModules();
|
||||
this.memory = (dictionary != null) ? dictionary : new HashMap<String, Object>();
|
||||
}
|
||||
|
||||
public CompletableFuture Delete(String[] keys) {
|
||||
synchronized (this.syncroot) {
|
||||
for (String key : keys) {
|
||||
Object o = this.memory.get(key);
|
||||
this.memory.remove(o);
|
||||
}
|
||||
}
|
||||
return completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Map<String, ?>> Read(String[] keys) throws JsonProcessingException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
Map<String, Object> storeItems = new HashMap<String, Object>(keys.length);
|
||||
synchronized (this.syncroot) {
|
||||
for (String key : keys) {
|
||||
if (this.memory.containsKey(key)) {
|
||||
Object state = this.memory.get(key);
|
||||
if (state != null) {
|
||||
try {
|
||||
if (!(state instanceof JsonNode))
|
||||
throw new RuntimeException("DictionaryRead failed: entry not JsonNode");
|
||||
JsonNode stateNode = (JsonNode) state;
|
||||
// Check if type info is set for the class
|
||||
if (!(stateNode.hasNonNull(this.typeNameForNonEntity))) {
|
||||
throw new RuntimeException(String.format("DictionaryRead failed: Type info not present"));
|
||||
}
|
||||
String clsName = stateNode.get(this.typeNameForNonEntity).textValue();
|
||||
|
||||
// Load the class info
|
||||
Class<?> cls;
|
||||
try {
|
||||
cls = Class.forName(clsName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(String.format("DictionaryRead failed: Could not load class %s", clsName));
|
||||
}
|
||||
|
||||
// Populate dictionary
|
||||
storeItems.put(key,DictionaryStorage.objectMapper.treeToValue(stateNode, cls ));
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(String.format("DictionaryRead failed: %s", e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return storeItems;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture Write(Map<String, ?> changes) throws Exception {
|
||||
synchronized (this.syncroot) {
|
||||
for (Map.Entry change : changes.entrySet()) {
|
||||
Object newValue = change.getValue();
|
||||
|
||||
String oldStateETag = null; // default(string);
|
||||
if (this.memory.containsValue(change.getKey())) {
|
||||
Map oldState = (Map) this.memory.get(change.getKey());
|
||||
if (oldState.containsValue("eTag")) {
|
||||
Map.Entry eTagToken = (Map.Entry) oldState.get("eTag");
|
||||
oldStateETag = (String) eTagToken.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
// Dictionary stores Key:JsonNode (with type information held within the JsonNode)
|
||||
JsonNode newState = DictionaryStorage.objectMapper.valueToTree(newValue);
|
||||
((ObjectNode)newState).put(this.typeNameForNonEntity, newValue.getClass().getTypeName());
|
||||
|
||||
// Set ETag if applicable
|
||||
if (newValue instanceof StoreItem) {
|
||||
StoreItem newStoreItem = (StoreItem) newValue;
|
||||
if(oldStateETag != null && newStoreItem.geteTag() != "*" &&
|
||||
newStoreItem.geteTag() != oldStateETag) {
|
||||
throw new Exception(String.format("Etag conflict.\r\n\r\nOriginal: %s\r\nCurrent: %s",
|
||||
newStoreItem.geteTag(), oldStateETag));
|
||||
}
|
||||
Integer newTag = _eTag++;
|
||||
((ObjectNode)newState).put("eTag", newTag.toString());
|
||||
}
|
||||
|
||||
this.memory.put((String)change.getKey(), newState);
|
||||
}
|
||||
}
|
||||
return completedFuture(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package com.microsoft.bot.builder;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
|
||||
/**
|
||||
* Models IStorage around a dictionary
|
||||
*/
|
||||
public class DictionaryStorage implements Storage {
|
||||
private static ObjectMapper objectMapper;
|
||||
|
||||
// TODO: Object needs to be defined
|
||||
private final Map<String, Object> memory;
|
||||
private final Object syncroot = new Object();
|
||||
private int _eTag = 0;
|
||||
private final String typeNameForNonEntity = "__type_name_";
|
||||
|
||||
public DictionaryStorage() {
|
||||
this(null);
|
||||
}
|
||||
public DictionaryStorage(Map<String, Object> dictionary ) {
|
||||
DictionaryStorage.objectMapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.findAndRegisterModules();
|
||||
this.memory = (dictionary != null) ? dictionary : new HashMap<String, Object>();
|
||||
}
|
||||
|
||||
public CompletableFuture Delete(String[] keys) {
|
||||
synchronized (this.syncroot) {
|
||||
for (String key : keys) {
|
||||
Object o = this.memory.get(key);
|
||||
this.memory.remove(o);
|
||||
}
|
||||
}
|
||||
return completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Map<String, ?>> Read(String[] keys) throws JsonProcessingException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
Map<String, Object> storeItems = new HashMap<String, Object>(keys.length);
|
||||
synchronized (this.syncroot) {
|
||||
for (String key : keys) {
|
||||
if (this.memory.containsKey(key)) {
|
||||
Object state = this.memory.get(key);
|
||||
if (state != null) {
|
||||
try {
|
||||
if (!(state instanceof JsonNode))
|
||||
throw new RuntimeException("DictionaryRead failed: entry not JsonNode");
|
||||
JsonNode stateNode = (JsonNode) state;
|
||||
// Check if type info is set for the class
|
||||
if (!(stateNode.hasNonNull(this.typeNameForNonEntity))) {
|
||||
throw new RuntimeException(String.format("DictionaryRead failed: Type info not present"));
|
||||
}
|
||||
String clsName = stateNode.get(this.typeNameForNonEntity).textValue();
|
||||
|
||||
// Load the class info
|
||||
Class<?> cls;
|
||||
try {
|
||||
cls = Class.forName(clsName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(String.format("DictionaryRead failed: Could not load class %s", clsName));
|
||||
}
|
||||
|
||||
// Populate dictionary
|
||||
storeItems.put(key,DictionaryStorage.objectMapper.treeToValue(stateNode, cls ));
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(String.format("DictionaryRead failed: %s", e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return storeItems;
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture Write(Map<String, ?> changes) throws Exception {
|
||||
synchronized (this.syncroot) {
|
||||
for (Map.Entry change : changes.entrySet()) {
|
||||
Object newValue = change.getValue();
|
||||
|
||||
String oldStateETag = null; // default(string);
|
||||
if (this.memory.containsValue(change.getKey())) {
|
||||
Map oldState = (Map) this.memory.get(change.getKey());
|
||||
if (oldState.containsValue("eTag")) {
|
||||
Map.Entry eTagToken = (Map.Entry) oldState.get("eTag");
|
||||
oldStateETag = (String) eTagToken.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
// Dictionary stores Key:JsonNode (with type information held within the JsonNode)
|
||||
JsonNode newState = DictionaryStorage.objectMapper.valueToTree(newValue);
|
||||
((ObjectNode)newState).put(this.typeNameForNonEntity, newValue.getClass().getTypeName());
|
||||
|
||||
// Set ETag if applicable
|
||||
if (newValue instanceof StoreItem) {
|
||||
StoreItem newStoreItem = (StoreItem) newValue;
|
||||
if(oldStateETag != null && newStoreItem.geteTag() != "*" &&
|
||||
newStoreItem.geteTag() != oldStateETag) {
|
||||
throw new Exception(String.format("Etag conflict.\r\n\r\nOriginal: %s\r\nCurrent: %s",
|
||||
newStoreItem.geteTag(), oldStateETag));
|
||||
}
|
||||
Integer newTag = _eTag++;
|
||||
((ObjectNode)newState).put("eTag", newTag.toString());
|
||||
}
|
||||
|
||||
this.memory.put((String)change.getKey(), newState);
|
||||
}
|
||||
}
|
||||
return completedFuture(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,468 +1,469 @@
|
|||
package com.microsoft.bot.builder.adapters;
|
||||
|
||||
import com.microsoft.bot.builder.TurnContext;
|
||||
import com.microsoft.bot.schema.ActivityImpl;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
|
||||
public class TestFlow {
|
||||
final TestAdapter adapter;
|
||||
CompletableFuture<String> testTask;
|
||||
Consumer<TurnContext> callback;
|
||||
|
||||
ArrayList<Supplier<String>> tasks = new ArrayList<Supplier<String>>();
|
||||
ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory()
|
||||
{
|
||||
@Override
|
||||
public ForkJoinWorkerThread newThread(ForkJoinPool pool)
|
||||
{
|
||||
final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
|
||||
worker.setName("TestFlow-" + worker.getPoolIndex());
|
||||
return worker;
|
||||
}
|
||||
};
|
||||
|
||||
ExecutorService executor = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), factory, null, true);
|
||||
|
||||
|
||||
public TestFlow(TestAdapter adapter) {
|
||||
this(adapter, null);
|
||||
}
|
||||
|
||||
public TestFlow(TestAdapter adapter, Consumer<TurnContext> callback) {
|
||||
this.adapter = adapter;
|
||||
this.callback = callback;
|
||||
this.testTask = completedFuture(null);
|
||||
}
|
||||
|
||||
|
||||
public TestFlow(Supplier<String> testTask, TestFlow flow) {
|
||||
this.tasks = flow.tasks;
|
||||
if (testTask != null)
|
||||
this.tasks.add(testTask);
|
||||
this.callback = flow.callback;
|
||||
this.adapter = flow.adapter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the execution of the test flow
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String StartTest() throws ExecutionException, InterruptedException {
|
||||
|
||||
System.out.printf("+------------------------------------------+\n");
|
||||
int count = 0;
|
||||
for (Supplier<String> task : this.tasks) {
|
||||
System.out.printf("| Running task %s of %s\n", count++, this.tasks.size());
|
||||
String result = null;
|
||||
result = task.get();
|
||||
System.out.printf("| --> Result: %s", result);
|
||||
System.out.flush();
|
||||
}
|
||||
System.out.printf("+------------------------------------------+\n");
|
||||
return "Completed";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message from the user to the bot
|
||||
*
|
||||
* @param userSays
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Send(String userSays) throws IllegalArgumentException {
|
||||
if (userSays == null)
|
||||
throw new IllegalArgumentException("You have to pass a userSays parameter");
|
||||
|
||||
// Function<TurnContextImpl, CompletableFuture>
|
||||
return new TestFlow((() -> {
|
||||
System.out.print(String.format("USER SAYS: %s (Thread Id: %s)\n", userSays, Thread.currentThread().getId()));
|
||||
System.out.flush();
|
||||
try {
|
||||
this.adapter.SendTextToBot(userSays, this.callback);
|
||||
return "Successfully sent " + userSays;
|
||||
} catch (Exception e) {
|
||||
Assert.fail(e.getMessage());
|
||||
return e.getMessage();
|
||||
}
|
||||
}), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an activity from the user to the bot
|
||||
*
|
||||
* @param userActivity
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Send(Activity userActivity) {
|
||||
if (userActivity == null)
|
||||
throw new IllegalArgumentException("You have to pass an Activity");
|
||||
|
||||
return new TestFlow((() -> {
|
||||
System.out.printf("TestFlow(%s): Send with User Activity! %s", Thread.currentThread().getId(), userActivity.text());
|
||||
System.out.flush();
|
||||
|
||||
|
||||
try {
|
||||
this.adapter.ProcessActivity((ActivityImpl) userActivity, this.callback);
|
||||
return "TestFlow: Send() -> ProcessActivity: " + userActivity.text();
|
||||
} catch (Exception e) {
|
||||
return e.getMessage();
|
||||
|
||||
}
|
||||
|
||||
}), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay for time period
|
||||
*
|
||||
* @param ms
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Delay(int ms) {
|
||||
return new TestFlow(() ->
|
||||
{
|
||||
System.out.printf("TestFlow(%s): Delay(%s ms) called. ", Thread.currentThread().getId(), ms);
|
||||
System.out.flush();
|
||||
try {
|
||||
Thread.sleep((int) ms);
|
||||
} catch (InterruptedException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
return null;
|
||||
}, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that reply is expected text
|
||||
*
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReply(String expected) {
|
||||
return this.AssertReply(expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(String expected, String description) {
|
||||
return this.AssertReply(expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(String expected, String description, int timeout) {
|
||||
return this.AssertReply(this.adapter.MakeActivity(expected), description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the reply is expected activity
|
||||
*
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReply(Activity expected) {
|
||||
String description = Thread.currentThread().getStackTrace()[1].getMethodName();
|
||||
return AssertReply(expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(Activity expected, String description, int timeout) {
|
||||
if (description == null)
|
||||
description = Thread.currentThread().getStackTrace()[1].getMethodName();
|
||||
String finalDescription = description;
|
||||
return this.AssertReply((reply) -> {
|
||||
if (expected.type() != reply.type())
|
||||
return String.format("%s: Type should match", finalDescription);
|
||||
if (expected.text().equals(reply.text())) {
|
||||
if (finalDescription == null)
|
||||
return String.format("Expected:%s\nReceived:{reply.AsMessageActivity().Text}", expected.text());
|
||||
else
|
||||
return String.format("%s: Text should match", finalDescription);
|
||||
}
|
||||
// TODO, expand this to do all properties set on expected
|
||||
return null;
|
||||
}, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the reply matches a custom validation routine
|
||||
*
|
||||
* @param validateActivity
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReply(Function<Activity, String> validateActivity) {
|
||||
String description = Thread.currentThread().getStackTrace()[1].getMethodName();
|
||||
return AssertReply(validateActivity, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(Function<Activity, String> validateActivity, String description) {
|
||||
return AssertReply(validateActivity, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(Function<Activity, String> validateActivity, String description, int timeout) {
|
||||
return new TestFlow(() -> {
|
||||
System.out.println(String.format("AssertReply: Starting loop : %s (Thread:%s)", description, Thread.currentThread().getId()));
|
||||
System.out.flush();
|
||||
|
||||
int finalTimeout = Integer.MAX_VALUE;
|
||||
if (isDebug())
|
||||
finalTimeout = Integer.MAX_VALUE;
|
||||
|
||||
DateTime start = DateTime.now();
|
||||
while (true) {
|
||||
DateTime current = DateTime.now();
|
||||
|
||||
if ((current.getMillis() - start.getMillis()) > (long) finalTimeout) {
|
||||
System.out.println("AssertReply: Timeout!\n");
|
||||
System.out.flush();
|
||||
return String.format("%d ms Timed out waiting for:'%s'", finalTimeout, description);
|
||||
}
|
||||
|
||||
// System.out.println("Before GetNextReply\n");
|
||||
// System.out.flush();
|
||||
|
||||
Activity replyActivity = this.adapter.GetNextReply();
|
||||
// System.out.println("After GetNextReply\n");
|
||||
// System.out.flush();
|
||||
|
||||
if (replyActivity != null) {
|
||||
System.out.printf("AssertReply(tid:%s): Received Reply: %s ", Thread.currentThread().getId(), (replyActivity.text() == null) ? "No Text set" : replyActivity.text());
|
||||
System.out.flush();
|
||||
System.out.printf("=============\n From: %s\n To:%s\n ==========\n", (replyActivity.from() == null) ? "No from set" : replyActivity.from().name(),
|
||||
(replyActivity.recipient() == null) ? "No recipient set" : replyActivity.recipient().name());
|
||||
System.out.flush();
|
||||
|
||||
// if we have a reply
|
||||
return validateActivity.apply(replyActivity);
|
||||
} else {
|
||||
System.out.printf("AssertReply(tid:%s): Waiting..\n", Thread.currentThread().getId());
|
||||
System.out.flush();
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
// Hack to determine if debugger attached..
|
||||
public boolean isDebug() {
|
||||
for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
|
||||
if (arg.contains("jdwp=")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Turn(String userSays, String expected, String description, int timeout) {
|
||||
String result = null;
|
||||
try {
|
||||
|
||||
result = CompletableFuture.supplyAsync(() -> { // Send the message
|
||||
|
||||
if (userSays == null)
|
||||
throw new IllegalArgumentException("You have to pass a userSays parameter");
|
||||
|
||||
System.out.print(String.format("TestTurn(%s): USER SAYS: %s \n", Thread.currentThread().getId(), userSays));
|
||||
System.out.flush();
|
||||
|
||||
try {
|
||||
this.adapter.SendTextToBot(userSays, this.callback);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
|
||||
})
|
||||
.thenApply(arg -> { // Assert Reply
|
||||
int finalTimeout = Integer.MAX_VALUE;
|
||||
if (isDebug())
|
||||
finalTimeout = Integer.MAX_VALUE;
|
||||
Function<Activity, String> validateActivity = activity -> {
|
||||
if (activity.text().equals(expected)) {
|
||||
System.out.println(String.format("TestTurn(tid:%s): Validated text is: %s", Thread.currentThread().getId(), expected));
|
||||
System.out.flush();
|
||||
|
||||
return "SUCCESS";
|
||||
}
|
||||
System.out.println(String.format("TestTurn(tid:%s): Failed validate text is: %s", Thread.currentThread().getId(), expected));
|
||||
System.out.flush();
|
||||
|
||||
return String.format("FAIL: %s received in Activity.text (%s expected)", activity.text(), expected);
|
||||
};
|
||||
|
||||
|
||||
System.out.println(String.format("TestTurn(tid:%s): Started receive loop: %s", Thread.currentThread().getId(), description));
|
||||
System.out.flush();
|
||||
DateTime start = DateTime.now();
|
||||
while (true) {
|
||||
DateTime current = DateTime.now();
|
||||
|
||||
if ((current.getMillis() - start.getMillis()) > (long) finalTimeout)
|
||||
return String.format("TestTurn: %d ms Timed out waiting for:'%s'", finalTimeout, description);
|
||||
|
||||
|
||||
Activity replyActivity = this.adapter.GetNextReply();
|
||||
|
||||
|
||||
if (replyActivity != null) {
|
||||
// if we have a reply
|
||||
System.out.println(String.format("TestTurn(tid:%s): Received Reply: %s",
|
||||
Thread.currentThread().getId(),
|
||||
String.format("\n========\n To:%s\n From:%s\n Msg:%s\n=======", replyActivity.recipient().name(), replyActivity.from().name(), replyActivity.text())
|
||||
));
|
||||
System.out.flush();
|
||||
return validateActivity.apply(replyActivity);
|
||||
} else {
|
||||
System.out.println(String.format("TestTurn(tid:%s): No reply..", Thread.currentThread().getId()));
|
||||
System.out.flush();
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
.get(timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ExecutionException e) {
|
||||
e.printStackTrace();
|
||||
} catch (TimeoutException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Say() -> shortcut for .Send(user).AssertReply(Expected)
|
||||
*
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Test(String userSays, String expected) {
|
||||
return Test(userSays, expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, String expected, String description) {
|
||||
return Test(userSays, expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, String expected, String description, int timeout) {
|
||||
if (expected == null)
|
||||
throw new IllegalArgumentException("expected");
|
||||
|
||||
return this.Send(userSays)
|
||||
.AssertReply(expected, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test() -> shortcut for .Send(user).AssertReply(Expected)
|
||||
*
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Test(String userSays, Activity expected) {
|
||||
return Test(userSays, expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Activity expected, String description) {
|
||||
return Test(userSays, expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Activity expected, String description, int timeout) {
|
||||
if (expected == null)
|
||||
throw new IllegalArgumentException("expected");
|
||||
|
||||
return this.Send(userSays)
|
||||
.AssertReply(expected, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Say() -> shortcut for .Send(user).AssertReply(Expected)
|
||||
*
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Test(String userSays, Function<Activity, String> expected) {
|
||||
return Test(userSays, expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Function<Activity, String> expected, String description) {
|
||||
return Test(userSays, expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Function<Activity, String> expected, String description, int timeout) {
|
||||
if (expected == null)
|
||||
throw new IllegalArgumentException("expected");
|
||||
|
||||
return this.Send(userSays)
|
||||
.AssertReply(expected, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that reply is one of the candidate responses
|
||||
*
|
||||
* @param candidates
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReplyOneOf(String[] candidates) {
|
||||
return AssertReplyOneOf(candidates, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReplyOneOf(String[] candidates, String description) {
|
||||
return AssertReplyOneOf(candidates, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReplyOneOf(String[] candidates, String description, int timeout) {
|
||||
if (candidates == null)
|
||||
throw new IllegalArgumentException("candidates");
|
||||
|
||||
return this.AssertReply((reply) -> {
|
||||
for (String candidate : candidates) {
|
||||
if (reply.text() == candidate)
|
||||
return null;
|
||||
}
|
||||
return String.format("%s: Not one of candidates: %s", description, String.join("\n ", candidates));
|
||||
}, description, timeout);
|
||||
}
|
||||
|
||||
}
|
||||
package com.microsoft.bot.builder.adapters;
|
||||
|
||||
import com.microsoft.bot.builder.TurnContext;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import com.microsoft.bot.schema.ActivityImpl;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
|
||||
public class TestFlow {
|
||||
final TestAdapter adapter;
|
||||
CompletableFuture<String> testTask;
|
||||
Consumer<TurnContext> callback;
|
||||
|
||||
ArrayList<Supplier<String>> tasks = new ArrayList<Supplier<String>>();
|
||||
ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory()
|
||||
{
|
||||
@Override
|
||||
public ForkJoinWorkerThread newThread(ForkJoinPool pool)
|
||||
{
|
||||
final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
|
||||
worker.setName("TestFlow-" + worker.getPoolIndex());
|
||||
return worker;
|
||||
}
|
||||
};
|
||||
|
||||
ExecutorService executor = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), factory, null, true);
|
||||
|
||||
|
||||
public TestFlow(TestAdapter adapter) {
|
||||
this(adapter, null);
|
||||
}
|
||||
|
||||
public TestFlow(TestAdapter adapter, Consumer<TurnContext> callback) {
|
||||
this.adapter = adapter;
|
||||
this.callback = callback;
|
||||
this.testTask = completedFuture(null);
|
||||
}
|
||||
|
||||
|
||||
public TestFlow(Supplier<String> testTask, TestFlow flow) {
|
||||
this.tasks = flow.tasks;
|
||||
if (testTask != null)
|
||||
this.tasks.add(testTask);
|
||||
this.callback = flow.callback;
|
||||
this.adapter = flow.adapter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the execution of the test flow
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String StartTest() throws ExecutionException, InterruptedException {
|
||||
|
||||
System.out.printf("+------------------------------------------+\n");
|
||||
int count = 0;
|
||||
for (Supplier<String> task : this.tasks) {
|
||||
System.out.printf("| Running task %s of %s\n", count++, this.tasks.size());
|
||||
String result = null;
|
||||
result = task.get();
|
||||
System.out.printf("| --> Result: %s", result);
|
||||
System.out.flush();
|
||||
}
|
||||
System.out.printf("+------------------------------------------+\n");
|
||||
return "Completed";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message from the user to the bot
|
||||
*
|
||||
* @param userSays
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Send(String userSays) throws IllegalArgumentException {
|
||||
if (userSays == null)
|
||||
throw new IllegalArgumentException("You have to pass a userSays parameter");
|
||||
|
||||
// Function<TurnContextImpl, CompletableFuture>
|
||||
return new TestFlow((() -> {
|
||||
System.out.print(String.format("USER SAYS: %s (Thread Id: %s)\n", userSays, Thread.currentThread().getId()));
|
||||
System.out.flush();
|
||||
try {
|
||||
this.adapter.SendTextToBot(userSays, this.callback);
|
||||
return "Successfully sent " + userSays;
|
||||
} catch (Exception e) {
|
||||
Assert.fail(e.getMessage());
|
||||
return e.getMessage();
|
||||
}
|
||||
}), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an activity from the user to the bot
|
||||
*
|
||||
* @param userActivity
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Send(Activity userActivity) {
|
||||
if (userActivity == null)
|
||||
throw new IllegalArgumentException("You have to pass an Activity");
|
||||
|
||||
return new TestFlow((() -> {
|
||||
System.out.printf("TestFlow(%s): Send with User Activity! %s", Thread.currentThread().getId(), userActivity.text());
|
||||
System.out.flush();
|
||||
|
||||
|
||||
try {
|
||||
this.adapter.ProcessActivity((ActivityImpl) userActivity, this.callback);
|
||||
return "TestFlow: Send() -> ProcessActivity: " + userActivity.text();
|
||||
} catch (Exception e) {
|
||||
return e.getMessage();
|
||||
|
||||
}
|
||||
|
||||
}), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay for time period
|
||||
*
|
||||
* @param ms
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Delay(int ms) {
|
||||
return new TestFlow(() ->
|
||||
{
|
||||
System.out.printf("TestFlow(%s): Delay(%s ms) called. ", Thread.currentThread().getId(), ms);
|
||||
System.out.flush();
|
||||
try {
|
||||
Thread.sleep((int) ms);
|
||||
} catch (InterruptedException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
return null;
|
||||
}, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that reply is expected text
|
||||
*
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReply(String expected) {
|
||||
return this.AssertReply(expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(String expected, String description) {
|
||||
return this.AssertReply(expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(String expected, String description, int timeout) {
|
||||
return this.AssertReply(this.adapter.MakeActivity(expected), description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the reply is expected activity
|
||||
*
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReply(Activity expected) {
|
||||
String description = Thread.currentThread().getStackTrace()[1].getMethodName();
|
||||
return AssertReply(expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(Activity expected, String description, int timeout) {
|
||||
if (description == null)
|
||||
description = Thread.currentThread().getStackTrace()[1].getMethodName();
|
||||
String finalDescription = description;
|
||||
return this.AssertReply((reply) -> {
|
||||
if (expected.type() != reply.type())
|
||||
return String.format("%s: Type should match", finalDescription);
|
||||
if (expected.text().equals(reply.text())) {
|
||||
if (finalDescription == null)
|
||||
return String.format("Expected:%s\nReceived:{reply.AsMessageActivity().Text}", expected.text());
|
||||
else
|
||||
return String.format("%s: Text should match", finalDescription);
|
||||
}
|
||||
// TODO, expand this to do all properties set on expected
|
||||
return null;
|
||||
}, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the reply matches a custom validation routine
|
||||
*
|
||||
* @param validateActivity
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReply(Function<Activity, String> validateActivity) {
|
||||
String description = Thread.currentThread().getStackTrace()[1].getMethodName();
|
||||
return AssertReply(validateActivity, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(Function<Activity, String> validateActivity, String description) {
|
||||
return AssertReply(validateActivity, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReply(Function<Activity, String> validateActivity, String description, int timeout) {
|
||||
return new TestFlow(() -> {
|
||||
System.out.println(String.format("AssertReply: Starting loop : %s (Thread:%s)", description, Thread.currentThread().getId()));
|
||||
System.out.flush();
|
||||
|
||||
int finalTimeout = Integer.MAX_VALUE;
|
||||
if (isDebug())
|
||||
finalTimeout = Integer.MAX_VALUE;
|
||||
|
||||
DateTime start = DateTime.now();
|
||||
while (true) {
|
||||
DateTime current = DateTime.now();
|
||||
|
||||
if ((current.getMillis() - start.getMillis()) > (long) finalTimeout) {
|
||||
System.out.println("AssertReply: Timeout!\n");
|
||||
System.out.flush();
|
||||
return String.format("%d ms Timed out waiting for:'%s'", finalTimeout, description);
|
||||
}
|
||||
|
||||
// System.out.println("Before GetNextReply\n");
|
||||
// System.out.flush();
|
||||
|
||||
Activity replyActivity = this.adapter.GetNextReply();
|
||||
// System.out.println("After GetNextReply\n");
|
||||
// System.out.flush();
|
||||
|
||||
if (replyActivity != null) {
|
||||
System.out.printf("AssertReply(tid:%s): Received Reply: %s ", Thread.currentThread().getId(), (replyActivity.text() == null) ? "No Text set" : replyActivity.text());
|
||||
System.out.flush();
|
||||
System.out.printf("=============\n From: %s\n To:%s\n ==========\n", (replyActivity.from() == null) ? "No from set" : replyActivity.from().name(),
|
||||
(replyActivity.recipient() == null) ? "No recipient set" : replyActivity.recipient().name());
|
||||
System.out.flush();
|
||||
|
||||
// if we have a reply
|
||||
return validateActivity.apply(replyActivity);
|
||||
} else {
|
||||
System.out.printf("AssertReply(tid:%s): Waiting..\n", Thread.currentThread().getId());
|
||||
System.out.flush();
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
// Hack to determine if debugger attached..
|
||||
public boolean isDebug() {
|
||||
for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
|
||||
if (arg.contains("jdwp=")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Turn(String userSays, String expected, String description, int timeout) {
|
||||
String result = null;
|
||||
try {
|
||||
|
||||
result = CompletableFuture.supplyAsync(() -> { // Send the message
|
||||
|
||||
if (userSays == null)
|
||||
throw new IllegalArgumentException("You have to pass a userSays parameter");
|
||||
|
||||
System.out.print(String.format("TestTurn(%s): USER SAYS: %s \n", Thread.currentThread().getId(), userSays));
|
||||
System.out.flush();
|
||||
|
||||
try {
|
||||
this.adapter.SendTextToBot(userSays, this.callback);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
|
||||
}, ExecutorFactory.getExecutor())
|
||||
.thenApply(arg -> { // Assert Reply
|
||||
int finalTimeout = Integer.MAX_VALUE;
|
||||
if (isDebug())
|
||||
finalTimeout = Integer.MAX_VALUE;
|
||||
Function<Activity, String> validateActivity = activity -> {
|
||||
if (activity.text().equals(expected)) {
|
||||
System.out.println(String.format("TestTurn(tid:%s): Validated text is: %s", Thread.currentThread().getId(), expected));
|
||||
System.out.flush();
|
||||
|
||||
return "SUCCESS";
|
||||
}
|
||||
System.out.println(String.format("TestTurn(tid:%s): Failed validate text is: %s", Thread.currentThread().getId(), expected));
|
||||
System.out.flush();
|
||||
|
||||
return String.format("FAIL: %s received in Activity.text (%s expected)", activity.text(), expected);
|
||||
};
|
||||
|
||||
|
||||
System.out.println(String.format("TestTurn(tid:%s): Started receive loop: %s", Thread.currentThread().getId(), description));
|
||||
System.out.flush();
|
||||
DateTime start = DateTime.now();
|
||||
while (true) {
|
||||
DateTime current = DateTime.now();
|
||||
|
||||
if ((current.getMillis() - start.getMillis()) > (long) finalTimeout)
|
||||
return String.format("TestTurn: %d ms Timed out waiting for:'%s'", finalTimeout, description);
|
||||
|
||||
|
||||
Activity replyActivity = this.adapter.GetNextReply();
|
||||
|
||||
|
||||
if (replyActivity != null) {
|
||||
// if we have a reply
|
||||
System.out.println(String.format("TestTurn(tid:%s): Received Reply: %s",
|
||||
Thread.currentThread().getId(),
|
||||
String.format("\n========\n To:%s\n From:%s\n Msg:%s\n=======", replyActivity.recipient().name(), replyActivity.from().name(), replyActivity.text())
|
||||
));
|
||||
System.out.flush();
|
||||
return validateActivity.apply(replyActivity);
|
||||
} else {
|
||||
System.out.println(String.format("TestTurn(tid:%s): No reply..", Thread.currentThread().getId()));
|
||||
System.out.flush();
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
.get(timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ExecutionException e) {
|
||||
e.printStackTrace();
|
||||
} catch (TimeoutException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Say() -> shortcut for .Send(user).AssertReply(Expected)
|
||||
*
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Test(String userSays, String expected) {
|
||||
return Test(userSays, expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, String expected, String description) {
|
||||
return Test(userSays, expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, String expected, String description, int timeout) {
|
||||
if (expected == null)
|
||||
throw new IllegalArgumentException("expected");
|
||||
|
||||
return this.Send(userSays)
|
||||
.AssertReply(expected, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test() -> shortcut for .Send(user).AssertReply(Expected)
|
||||
*
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Test(String userSays, Activity expected) {
|
||||
return Test(userSays, expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Activity expected, String description) {
|
||||
return Test(userSays, expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Activity expected, String description, int timeout) {
|
||||
if (expected == null)
|
||||
throw new IllegalArgumentException("expected");
|
||||
|
||||
return this.Send(userSays)
|
||||
.AssertReply(expected, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Say() -> shortcut for .Send(user).AssertReply(Expected)
|
||||
*
|
||||
* @param userSays
|
||||
* @param expected
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow Test(String userSays, Function<Activity, String> expected) {
|
||||
return Test(userSays, expected, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Function<Activity, String> expected, String description) {
|
||||
return Test(userSays, expected, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow Test(String userSays, Function<Activity, String> expected, String description, int timeout) {
|
||||
if (expected == null)
|
||||
throw new IllegalArgumentException("expected");
|
||||
|
||||
return this.Send(userSays)
|
||||
.AssertReply(expected, description, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that reply is one of the candidate responses
|
||||
*
|
||||
* @param candidates
|
||||
* @param description
|
||||
* @param timeout
|
||||
* @return
|
||||
*/
|
||||
public TestFlow AssertReplyOneOf(String[] candidates) {
|
||||
return AssertReplyOneOf(candidates, null, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReplyOneOf(String[] candidates, String description) {
|
||||
return AssertReplyOneOf(candidates, description, 3000);
|
||||
}
|
||||
|
||||
public TestFlow AssertReplyOneOf(String[] candidates, String description, int timeout) {
|
||||
if (candidates == null)
|
||||
throw new IllegalArgumentException("candidates");
|
||||
|
||||
return this.AssertReply((reply) -> {
|
||||
for (String candidate : candidates) {
|
||||
if (reply.text() == candidate)
|
||||
return null;
|
||||
}
|
||||
return String.format("%s: Not one of candidates: %s", description, String.join("\n ", candidates));
|
||||
}, description, timeout);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -86,6 +86,10 @@
|
|||
<groupId>com.auth0</groupId>
|
||||
<artifactId>jwks-rsa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.bot</groupId>
|
||||
|
|
|
@ -2,22 +2,18 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for
|
||||
* license information.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
*/
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.schema.models.AttachmentInfo;
|
||||
import com.microsoft.bot.connector.models.ErrorResponseException;
|
||||
import com.microsoft.rest.ServiceCallback;
|
||||
import com.microsoft.rest.ServiceFuture;
|
||||
import com.microsoft.rest.ServiceResponse;
|
||||
import java.io.InputStream;
|
||||
import rx.Observable;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* An instance of this class provides access to all the operations defined
|
||||
* in Attachments.
|
||||
|
@ -28,10 +24,9 @@ public interface Attachments {
|
|||
* Get AttachmentInfo structure describing the attachment views.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the AttachmentInfo object if successful.
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
*/
|
||||
AttachmentInfo getAttachmentInfo(String attachmentId);
|
||||
|
||||
|
@ -39,10 +34,10 @@ public interface Attachments {
|
|||
* GetAttachmentInfo.
|
||||
* Get AttachmentInfo structure describing the attachment views.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @param attachmentId attachment id
|
||||
* @param serviceCallback the async ServiceCallback to handle successful and failed responses.
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @return the {@link ServiceFuture} object
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
*/
|
||||
ServiceFuture<AttachmentInfo> getAttachmentInfoAsync(String attachmentId, final ServiceCallback<AttachmentInfo> serviceCallback);
|
||||
|
||||
|
@ -51,8 +46,8 @@ public interface Attachments {
|
|||
* Get AttachmentInfo structure describing the attachment views.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @return the observable to the AttachmentInfo object
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
*/
|
||||
Observable<AttachmentInfo> getAttachmentInfoAsync(String attachmentId);
|
||||
|
||||
|
@ -61,8 +56,8 @@ public interface Attachments {
|
|||
* Get AttachmentInfo structure describing the attachment views.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @return the observable to the AttachmentInfo object
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
*/
|
||||
Observable<ServiceResponse<AttachmentInfo>> getAttachmentInfoWithServiceResponseAsync(String attachmentId);
|
||||
|
||||
|
@ -71,11 +66,10 @@ public interface Attachments {
|
|||
* Get the named view as binary content.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @return the InputStream object if successful.
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
*/
|
||||
InputStream getAttachment(String attachmentId, String viewId);
|
||||
|
||||
|
@ -83,11 +77,11 @@ public interface Attachments {
|
|||
* GetAttachment.
|
||||
* Get the named view as binary content.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @param attachmentId attachment id
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @param serviceCallback the async ServiceCallback to handle successful and failed responses.
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @return the {@link ServiceFuture} object
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
*/
|
||||
ServiceFuture<InputStream> getAttachmentAsync(String attachmentId, String viewId, final ServiceCallback<InputStream> serviceCallback);
|
||||
|
||||
|
@ -96,9 +90,9 @@ public interface Attachments {
|
|||
* Get the named view as binary content.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @return the observable to the InputStream object
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
*/
|
||||
Observable<InputStream> getAttachmentAsync(String attachmentId, String viewId);
|
||||
|
||||
|
@ -107,9 +101,9 @@ public interface Attachments {
|
|||
* Get the named view as binary content.
|
||||
*
|
||||
* @param attachmentId attachment id
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @param viewId View id from attachmentInfo
|
||||
* @return the observable to the InputStream object
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
*/
|
||||
Observable<ServiceResponse<InputStream>> getAttachmentWithServiceResponseAsync(String attachmentId, String viewId);
|
||||
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
public class Channels {
|
||||
/**
|
||||
* Console channel.
|
||||
*/
|
||||
public static final String CONSOLE = "console";
|
||||
|
||||
/**
|
||||
* Cortana channel.
|
||||
*/
|
||||
public static final String CORTANA = "cortana";
|
||||
|
||||
/**
|
||||
* Direct Line channel.
|
||||
*/
|
||||
public static final String DIRECTLINE = "directline";
|
||||
|
||||
/**
|
||||
* Email channel.
|
||||
*/
|
||||
public static final String EMAIL = "email";
|
||||
|
||||
/**
|
||||
* Emulator channel.
|
||||
*/
|
||||
public static final String EMULATOR = "emulator";
|
||||
|
||||
/**
|
||||
* Facebook channel.
|
||||
*/
|
||||
public static final String FACEBOOK = "facebook";
|
||||
|
||||
/**
|
||||
* Group Me channel.
|
||||
*/
|
||||
public static final String GROUPME = "groupme";
|
||||
|
||||
/**
|
||||
* Kik channel.
|
||||
*/
|
||||
public static final String KIK = "kik";
|
||||
|
||||
/**
|
||||
* Line channel.
|
||||
*/
|
||||
public static final String LINE = "line";
|
||||
|
||||
/**
|
||||
* MS Teams channel.
|
||||
*/
|
||||
public static final String MSTEAMS = "msteams";
|
||||
|
||||
/**
|
||||
* Skype channel.
|
||||
*/
|
||||
public static final String SKYPE = "skype";
|
||||
|
||||
/**
|
||||
* Skype for Business channel.
|
||||
*/
|
||||
public static final String SKYPEFORBUSINESS = "skypeforbusiness";
|
||||
|
||||
/**
|
||||
* Slack channel.
|
||||
*/
|
||||
public static final String SLACK = "slack";
|
||||
|
||||
/**
|
||||
* SMS (Twilio) channel.
|
||||
*/
|
||||
public static final String SMS = "sms";
|
||||
|
||||
/**
|
||||
* Telegram channel.
|
||||
*/
|
||||
public static final String TELEGRAM = "telegram";
|
||||
|
||||
/**
|
||||
* WebChat channel.
|
||||
*/
|
||||
public static final String WEBCHAT = "webchat";
|
||||
|
||||
/**
|
||||
* Test channel.
|
||||
*/
|
||||
public static final String TEST = "test";
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for
|
||||
* license information.
|
||||
*
|
||||
* <p>
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
|
@ -21,7 +21,7 @@ public interface ConnectorClient {
|
|||
* Gets the REST client.
|
||||
*
|
||||
* @return the {@link RestClient} object.
|
||||
*/
|
||||
*/
|
||||
RestClient restClient();
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.rest.RestClient;
|
||||
import com.microsoft.rest.credentials.ServiceClientCredentials;
|
||||
|
||||
public class ConnectorClientFuture extends ConnectorClientImpl {
|
||||
|
||||
/**
|
||||
* Initializes an instance of ConnectorClient client.
|
||||
*
|
||||
* @param credentials the management credentials for Azure
|
||||
*/
|
||||
public ConnectorClientFuture(ServiceClientCredentials credentials) {
|
||||
super(credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an instance of ConnectorClient client.
|
||||
*
|
||||
* @param baseUrl the base URL of the host
|
||||
* @param credentials the management credentials for Azure
|
||||
*/
|
||||
public ConnectorClientFuture(String baseUrl, ServiceClientCredentials credentials) {
|
||||
super(baseUrl, credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an instance of ConnectorClient client.
|
||||
*
|
||||
* @param restClient the REST client to connect to Azure.
|
||||
*/
|
||||
public ConnectorClientFuture(RestClient restClient) {
|
||||
super(restClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String userAgent() {
|
||||
return "Microsoft-BotFramework/4.0";
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for
|
||||
* license information.
|
||||
*
|
||||
* <p>
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
|
@ -10,22 +10,14 @@
|
|||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import com.microsoft.bot.schema.models.AttachmentData;
|
||||
import com.microsoft.bot.schema.models.ChannelAccount;
|
||||
import com.microsoft.bot.schema.models.ConversationParameters;
|
||||
import com.microsoft.bot.schema.models.ConversationResourceResponse;
|
||||
import com.microsoft.bot.schema.models.ConversationsResult;
|
||||
import com.microsoft.bot.schema.models.PagedMembersResult;
|
||||
import com.microsoft.bot.connector.models.ErrorResponseException;
|
||||
import com.microsoft.bot.schema.models.ResourceResponse;
|
||||
import com.microsoft.bot.schema.models.Transcript;
|
||||
import com.microsoft.bot.schema.models.*;
|
||||
import com.microsoft.rest.ServiceCallback;
|
||||
import com.microsoft.rest.ServiceFuture;
|
||||
import com.microsoft.rest.ServiceResponse;
|
||||
import java.util.List;
|
||||
import rx.Observable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An instance of this class provides access to all the operations defined
|
||||
* in Conversations.
|
||||
|
@ -40,7 +32,6 @@ public interface Conversations {
|
|||
Each ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation.
|
||||
*
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ConversationsResult object if successful.
|
||||
*/
|
||||
|
@ -85,6 +76,7 @@ public interface Conversations {
|
|||
* @return the observable to the ConversationsResult object
|
||||
*/
|
||||
Observable<ServiceResponse<ConversationsResult>> getConversationsWithServiceResponseAsync();
|
||||
|
||||
/**
|
||||
* GetConversations.
|
||||
* List the Conversations in which this bot has participated.
|
||||
|
@ -95,7 +87,6 @@ public interface Conversations {
|
|||
*
|
||||
* @param continuationToken skip or continuation token
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ConversationsResult object if successful.
|
||||
*/
|
||||
|
@ -161,7 +152,6 @@ public interface Conversations {
|
|||
*
|
||||
* @param parameters Parameters to create the conversation from
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ConversationResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -243,7 +233,6 @@ public interface Conversations {
|
|||
* @param conversationId Conversation ID
|
||||
* @param activity Activity to send
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -308,7 +297,6 @@ public interface Conversations {
|
|||
* @param activityId activityId to update
|
||||
* @param activity replacement Activity
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -370,7 +358,6 @@ public interface Conversations {
|
|||
* @param activityId activityId the reply is to (OPTIONAL)
|
||||
* @param activity Activity to send
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -436,7 +423,6 @@ public interface Conversations {
|
|||
* @param conversationId Conversation ID
|
||||
* @param activityId activityId to delete
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
*/
|
||||
void deleteActivity(String conversationId, String activityId);
|
||||
|
@ -485,7 +471,6 @@ public interface Conversations {
|
|||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the List<ChannelAccount> object if successful.
|
||||
*/
|
||||
|
@ -534,7 +519,6 @@ public interface Conversations {
|
|||
* @param conversationId Conversation ID
|
||||
* @param memberId ID of the member to delete from this conversation
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
*/
|
||||
void deleteConversationMember(String conversationId, String memberId);
|
||||
|
@ -587,7 +571,6 @@ public interface Conversations {
|
|||
* @param conversationId Conversation ID
|
||||
* @param activityId Activity ID
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the List<ChannelAccount> object if successful.
|
||||
*/
|
||||
|
@ -639,7 +622,6 @@ public interface Conversations {
|
|||
* @param conversationId Conversation ID
|
||||
* @param attachmentUpload Attachment data
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -688,15 +670,14 @@ public interface Conversations {
|
|||
|
||||
/**
|
||||
* This method allows you to upload the historic activities to the conversation.
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
* the client to render the activities in the right order.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @param history Historic activities
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -704,16 +685,15 @@ public interface Conversations {
|
|||
|
||||
/**
|
||||
* This method allows you to upload the historic activities to the conversation.
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
* the client to render the activities in the right order.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @param history Historic activities
|
||||
* @param serviceCallback the async ServiceCallback to handle successful and failed responses.
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -721,15 +701,14 @@ public interface Conversations {
|
|||
|
||||
/**
|
||||
* This method allows you to upload the historic activities to the conversation.
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
* the client to render the activities in the right order.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @param history Historic activities
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
|
@ -737,38 +716,36 @@ public interface Conversations {
|
|||
|
||||
/**
|
||||
* This method allows you to upload the historic activities to the conversation.
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
*
|
||||
* Sender must ensure that the historic activities have unique ids and appropriate timestamps.
|
||||
* The ids are used by the client to deal with duplicate activities and the timestamps are used by
|
||||
* the client to render the activities in the right order.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @param history Historic activities
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the ResourceResponse object if successful.
|
||||
*/
|
||||
Observable<ServiceResponse<ResourceResponse>> sendConversationHistoryWithServiceResponseAsync(String conversationId, Transcript history);
|
||||
Observable<ServiceResponse<ResourceResponse>> sendConversationHistoryWithServiceResponseAsync(String conversationId, Transcript history);
|
||||
|
||||
/**
|
||||
* Enumerate the members of a conversation one page at a time.
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
* of the conversation and a continuation token that can be used to get more values.
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
* conversation the Members will be empty or not present in the response.
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
* from a previous request.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the PagedMembersResult object if successful.
|
||||
*/
|
||||
|
@ -776,23 +753,22 @@ public interface Conversations {
|
|||
|
||||
/**
|
||||
* Enumerate the members of a conversation one page at a time.
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
* of the conversation and a continuation token that can be used to get more values.
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
* conversation the Members will be empty or not present in the response.
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
* from a previous request.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @param serviceCallback the async ServiceCallback to handle successful and failed responses.
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the PagedMembersResult object if successful.
|
||||
*/
|
||||
|
@ -800,22 +776,21 @@ public interface Conversations {
|
|||
|
||||
/**
|
||||
* Enumerate the members of a conversation one page at a time.
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
* of the conversation and a continuation token that can be used to get more values.
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
* conversation the Members will be empty or not present in the response.
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
* from a previous request.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the PagedMembersResult object if successful.
|
||||
*/
|
||||
|
@ -823,25 +798,24 @@ public interface Conversations {
|
|||
|
||||
/**
|
||||
* Enumerate the members of a conversation one page at a time.
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
*
|
||||
* This REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided.
|
||||
* It returns a PagedMembersResult, which contains an array of ChannelAccounts representing the members
|
||||
* of the conversation and a continuation token that can be used to get more values.
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
*
|
||||
* One page of ChannelAccounts records are returned with each call. The number of records in a page may
|
||||
* vary between channels and calls. The pageSize parameter can be used as a suggestion. If there are no
|
||||
* additional results the response will not contain a continuation token. If there are no members in the
|
||||
* conversation the Members will be empty or not present in the response.
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
*
|
||||
* A response to a request that has a continuation token from a prior request may rarely return members
|
||||
* from a previous request.
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @throws IllegalArgumentException thrown if parameters fail the validation
|
||||
* @throws ErrorResponseException thrown if the request is rejected by server
|
||||
* @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent
|
||||
* @return the observable to the ResourceResponse object
|
||||
*/
|
||||
Observable<ServiceResponse<PagedMembersResult>> getConversationPagedMembersWithServiceResponseAsync(String conversationId);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for
|
||||
// license information.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
|
||||
/**
|
||||
* Provides a common Executor for Future operations.
|
||||
*/
|
||||
public class ExecutorFactory {
|
||||
private static ForkJoinWorkerThreadFactory factory = new ForkJoinWorkerThreadFactory() {
|
||||
@Override
|
||||
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
|
||||
ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
|
||||
worker.setName("Bot-" + worker.getPoolIndex());
|
||||
return worker;
|
||||
}
|
||||
};
|
||||
|
||||
private static ExecutorService executor = new ForkJoinPool(
|
||||
Runtime.getRuntime().availableProcessors() * 2,
|
||||
factory,
|
||||
null,
|
||||
false);
|
||||
|
||||
public static ExecutorService getExecutor() {
|
||||
return executor;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -23,7 +23,7 @@ public class UserAgent {
|
|||
String build_version;
|
||||
final Properties properties = new Properties();
|
||||
try {
|
||||
InputStream propStream = ConnectorClientImpl.class.getClassLoader().getResourceAsStream("project.properties");
|
||||
InputStream propStream = RestConnectorClient.class.getClassLoader().getResourceAsStream("project.properties");
|
||||
properties.load(propStream);
|
||||
build_version = properties.getProperty("version");
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for
|
||||
// license information.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationContext;
|
||||
import com.microsoft.aad.adal4j.AuthenticationResult;
|
||||
import com.microsoft.aad.adal4j.ClientCredential;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class AdalAuthenticator {
|
||||
private AuthenticationContext context;
|
||||
private OAuthConfiguration oAuthConfiguration;
|
||||
private ClientCredential clientCredential;
|
||||
|
||||
public AdalAuthenticator(ClientCredential clientCredential, OAuthConfiguration configurationOAuth) throws MalformedURLException {
|
||||
this.oAuthConfiguration = configurationOAuth;
|
||||
this.clientCredential = clientCredential;
|
||||
this.context = new AuthenticationContext(configurationOAuth.authority(), false, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
public Future<AuthenticationResult> acquireToken() {
|
||||
return context.acquireToken(oAuthConfiguration.scope(), clientCredential, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* General configuration settings for authentication.
|
||||
*/
|
||||
public class AuthenticationConfiguration {
|
||||
public List<String> requiredEndorsements() {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
}
|
|
@ -7,23 +7,127 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
public final class AuthenticationConstants {
|
||||
public static final String ToChannelFromBotLoginUrl = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token";
|
||||
public static final String ToChannelFromBotOAuthScope = "https://api.botframework.com/.default";
|
||||
public static final String ToBotFromChannelTokenIssuer = "https://api.botframework.com";
|
||||
public static final String ToBotFromChannelOpenIdMetadataUrl = "https://login.botframework.com/v1/.well-known/openidconfiguration";
|
||||
public static final String ToBotFromEmulatorOpenIdMetadataUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
|
||||
public static final List<String> AllowedSigningAlgorithms = new ArrayList<>();
|
||||
public static final String AuthorizedParty = "azp";
|
||||
public static final String AudienceClaim = "aud";
|
||||
public static final String ServiceUrlClaim = "serviceurl";
|
||||
public static final String VersionClaim = "ver";
|
||||
public static final String AppIdClaim = "appid";
|
||||
/**
|
||||
* OAuth Url used to get a token from OAuthApiClient
|
||||
* TO CHANNEL FROM BOT: Login URL.
|
||||
*/
|
||||
public static final String OAuthUrl = "https://api.botframework.com";
|
||||
@Deprecated
|
||||
public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL = "https://login.microsoftonline.com/botframework.com";
|
||||
|
||||
/**
|
||||
* TO CHANNEL FROM BOT: Login URL template string. Bot developer may specify
|
||||
* which tenant to obtain an access token from. By default, the channels only
|
||||
* accept tokens from "botframework.com". For more details see https://aka.ms/bots/tenant-restriction.
|
||||
*/
|
||||
public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE = "https://login.microsoftonline.com/%s";
|
||||
|
||||
/**
|
||||
* TO CHANNEL FROM BOT: OAuth scope to request.
|
||||
*/
|
||||
public static final String TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = "https://api.botframework.com";
|
||||
|
||||
/**
|
||||
* TO BOT FROM CHANNEL: Token issuer.
|
||||
*/
|
||||
public static final String TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com";
|
||||
|
||||
/**
|
||||
* TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA.
|
||||
*/
|
||||
public static final String TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = "https://login.botframework.com/v1/.well-known/openidconfiguration";
|
||||
|
||||
/**
|
||||
* TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA.
|
||||
*/
|
||||
public static final String TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
|
||||
|
||||
/**
|
||||
* TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA.
|
||||
*/
|
||||
public static final String TO_BOT_FROM_ENTERPRISE_CHANNEL_OPENID_METADATA_URL_FORMAT = "https://%s.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration";
|
||||
|
||||
/**
|
||||
* Allowed token signing algorithms. Tokens come from channels to the bot. The code
|
||||
* that uses this also supports tokens coming from the emulator.
|
||||
*/
|
||||
public static final List<String> AllowedSigningAlgorithms = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Application Setting Key for the OAuthUrl value.
|
||||
*/
|
||||
public static final String OAUTH_URL_KEY = "OAuthApiEndpoint";
|
||||
|
||||
/**
|
||||
* OAuth Url used to get a token from OAuthApiClient.
|
||||
*/
|
||||
public static final String OAUTH_URL = "https://api.botframework.com";
|
||||
|
||||
/**
|
||||
* Application Settings Key for whether to emulate OAuthCards when using the emulator.
|
||||
*/
|
||||
public static final String EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards";
|
||||
|
||||
/**
|
||||
* Application Setting Key for the OpenIdMetadataUrl value.
|
||||
*/
|
||||
public static final String BOT_OPENID_METADATA_KEY = "BotOpenIdMetadata";
|
||||
|
||||
/**
|
||||
* The default tenant to acquire bot to channel token from.
|
||||
*/
|
||||
public static final String DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com";
|
||||
|
||||
/**
|
||||
* "azp" Claim.
|
||||
* Authorized party - the party to which the ID Token was issued.
|
||||
* This claim follows the general format set forth in the OpenID Spec.
|
||||
* http://openid.net/specs/openid-connect-core-1_0.html#IDToken.
|
||||
*/
|
||||
public static final String AUTHORIZED_PARTY = "azp";
|
||||
|
||||
/**
|
||||
* Audience Claim. From RFC 7519.
|
||||
* https://tools.ietf.org/html/rfc7519#section-4.1.3
|
||||
* The "aud" (audience) claim identifies the recipients that the JWT is
|
||||
* intended for. Each principal intended to process the JWT MUST
|
||||
* identify itself with a value in the audience claim. If the principal
|
||||
* processing the claim does not identify itself with a value in the
|
||||
* "aud" claim when this claim is present, then the JWT MUST be
|
||||
* rejected. In the general case, the "aud" value is an array of case-
|
||||
* sensitive strings, each containing a StringOrURI value. In the
|
||||
* special case when the JWT has one audience, the "aud" value MAY be a
|
||||
* single case-sensitive string containing a StringOrURI value. The
|
||||
* interpretation of audience values is generally application specific.
|
||||
* Use of this claim is OPTIONAL.
|
||||
*/
|
||||
public static final String AUDIENCE_CLAIM = "aud";
|
||||
|
||||
/**
|
||||
* From RFC 7515
|
||||
* https://tools.ietf.org/html/rfc7515#section-4.1.4
|
||||
* The "kid" (key ID) Header Parameter is a hint indicating which key
|
||||
* was used to secure the JWS. This parameter allows originators to
|
||||
* explicitly signal a change of key to recipients. The structure of
|
||||
* the "kid" value is unspecified. Its value MUST be a case-sensitive
|
||||
* string. Use of this Header Parameter is OPTIONAL.
|
||||
* When used with a JWK, the "kid" value is used to match a JWK "kid"
|
||||
* parameter value.
|
||||
*/
|
||||
public static final String KEY_ID_HEADER = "kid";
|
||||
|
||||
/**
|
||||
* Service URL claim name. As used in Microsoft Bot Framework v3.1 auth.
|
||||
*/
|
||||
public static final String SERVICE_URL_CLAIM = "serviceurl";
|
||||
|
||||
/**
|
||||
* Token version claim name. As used in Microsoft AAD tokens.
|
||||
*/
|
||||
public static final String VERSION_CLAIM = "ver";
|
||||
|
||||
/**
|
||||
* App ID claim name. As used in Microsoft AAD 1.0 tokens.
|
||||
*/
|
||||
public static final String APPID_CLAIM = "appid";
|
||||
|
||||
static {
|
||||
AllowedSigningAlgorithms.add("RS256");
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
public class BotCredentials {
|
||||
protected String appId;
|
||||
protected String appPassword;
|
||||
|
||||
public String appId() { return this.appId; }
|
||||
|
||||
public String appPassword() { return this.appPassword; }
|
||||
|
||||
public BotCredentials withAppId(String appId) {
|
||||
this.appId = appId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BotCredentials withAppPassword(String appPassword) {
|
||||
this.appPassword = appPassword;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* ChannelProvider interface. This interface allows Bots to provide their own
|
||||
* implementation for the configuration parameters to connect to a Bot.
|
||||
* Framework channel service.
|
||||
*/
|
||||
public interface ChannelProvider {
|
||||
/**
|
||||
* Gets the channel service property for this channel provider.
|
||||
*
|
||||
* @return The channel service property for the channel provider.
|
||||
*/
|
||||
CompletableFuture<String> getChannelService();
|
||||
|
||||
/**
|
||||
* Gets a value of whether this provider represents a channel on Government Azure.
|
||||
*
|
||||
* @return True if this channel provider represents a channel on Government Azure.
|
||||
*/
|
||||
boolean isGovernment();
|
||||
|
||||
/**
|
||||
* Gets a value of whether this provider represents a channel on Public Azure.
|
||||
*
|
||||
* @return True if this channel provider represents a channel on Public Azure.
|
||||
*/
|
||||
boolean isPublicAzure();
|
||||
}
|
|
@ -4,88 +4,141 @@
|
|||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class ChannelValidation {
|
||||
/**
|
||||
* TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot
|
||||
*/
|
||||
public static final TokenValidationParameters ToBotFromChannelTokenValidationParameters = TokenValidationParameters.toBotFromChannelTokenValidationParameters();
|
||||
public static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{
|
||||
this.validateIssuer = true;
|
||||
this.validIssuers = new ArrayList<String>() {{
|
||||
add(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER);
|
||||
}};
|
||||
this.validateAudience = false;
|
||||
this.validateLifetime = true;
|
||||
this.clockSkew = Duration.ofMinutes(5);
|
||||
this.requireSignedTokens = true;
|
||||
}};
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from the Bot Framework Service.
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]"
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelId ChannelId for endorsements validation.
|
||||
* @param channelId ChannelId for endorsements validation.
|
||||
* @return A valid ClaimsIdentity.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String channelId) throws ExecutionException, InterruptedException, AuthenticationException {
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String channelId) {
|
||||
return authenticateToken(authHeader, credentials, channelId, new AuthenticationConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from the Bot Framework Service.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelId ChannelId for endorsements validation.
|
||||
* @param authConfig The AuthenticationConfiguration.
|
||||
* @return A valid ClaimsIdentity.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String channelId, AuthenticationConfiguration authConfig) {
|
||||
JwtTokenExtractor tokenExtractor = new JwtTokenExtractor(
|
||||
ToBotFromChannelTokenValidationParameters,
|
||||
AuthenticationConstants.ToBotFromChannelOpenIdMetadataUrl,
|
||||
TOKENVALIDATIONPARAMETERS,
|
||||
AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL,
|
||||
AuthenticationConstants.AllowedSigningAlgorithms);
|
||||
|
||||
ClaimsIdentity identity = tokenExtractor.getIdentityAsync(authHeader, channelId).get();
|
||||
if (identity == null) {
|
||||
// No valid identity. Not Authorized.
|
||||
throw new AuthenticationException("Invalid Identity");
|
||||
}
|
||||
return tokenExtractor.getIdentityAsync(authHeader, channelId)
|
||||
.thenApply(identity -> {
|
||||
if (identity == null) {
|
||||
// No valid identity. Not Authorized.
|
||||
throw new AuthenticationException("Invalid Identity");
|
||||
}
|
||||
|
||||
if (!identity.isAuthenticated()) {
|
||||
// The token is in some way invalid. Not Authorized.
|
||||
throw new AuthenticationException("Token Not Authenticated");
|
||||
}
|
||||
if (!identity.isAuthenticated()) {
|
||||
// The token is in some way invalid. Not Authorized.
|
||||
throw new AuthenticationException("Token Not Authenticated");
|
||||
}
|
||||
|
||||
// Now check that the AppID in the claims set matches
|
||||
// what we're looking for. Note that in a multi-tenant bot, this value
|
||||
// comes from developer code that may be reaching out to a service, hence the
|
||||
// Async validation.
|
||||
// Now check that the AppID in the claims set matches
|
||||
// what we're looking for. Note that in a multi-tenant bot, this value
|
||||
// comes from developer code that may be reaching out to a service, hence the
|
||||
// Async validation.
|
||||
|
||||
// Look for the "aud" claim, but only if issued from the Bot Framework
|
||||
if (!identity.getIssuer().equalsIgnoreCase(AuthenticationConstants.ToBotFromChannelTokenIssuer)) {
|
||||
throw new AuthenticationException("Token Not Authenticated");
|
||||
}
|
||||
// Look for the "aud" claim, but only if issued from the Bot Framework
|
||||
if (!identity.getIssuer().equalsIgnoreCase(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER)) {
|
||||
throw new AuthenticationException("Wrong Issuer");
|
||||
}
|
||||
|
||||
// The AppId from the claim in the token must match the AppId specified by the developer. Note that
|
||||
// the Bot Framework uses the Audience claim ("aud") to pass the AppID.
|
||||
String appIdFromClaim = identity.claims().get(AuthenticationConstants.AudienceClaim);
|
||||
if (appIdFromClaim == null || appIdFromClaim.isEmpty()) {
|
||||
// Claim is present, but doesn't have a value. Not Authorized.
|
||||
throw new AuthenticationException("Token Not Authenticated");
|
||||
}
|
||||
// The AppId from the claim in the token must match the AppId specified by the developer. Note that
|
||||
// the Bot Framework uses the Audience claim ("aud") to pass the AppID.
|
||||
String appIdFromAudienceClaim = identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM);
|
||||
if (StringUtils.isEmpty(appIdFromAudienceClaim)) {
|
||||
// Claim is present, but doesn't have a value. Not Authorized.
|
||||
throw new AuthenticationException("No Audience Claim");
|
||||
}
|
||||
|
||||
if (!credentials.isValidAppIdAsync(appIdFromClaim).get()) {
|
||||
throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromClaim));
|
||||
}
|
||||
if (!credentials.isValidAppIdAsync(appIdFromAudienceClaim).join()) {
|
||||
throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim));
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(identity);
|
||||
return identity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from the Bot Framework Service.
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]"
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]"
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelId ChannelId for endorsements validation.
|
||||
* @param serviceUrl Service url.
|
||||
* @param channelId ChannelId for endorsements validation.
|
||||
* @param serviceUrl Service url.
|
||||
* @return A valid ClaimsIdentity.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader,CredentialProvider credentials, String channelId, String serviceUrl) throws ExecutionException, InterruptedException, AuthenticationException {
|
||||
ClaimsIdentity identity = ChannelValidation.authenticateToken(authHeader, credentials, channelId).get();
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String channelId, String serviceUrl) {
|
||||
return authenticateToken(authHeader, credentials, channelId, serviceUrl, new AuthenticationConfiguration());
|
||||
}
|
||||
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.ServiceUrlClaim)) {
|
||||
// Claim must be present. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Channel Token.", AuthenticationConstants.ServiceUrlClaim));
|
||||
}
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from the Bot Framework Service.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]"
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelId ChannelId for endorsements validation.
|
||||
* @param serviceUrl Service url.
|
||||
* @param authConfig The authentication configuration.
|
||||
* @return A valid ClaimsIdentity.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework emulator will FAIL this check.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String channelId, String serviceUrl, AuthenticationConfiguration authConfig) {
|
||||
return ChannelValidation.authenticateToken(authHeader, credentials, channelId, authConfig)
|
||||
.thenApply(identity -> {
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.SERVICE_URL_CLAIM)) {
|
||||
// Claim must be present. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Channel Token.", AuthenticationConstants.SERVICE_URL_CLAIM));
|
||||
}
|
||||
|
||||
if (!serviceUrl.equalsIgnoreCase(identity.claims().get(AuthenticationConstants.ServiceUrlClaim))) {
|
||||
// Claim must match. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim does not match service url provided (%s).", AuthenticationConstants.ServiceUrlClaim, serviceUrl));
|
||||
}
|
||||
if (!serviceUrl.equalsIgnoreCase(identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM))) {
|
||||
// Claim must match. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim does not match service url provided (%s).", AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl));
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(identity);
|
||||
return identity;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,45 @@
|
|||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ClaimsIdentity {
|
||||
boolean isAuthenticated();
|
||||
Map<String, String> claims();
|
||||
String getIssuer();
|
||||
public class ClaimsIdentity {
|
||||
private String issuer;
|
||||
private Map<String, String> claims;
|
||||
|
||||
private ClaimsIdentity() {
|
||||
this("", new HashMap<>());
|
||||
}
|
||||
|
||||
public ClaimsIdentity(String authIssuer) {
|
||||
this(authIssuer, new HashMap<>());
|
||||
}
|
||||
|
||||
public ClaimsIdentity(String authIssuer, Map<String, String> claims) {
|
||||
this.issuer = authIssuer;
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
public ClaimsIdentity(DecodedJWT jwt) {
|
||||
claims = new HashMap<>();
|
||||
if (jwt.getClaims() != null) {
|
||||
jwt.getClaims().forEach((k, v) -> claims.put(k, v.asString()));
|
||||
}
|
||||
issuer = jwt.getIssuer();
|
||||
}
|
||||
|
||||
public boolean isAuthenticated() {
|
||||
return this.issuer != null && !this.issuer.isEmpty();
|
||||
}
|
||||
|
||||
public Map<String, String> claims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ClaimsIdentityImpl implements ClaimsIdentity {
|
||||
private String issuer;
|
||||
private Map<String, String> claims;
|
||||
|
||||
public ClaimsIdentityImpl() {
|
||||
this("", new HashMap<>());
|
||||
}
|
||||
|
||||
public ClaimsIdentityImpl(String authType) {
|
||||
this(authType, new HashMap<>());
|
||||
}
|
||||
|
||||
public ClaimsIdentityImpl(String authType, Map<String, String> claims) {
|
||||
this.issuer = authType;
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthenticated() {
|
||||
return this.issuer != null && !this.issuer.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> claims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class CredentialProviderImpl extends BotCredentials implements CredentialProvider {
|
||||
|
||||
public CredentialProviderImpl(String appId, String appPassword) {
|
||||
this.appId = appId;
|
||||
this.appPassword = appPassword;
|
||||
}
|
||||
|
||||
public CredentialProviderImpl(BotCredentials credentials) {
|
||||
this(credentials.appId, credentials.appPassword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialProviderImpl withAppId(String appId) {
|
||||
return (CredentialProviderImpl) super.withAppId(appId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialProviderImpl withAppPassword(String appPassword) {
|
||||
return (CredentialProviderImpl) super.withAppPassword(appPassword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> isValidAppIdAsync(String appId) {
|
||||
return CompletableFuture.completedFuture(this.appId.equals(appId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> getAppPasswordAsync(String appId) {
|
||||
return CompletableFuture.completedFuture(this.appId.equals(appId) ? this.appPassword : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> isAuthenticationDisabledAsync() {
|
||||
return CompletableFuture.completedFuture(this.appId == null || this.appId.isEmpty());
|
||||
}
|
||||
}
|
|
@ -6,9 +6,11 @@ package com.microsoft.bot.connector.authentication;
|
|||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Validates and Examines JWT tokens from the Bot Framework Emulator
|
||||
|
@ -17,25 +19,40 @@ public class EmulatorValidation {
|
|||
/**
|
||||
* TO BOT FROM EMULATOR: Token validation parameters when connecting to a channel.
|
||||
*/
|
||||
public static final TokenValidationParameters ToBotFromEmulatorTokenValidationParameters = TokenValidationParameters.toBotFromEmulatorTokenValidationParameters();
|
||||
private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{
|
||||
this.validateIssuer = true;
|
||||
this.validIssuers = new ArrayList<String>() {{
|
||||
add("https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/"); // Auth v3.1, 1.0 token
|
||||
add("https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0"); // Auth v3.1, 2.0 token
|
||||
add("https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/"); // Auth v3.2, 1.0 token
|
||||
add("https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0"); // Auth v3.2, 2.0 token
|
||||
add("https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/"); // Auth for US Gov, 1.0 token
|
||||
add("https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0"); // Auth for US Gov, 2.0 token
|
||||
}};
|
||||
this.validateAudience = false;
|
||||
this.validateLifetime = true;
|
||||
this.clockSkew = Duration.ofMinutes(5);
|
||||
this.requireSignedTokens = true;
|
||||
}};
|
||||
|
||||
/**
|
||||
* Determines if a given Auth header is from the Bot Framework Emulator
|
||||
*
|
||||
* @param authHeader Bearer Token, in the "Bearer [Long String]" Format.
|
||||
* @return True, if the token was issued by the Emulator. Otherwise, false.
|
||||
*/
|
||||
public static CompletableFuture<Boolean> isTokenFromEmulator(String authHeader) {
|
||||
public static Boolean isTokenFromEmulator(String authHeader) {
|
||||
// The Auth Header generally looks like this:
|
||||
// "Bearer eyJ0e[...Big Long String...]XAiO"
|
||||
if (authHeader == null || authHeader.isEmpty()) {
|
||||
if (StringUtils.isEmpty(authHeader)) {
|
||||
// No token. Can't be an emulator token.
|
||||
return CompletableFuture.completedFuture(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] parts = authHeader.split(" ");
|
||||
if (parts.length != 2) {
|
||||
// Emulator tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not an emulator token
|
||||
return CompletableFuture.completedFuture(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
String schema = parts[0];
|
||||
|
@ -43,91 +60,120 @@ public class EmulatorValidation {
|
|||
|
||||
if (!schema.equalsIgnoreCase("bearer")) {
|
||||
// The scheme from the emulator MUST be "Bearer"
|
||||
return CompletableFuture.completedFuture(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the Big Long String into an actual token.
|
||||
DecodedJWT decodedJWT = JWT.decode(token);
|
||||
try {
|
||||
DecodedJWT decodedJWT = JWT.decode(token);
|
||||
|
||||
// Is there an Issuer?
|
||||
if (decodedJWT.getIssuer().isEmpty()) {
|
||||
// No Issuer, means it's not from the Emulator.
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
// Is there an Issuer?
|
||||
if (StringUtils.isEmpty(decodedJWT.getIssuer())) {
|
||||
// No Issuer, means it's not from the Emulator.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the token issues by a source we consider to be the emulator?
|
||||
if (!ToBotFromEmulatorTokenValidationParameters.validIssuers.contains(decodedJWT.getIssuer())) {
|
||||
// Is the token issues by a source we consider to be the emulator?
|
||||
// Not a Valid Issuer. This is NOT a Bot Framework Emulator Token.
|
||||
return CompletableFuture.completedFuture(false);
|
||||
return TOKENVALIDATIONPARAMETERS.validIssuers.contains(decodedJWT.getIssuer());
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The Token is from the Bot Framework Emulator. Success!
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from the Bot Framework Emulator.
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]"
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelProvider The channelService value that distinguishes public Azure from US Government Azure.
|
||||
* @param channelId The ID of the channel to validate.
|
||||
* @return A valid ClaimsIdentity.
|
||||
* <p>
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String channelId) throws ExecutionException, InterruptedException, AuthenticationException {
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId) {
|
||||
return authenticateToken(authHeader, credentials, channelProvider, channelId, new AuthenticationConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from the Bot Framework Emulator.
|
||||
* A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelProvider The channelService value that distinguishes public Azure from US Government Azure.
|
||||
* @param channelId The ID of the channel to validate.
|
||||
* @param authConfig The authentication configuration.
|
||||
* @return A valid ClaimsIdentity.
|
||||
* <p>
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId, AuthenticationConfiguration authConfig) {
|
||||
String openIdMetadataUrl = channelProvider != null && channelProvider.isGovernment() ?
|
||||
GovernmentAuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL :
|
||||
AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL;
|
||||
|
||||
JwtTokenExtractor tokenExtractor = new JwtTokenExtractor(
|
||||
ToBotFromEmulatorTokenValidationParameters,
|
||||
AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl,
|
||||
AuthenticationConstants.AllowedSigningAlgorithms);
|
||||
TOKENVALIDATIONPARAMETERS,
|
||||
openIdMetadataUrl,
|
||||
AuthenticationConstants.AllowedSigningAlgorithms);
|
||||
|
||||
ClaimsIdentity identity = tokenExtractor.getIdentityAsync(authHeader, channelId).get();
|
||||
if (identity == null) {
|
||||
// No valid identity. Not Authorized.
|
||||
throw new AuthenticationException("Invalid Identity");
|
||||
}
|
||||
return tokenExtractor.getIdentityAsync(authHeader, channelId, authConfig.requiredEndorsements())
|
||||
.thenApply(identity -> {
|
||||
if (identity == null) {
|
||||
// No valid identity. Not Authorized.
|
||||
throw new AuthenticationException("Invalid Identity");
|
||||
}
|
||||
|
||||
if (!identity.isAuthenticated()) {
|
||||
// The token is in some way invalid. Not Authorized.
|
||||
throw new AuthenticationException("Token Not Authenticated");
|
||||
}
|
||||
if (!identity.isAuthenticated()) {
|
||||
// The token is in some way invalid. Not Authorized.
|
||||
throw new AuthenticationException("Token Not Authenticated");
|
||||
}
|
||||
|
||||
// Now check that the AppID in the claims set matches
|
||||
// what we're looking for. Note that in a multi-tenant bot, this value
|
||||
// comes from developer code that may be reaching out to a service, hence the
|
||||
// Async validation.
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.VersionClaim)) {
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Emulator Tokens.", AuthenticationConstants.VersionClaim));
|
||||
}
|
||||
// Now check that the AppID in the claims set matches
|
||||
// what we're looking for. Note that in a multi-tenant bot, this value
|
||||
// comes from developer code that may be reaching out to a service, hence the
|
||||
// Async validation.
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.VERSION_CLAIM)) {
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Emulator Tokens.", AuthenticationConstants.VERSION_CLAIM));
|
||||
}
|
||||
|
||||
String tokenVersion = identity.claims().get(AuthenticationConstants.VersionClaim);
|
||||
String appId = "";
|
||||
String tokenVersion = identity.claims().get(AuthenticationConstants.VERSION_CLAIM);
|
||||
String appId = "";
|
||||
|
||||
// The Emulator, depending on Version, sends the AppId via either the
|
||||
// appid claim (Version 1) or the Authorized Party claim (Version 2).
|
||||
if (tokenVersion.isEmpty() || tokenVersion.equalsIgnoreCase("1.0")) {
|
||||
// either no Version or a version of "1.0" means we should look for
|
||||
// the claim in the "appid" claim.
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.AppIdClaim)) {
|
||||
// No claim around AppID. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '1.0'.", AuthenticationConstants.AppIdClaim));
|
||||
}
|
||||
// The Emulator, depending on Version, sends the AppId via either the
|
||||
// appid claim (Version 1) or the Authorized Party claim (Version 2).
|
||||
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.
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.APPID_CLAIM)) {
|
||||
// No claim around AppID. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '1.0'.", AuthenticationConstants.APPID_CLAIM));
|
||||
}
|
||||
|
||||
appId = identity.claims().get(AuthenticationConstants.AppIdClaim);
|
||||
} else if (tokenVersion.equalsIgnoreCase("2.0")) {
|
||||
// Emulator, "2.0" puts the AppId in the "azp" claim.
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.AuthorizedParty)) {
|
||||
// No claim around AppID. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '2.0'.", AuthenticationConstants.AuthorizedParty));
|
||||
}
|
||||
appId = identity.claims().get(AuthenticationConstants.APPID_CLAIM);
|
||||
} else if (tokenVersion.equalsIgnoreCase("2.0")) {
|
||||
// Emulator, "2.0" puts the AppId in the "azp" claim.
|
||||
if (!identity.claims().containsKey(AuthenticationConstants.AUTHORIZED_PARTY)) {
|
||||
// No claim around AppID. Not Authorized.
|
||||
throw new AuthenticationException(String.format("'%s' claim is required on Emulator Token version '2.0'.", AuthenticationConstants.AUTHORIZED_PARTY));
|
||||
}
|
||||
|
||||
appId = identity.claims().get(AuthenticationConstants.AuthorizedParty);
|
||||
} else {
|
||||
// Unknown Version. Not Authorized.
|
||||
throw new AuthenticationException(String.format("Unknown Emulator Token version '%s'.", tokenVersion));
|
||||
}
|
||||
appId = identity.claims().get(AuthenticationConstants.AUTHORIZED_PARTY);
|
||||
} else {
|
||||
// Unknown Version. Not Authorized.
|
||||
throw new AuthenticationException(String.format("Unknown Emulator Token version '%s'.", tokenVersion));
|
||||
}
|
||||
|
||||
if (!credentials.isValidAppIdAsync(appId).get()) {
|
||||
throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appId));
|
||||
}
|
||||
if (!credentials.isValidAppIdAsync(appId).join()) {
|
||||
throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appId));
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(identity);
|
||||
return identity;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class EndorsementsValidator {
|
||||
|
||||
/**
|
||||
* Verify that the set of ChannelIds, which come from the incoming activities,
|
||||
* all match the endorsements found on the JWT Token.
|
||||
* For example, if an Activity comes from webchat, that channelId says
|
||||
* says "webchat" and the jwt token endorsement MUST match that.
|
||||
* @param channelId The channel name, typically extracted from the activity.ChannelId field, that
|
||||
* to which the Activity is affinitized.
|
||||
*
|
||||
* @param channelId The channel name, typically extracted from the activity.ChannelId field, that
|
||||
* to which the Activity is affinitized.
|
||||
* @param endorsements Whoever signed the JWT token is permitted to send activities only for
|
||||
* some specific channels. That list is the endorsement list, and is validated here against the channelId.
|
||||
* some specific channels. That list is the endorsement list, and is validated here against the channelId.
|
||||
* @return True is the channelId is found in the Endorsement set. False if the channelId is not found.
|
||||
*/
|
||||
public static boolean validate(String channelId, List<String> endorsements) {
|
||||
|
||||
// If the Activity came in and doesn't have a Channel ID then it's making no
|
||||
// assertions as to who endorses it. This means it should pass.
|
||||
if (channelId == null || channelId.isEmpty())
|
||||
if (StringUtils.isEmpty(channelId))
|
||||
return true;
|
||||
|
||||
if (endorsements == null)
|
||||
|
@ -35,12 +40,6 @@ public abstract class EndorsementsValidator {
|
|||
// JwtTokenExtractor
|
||||
|
||||
// Does the set of endorsements match the channelId that was passed in?
|
||||
|
||||
// ToDo: Consider moving this to a HashSet instead of a string
|
||||
// array, to make lookups O(1) instead of O(N). To give a sense
|
||||
// of scope, tokens from WebChat have about 10 endorsements, and
|
||||
// tokens coming from Teams have about 20.
|
||||
|
||||
return endorsements.contains(channelId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class EnterpriseChannelValidation {
|
||||
private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{
|
||||
this.validateIssuer = true;
|
||||
this.validIssuers = new ArrayList<String>() {{
|
||||
add(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER);
|
||||
}};
|
||||
this.validateAudience = false;
|
||||
this.validateLifetime = true;
|
||||
this.clockSkew = Duration.ofMinutes(5);
|
||||
this.requireSignedTokens = true;
|
||||
}};
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from a Bot Framework Channel Service.
|
||||
* A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelProvider The channelService value that distinguishes public Azure from US Government Azure.
|
||||
* @param channelId The ID of the channel to validate.
|
||||
* @return A valid ClaimsIdentity.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String serviceUrl, String channelId) {
|
||||
return authenticateToken(authHeader, credentials, channelProvider, serviceUrl, channelId, new AuthenticationConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from a Bot Framework Channel Service.
|
||||
* A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param channelProvider The channelService value that distinguishes public Azure from US Government Azure.
|
||||
* @param channelId The ID of the channel to validate.
|
||||
* @param authConfig The authentication configuration.
|
||||
* @return A valid ClaimsIdentity.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String serviceUrl, String channelId, AuthenticationConfiguration authConfig) {
|
||||
if (authConfig == null) {
|
||||
throw new IllegalArgumentException("Missing AuthenticationConfiguration");
|
||||
}
|
||||
|
||||
return channelProvider.getChannelService()
|
||||
|
||||
.thenCompose(channelService -> {
|
||||
JwtTokenExtractor tokenExtractor = new JwtTokenExtractor(
|
||||
TOKENVALIDATIONPARAMETERS,
|
||||
String.format(AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPENID_METADATA_URL_FORMAT, channelService),
|
||||
AuthenticationConstants.AllowedSigningAlgorithms);
|
||||
|
||||
return tokenExtractor.getIdentityAsync(authHeader, channelId, authConfig.requiredEndorsements());
|
||||
})
|
||||
|
||||
.thenCompose(identity -> {
|
||||
if (identity == null) {
|
||||
// No valid identity. Not Authorized.
|
||||
throw new AuthenticationException("Invalid Identity");
|
||||
}
|
||||
|
||||
return validateIdentity(identity, credentials, serviceUrl);
|
||||
});
|
||||
}
|
||||
|
||||
public static CompletableFuture<ClaimsIdentity> validateIdentity(ClaimsIdentity identity, CredentialProvider credentials, String serviceUrl) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (identity == null || !identity.isAuthenticated()) {
|
||||
throw new AuthenticationException("Invalid Identity");
|
||||
}
|
||||
|
||||
// Now check that the AppID in the claim set matches
|
||||
// what we're looking for. Note that in a multi-tenant bot, this value
|
||||
// comes from developer code that may be reaching out to a service, hence the
|
||||
// Async validation.
|
||||
|
||||
if (!StringUtils.equalsIgnoreCase(identity.getIssuer(), AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER)) {
|
||||
throw new AuthenticationException("Wrong Issuer");
|
||||
}
|
||||
|
||||
// The AppId from the claim in the token must match the AppId specified by the developer. Note that
|
||||
// the Bot Framework uses the Audience claim ("aud") to pass the AppID.
|
||||
String appIdFromAudienceClaim = identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM);
|
||||
if (StringUtils.isEmpty(appIdFromAudienceClaim)) {
|
||||
// Claim is present, but doesn't have a value. Not Authorized.
|
||||
throw new AuthenticationException("No Audience Claim");
|
||||
}
|
||||
|
||||
boolean isValid = credentials.isValidAppIdAsync(appIdFromAudienceClaim).join();
|
||||
if (!isValid) {
|
||||
throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim));
|
||||
}
|
||||
|
||||
String serviceUrlClaim = identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM);
|
||||
if (StringUtils.isEmpty(serviceUrl)) {
|
||||
throw new AuthenticationException(String.format("Invalid serviceurl passed on token: '%s'.", serviceUrlClaim));
|
||||
}
|
||||
|
||||
if (!StringUtils.equals(serviceUrl, serviceUrlClaim)) {
|
||||
throw new AuthenticationException(String.format("serviceurl doesn't match claim: '%s'.", serviceUrlClaim));
|
||||
}
|
||||
|
||||
return identity;
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
/**
|
||||
* Values and Constants used for Authentication and Authorization by the Bot Framework Protocol
|
||||
* to US Government DataCenters.
|
||||
*/
|
||||
public class GovernmentAuthenticationConstants {
|
||||
public static final String CHANNELSERVICE = "https://botframework.azure.us";
|
||||
|
||||
/**
|
||||
* TO GOVERNMENT CHANNEL FROM BOT: Login URL.
|
||||
*/
|
||||
public static final String TO_CHANNEL_FROM_BOT_LOGIN_URL = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e";
|
||||
|
||||
/**
|
||||
* TO GOVERNMENT CHANNEL FROM BOT: OAuth scope to request.
|
||||
*/
|
||||
public static final String TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = "https://api.botframework.us";
|
||||
|
||||
/**
|
||||
* TO BOT FROM GOVERNMENT CHANNEL: Token issuer.
|
||||
*/
|
||||
public static final String TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.us";
|
||||
|
||||
/**
|
||||
* OAuth Url used to get a token from OAuthApiClient.
|
||||
*/
|
||||
public static final String OAUTH_URL_GOV = "https://api.botframework.azure.us";
|
||||
|
||||
/**
|
||||
* TO BOT FROM GOVERNMANT CHANNEL: OpenID metadata document for tokens coming from MSA.
|
||||
*/
|
||||
public static final String TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = "https://login.botframework.azure.us/v1/.well-known/openidconfiguration";
|
||||
|
||||
/**
|
||||
* TO BOT FROM GOVERNMENT EMULATOR: OpenID metadata document for tokens coming from MSA.
|
||||
*/
|
||||
public static final String TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration";
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* TO BOT FROM GOVERNMENT CHANNEL: Token validation parameters when connecting to a bot.
|
||||
*/
|
||||
public class GovernmentChannelValidation {
|
||||
private static final TokenValidationParameters TOKENVALIDATIONPARAMETERS = new TokenValidationParameters() {{
|
||||
this.validateIssuer = true;
|
||||
this.validIssuers = new ArrayList<String>() {{
|
||||
add(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER);
|
||||
}};
|
||||
this.validateAudience = false;
|
||||
this.validateLifetime = true;
|
||||
this.clockSkew = Duration.ofMinutes(5);
|
||||
this.requireSignedTokens = true;
|
||||
}};
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from a Bot Framework Government Channel Service.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param serviceUrl The service url from the request.
|
||||
* @param channelId The ID of the channel to validate.
|
||||
* @return A CompletableFuture representing the asynchronous operation.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException Authentication failed.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String serviceUrl, String channelId) {
|
||||
return authenticateToken(authHeader, credentials, serviceUrl, channelId, new AuthenticationConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the incoming Auth Header as a token sent from a Bot Framework Government Channel Service.
|
||||
*
|
||||
* @param authHeader The raw HTTP header in the format: "Bearer [longString]".
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param serviceUrl The service url from the request.
|
||||
* @param channelId The ID of the channel to validate.
|
||||
* @param authConfig The authentication configuration.
|
||||
* @return A CompletableFuture representing the asynchronous operation.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException Authentication failed.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateToken(String authHeader, CredentialProvider credentials, String serviceUrl, String channelId, AuthenticationConfiguration authConfig) {
|
||||
JwtTokenExtractor tokenExtractor = new JwtTokenExtractor(
|
||||
TOKENVALIDATIONPARAMETERS,
|
||||
GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL,
|
||||
AuthenticationConstants.AllowedSigningAlgorithms);
|
||||
|
||||
return tokenExtractor.getIdentityAsync(authHeader, channelId, authConfig.requiredEndorsements())
|
||||
.thenCompose(identity -> {
|
||||
return validateIdentity(identity, credentials, serviceUrl);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the ClaimsIdentity as sent from a Bot Framework Government Channel Service.
|
||||
*
|
||||
* @param identity The claims identity to validate.
|
||||
* @param credentials The user defined set of valid credentials, such as the AppId.
|
||||
* @param serviceUrl The service url from the request.
|
||||
* @return A CompletableFuture representing the asynchronous operation.
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException Validation failed.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> validateIdentity(ClaimsIdentity identity, CredentialProvider credentials, String serviceUrl) {
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (identity == null || !identity.isAuthenticated()) {
|
||||
throw new AuthenticationException("Invalid Identity");
|
||||
}
|
||||
|
||||
// Now check that the AppID in the claim set matches
|
||||
// what we're looking for. Note that in a multi-tenant bot, this value
|
||||
// comes from developer code that may be reaching out to a service, hence the
|
||||
// Async validation.
|
||||
|
||||
if (!StringUtils.equalsIgnoreCase(identity.getIssuer(), GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER)) {
|
||||
throw new AuthenticationException("Wrong Issuer");
|
||||
}
|
||||
|
||||
// The AppId from the claim in the token must match the AppId specified by the developer. Note that
|
||||
// the Bot Framework uses the Audience claim ("aud") to pass the AppID.
|
||||
String appIdFromAudienceClaim = identity.claims().get(AuthenticationConstants.AUDIENCE_CLAIM);
|
||||
if (StringUtils.isEmpty(appIdFromAudienceClaim)) {
|
||||
// Claim is present, but doesn't have a value. Not Authorized.
|
||||
throw new AuthenticationException("No Audience Claim");
|
||||
}
|
||||
|
||||
boolean isValid = credentials.isValidAppIdAsync(appIdFromAudienceClaim).join();
|
||||
if (!isValid) {
|
||||
throw new AuthenticationException(String.format("Invalid AppId passed on token: '%s'.", appIdFromAudienceClaim));
|
||||
}
|
||||
|
||||
String serviceUrlClaim = identity.claims().get(AuthenticationConstants.SERVICE_URL_CLAIM);
|
||||
if (StringUtils.isEmpty(serviceUrl)) {
|
||||
throw new AuthenticationException(String.format("Invalid serviceurl passed on token: '%s'.", serviceUrlClaim));
|
||||
}
|
||||
|
||||
if (!StringUtils.equals(serviceUrl, serviceUrlClaim)) {
|
||||
throw new AuthenticationException(String.format("serviceurl doesn't match claim: '%s'.", serviceUrlClaim));
|
||||
}
|
||||
|
||||
return identity;
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
}
|
|
@ -9,19 +9,19 @@ import com.auth0.jwt.exceptions.JWTVerificationException;
|
|||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.auth0.jwt.interfaces.Verification;
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class JwtTokenExtractor {
|
||||
private static final Logger LOGGER = Logger.getLogger(OpenIdMetadata.class.getName());
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdMetadata.class);
|
||||
|
||||
private static final ConcurrentMap<String, OpenIdMetadata> openIdMetadataCache = new ConcurrentHashMap<>();
|
||||
|
||||
|
@ -37,30 +37,34 @@ public class JwtTokenExtractor {
|
|||
}
|
||||
|
||||
public CompletableFuture<ClaimsIdentity> getIdentityAsync(String authorizationHeader, String channelId) {
|
||||
return getIdentityAsync(authorizationHeader, channelId, new ArrayList<>());
|
||||
}
|
||||
|
||||
public CompletableFuture<ClaimsIdentity> getIdentityAsync(String authorizationHeader, String channelId, List<String> requiredEndorsements) {
|
||||
if (authorizationHeader == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
throw new IllegalArgumentException("authorizationHeader is required");
|
||||
}
|
||||
|
||||
String[] parts = authorizationHeader.split(" ");
|
||||
if (parts.length == 2) {
|
||||
return getIdentityAsync(parts[0], parts[1], channelId);
|
||||
return getIdentityAsync(parts[0], parts[1], channelId, requiredEndorsements);
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
public CompletableFuture<ClaimsIdentity> getIdentityAsync(String schema, String token, String channelId) {
|
||||
public CompletableFuture<ClaimsIdentity> getIdentityAsync(String schema, String token, String channelId, List<String> requiredEndorsements) {
|
||||
// No header in correct scheme or no token
|
||||
if (!schema.equalsIgnoreCase("bearer") || token == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
// Issuer isn't allowed? No need to check signature
|
||||
if (!this.hasAllowedIssuer(token)) {
|
||||
if (!hasAllowedIssuer(token)) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
return this.validateTokenAsync(token, channelId);
|
||||
return validateTokenAsync(token, channelId, requiredEndorsements);
|
||||
}
|
||||
|
||||
private boolean hasAllowedIssuer(String token) {
|
||||
|
@ -69,11 +73,15 @@ public class JwtTokenExtractor {
|
|||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private CompletableFuture<ClaimsIdentity> validateTokenAsync(String token, String channelId) {
|
||||
private CompletableFuture<ClaimsIdentity> validateTokenAsync(String token, String channelId, List<String> requiredEndorsements) {
|
||||
DecodedJWT decodedJWT = JWT.decode(token);
|
||||
OpenIdMetadataKey key = this.openIdMetadata.getKey(decodedJWT.getKeyId());
|
||||
|
||||
if (key != null) {
|
||||
if (key == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
Verification verification = JWT.require(Algorithm.RSA256(key.key, null));
|
||||
try {
|
||||
verification.build().verify(token);
|
||||
|
@ -87,26 +95,25 @@ public class JwtTokenExtractor {
|
|||
if (!isEndorsed) {
|
||||
throw new AuthenticationException(String.format("Could not validate endorsement for key: %s with endorsements: %s", key.key.toString(), StringUtils.join(key.endorsements)));
|
||||
}
|
||||
|
||||
// Verify that additional endorsements are satisfied. If no additional endorsements are expected, the requirement is satisfied as well
|
||||
boolean additionalEndorsementsSatisfied =
|
||||
requiredEndorsements.stream().allMatch((endorsement) -> EndorsementsValidator.validate(endorsement, key.endorsements));
|
||||
if (!additionalEndorsementsSatisfied) {
|
||||
throw new AuthenticationException(String.format("Could not validate additional endorsement for key: %s with endorsements: %s", key.key.toString(), StringUtils.join(requiredEndorsements)));
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.allowedSigningAlgorithms.contains(decodedJWT.getAlgorithm())) {
|
||||
throw new AuthenticationException(String.format("Could not validate algorithm for key: %s with algorithms: %s", decodedJWT.getAlgorithm(), StringUtils.join(allowedSigningAlgorithms)));
|
||||
}
|
||||
|
||||
Map<String, String> claims = new HashMap<>();
|
||||
if (decodedJWT.getClaims() != null) {
|
||||
decodedJWT.getClaims().forEach((k, v) -> claims.put(k, v.asString()));
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(new ClaimsIdentityImpl(decodedJWT.getIssuer(), claims));
|
||||
|
||||
return new ClaimsIdentity(decodedJWT);
|
||||
} catch (JWTVerificationException ex) {
|
||||
String errorDescription = ex.getMessage();
|
||||
LOGGER.log(Level.WARNING, errorDescription);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
LOGGER.warn(errorDescription);
|
||||
throw new AuthenticationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,16 @@
|
|||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Contains helper methods for authenticating incoming HTTP requests.
|
||||
*/
|
||||
public class JwtTokenValidation {
|
||||
|
||||
/**
|
||||
|
@ -17,48 +22,106 @@ public class JwtTokenValidation {
|
|||
* @param activity The incoming Activity from the Bot Framework or the Emulator
|
||||
* @param authHeader The Bearer token included as part of the request
|
||||
* @param credentials The set of valid credentials, such as the Bot Application ID
|
||||
* @return Nothing
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws AuthenticationException Throws on auth failed.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateRequest(Activity activity, String authHeader, CredentialProvider credentials) throws AuthenticationException, InterruptedException, ExecutionException {
|
||||
if (authHeader == null || authHeader.isEmpty()) {
|
||||
// No auth header was sent. We might be on the anonymous code path.
|
||||
boolean isAuthDisable = credentials.isAuthenticationDisabledAsync().get();
|
||||
if (isAuthDisable) {
|
||||
// In the scenario where Auth is disabled, we still want to have the
|
||||
// IsAuthenticated flag set in the ClaimsIdentity. To do this requires
|
||||
// adding in an empty claim.
|
||||
return CompletableFuture.completedFuture(new ClaimsIdentityImpl("anonymous"));
|
||||
}
|
||||
|
||||
// No Auth Header. Auth is required. Request is not authorized.
|
||||
throw new AuthenticationException("No Auth Header. Auth is required.");
|
||||
}
|
||||
|
||||
// Go through the standard authentication path.
|
||||
ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(authHeader, credentials, activity.channelId(), activity.serviceUrl()).get();
|
||||
|
||||
// On the standard Auth path, we need to trust the URL that was incoming.
|
||||
MicrosoftAppCredentials.trustServiceUrl(activity.serviceUrl());
|
||||
return CompletableFuture.completedFuture(identity);
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateRequest(Activity activity, String authHeader, CredentialProvider credentials, ChannelProvider channelProvider) throws AuthenticationException, InterruptedException, ExecutionException {
|
||||
return authenticateRequest(activity, authHeader, credentials, channelProvider, new AuthenticationConfiguration());
|
||||
}
|
||||
|
||||
// TODO: Recieve httpClient and use ClientID
|
||||
public static CompletableFuture<ClaimsIdentity> validateAuthHeader(String authHeader, CredentialProvider credentials, String channelId, String serviceUrl) throws ExecutionException, InterruptedException, AuthenticationException {
|
||||
if (authHeader == null || authHeader.isEmpty()) {
|
||||
/**
|
||||
* Validates the security tokens required by the Bot Framework Protocol. Throws on any exceptions.
|
||||
*
|
||||
* @param activity The incoming Activity from the Bot Framework or the Emulator
|
||||
* @param authHeader The Bearer token included as part of the request
|
||||
* @param credentials The set of valid credentials, such as the Bot Application ID
|
||||
* @param authConfig The authentication configuration.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws AuthenticationException Throws on auth failed.
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> authenticateRequest(Activity activity, String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, AuthenticationConfiguration authConfig) throws AuthenticationException, InterruptedException, ExecutionException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (StringUtils.isEmpty(authHeader)) {
|
||||
// No auth header was sent. We might be on the anonymous code path.
|
||||
boolean isAuthDisable = credentials.isAuthenticationDisabledAsync().join();
|
||||
if (isAuthDisable) {
|
||||
// In the scenario where Auth is disabled, we still want to have the
|
||||
// IsAuthenticated flag set in the ClaimsIdentity. To do this requires
|
||||
// adding in an empty claim.
|
||||
return new ClaimsIdentity("anonymous");
|
||||
}
|
||||
|
||||
// No Auth Header. Auth is required. Request is not authorized.
|
||||
throw new AuthenticationException("No Auth Header. Auth is required.");
|
||||
}
|
||||
|
||||
// Go through the standard authentication path. This will throw AuthenticationException if
|
||||
// it fails.
|
||||
ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(authHeader, credentials, channelProvider, activity.channelId(), activity.serviceUrl(), authConfig).join();
|
||||
|
||||
// On the standard Auth path, we need to trust the URL that was incoming.
|
||||
MicrosoftAppCredentials.trustServiceUrl(activity.serviceUrl());
|
||||
|
||||
return identity;
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the authentication header of an incoming request.
|
||||
*
|
||||
* @param authHeader The authentication header to validate.
|
||||
* @param credentials The bot's credential provider.
|
||||
* @param channelProvider The bot's channel service provider.
|
||||
* @param channelId The ID of the channel that sent the request.
|
||||
* @param serviceUrl The service URL for the activity.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*
|
||||
* On Call:
|
||||
* @throws IllegalArgumentException Incorrect arguments supplied
|
||||
*
|
||||
* On join:
|
||||
* @throws AuthenticationException Authentication Error
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> validateAuthHeader(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId, String serviceUrl) {
|
||||
return validateAuthHeader(authHeader, credentials, channelProvider, channelId, serviceUrl, new AuthenticationConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the authentication header of an incoming request.
|
||||
*
|
||||
* @param authHeader The authentication header to validate.
|
||||
* @param credentials The bot's credential provider.
|
||||
* @param channelProvider The bot's channel service provider.
|
||||
* @param channelId The ID of the channel that sent the request.
|
||||
* @param serviceUrl The service URL for the activity.
|
||||
* @param authConfig The authentication configuration.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*
|
||||
* On Call:
|
||||
* @throws IllegalArgumentException Incorrect arguments supplied
|
||||
*
|
||||
* On Join:
|
||||
* @throws AuthenticationException Authentication Error
|
||||
*/
|
||||
public static CompletableFuture<ClaimsIdentity> validateAuthHeader(String authHeader, CredentialProvider credentials, ChannelProvider channelProvider, String channelId, String serviceUrl, AuthenticationConfiguration authConfig) {
|
||||
if (StringUtils.isEmpty(authHeader)) {
|
||||
throw new IllegalArgumentException("No authHeader present. Auth is required.");
|
||||
}
|
||||
|
||||
boolean usingEmulator = EmulatorValidation.isTokenFromEmulator(authHeader).get();
|
||||
boolean usingEmulator = EmulatorValidation.isTokenFromEmulator(authHeader);
|
||||
if (usingEmulator) {
|
||||
return EmulatorValidation.authenticateToken(authHeader, credentials, channelId);
|
||||
} else {
|
||||
return EmulatorValidation.authenticateToken(authHeader, credentials, channelProvider, channelId, authConfig);
|
||||
} else if (channelProvider == null || channelProvider.isPublicAzure()) {
|
||||
// No empty or null check. Empty can point to issues. Null checks only.
|
||||
if (serviceUrl != null) {
|
||||
return ChannelValidation.authenticateToken(authHeader, credentials, channelId, serviceUrl);
|
||||
return ChannelValidation.authenticateToken(authHeader, credentials, channelId, serviceUrl, authConfig);
|
||||
} else {
|
||||
return ChannelValidation.authenticateToken(authHeader, credentials, channelId);
|
||||
return ChannelValidation.authenticateToken(authHeader, credentials, channelId, authConfig);
|
||||
}
|
||||
} else if (channelProvider.isGovernment()) {
|
||||
return GovernmentChannelValidation.authenticateToken(authHeader, credentials, serviceUrl, channelId, authConfig);
|
||||
} else {
|
||||
return EnterpriseChannelValidation.authenticateToken(authHeader, credentials, channelProvider, serviceUrl, channelId, authConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,111 +3,60 @@
|
|||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.aad.adal4j.AuthenticationResult;
|
||||
import com.microsoft.aad.adal4j.ClientCredential;
|
||||
import com.microsoft.rest.credentials.ServiceClientCredentials;
|
||||
import okhttp3.*;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToChannelFromBotLoginUrl;
|
||||
import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToChannelFromBotOAuthScope;
|
||||
|
||||
/**
|
||||
* MicrosoftAppCredentials auth implementation
|
||||
*/
|
||||
public class MicrosoftAppCredentials implements ServiceClientCredentials {
|
||||
public static final String MICROSOFTAPPID = "MicrosoftAppId";
|
||||
public static final String MICROSOFTAPPPASSWORD = "MicrosoftAppPassword";
|
||||
|
||||
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static ConcurrentMap<String, LocalDateTime> trustHostNames = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
trustHostNames.put("api.botframework.com", LocalDateTime.MAX);
|
||||
trustHostNames.put("token.botframework.com", LocalDateTime.MAX);
|
||||
trustHostNames.put("api.botframework.azure.us", LocalDateTime.MAX);
|
||||
trustHostNames.put("token.botframework.azure.us", LocalDateTime.MAX);
|
||||
}
|
||||
|
||||
private String appId;
|
||||
private String appPassword;
|
||||
|
||||
private OkHttpClient client;
|
||||
private ObjectMapper mapper;
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
public static final MediaType FORM_ENCODE = MediaType.parse("application/x-www-form-urlencoded");
|
||||
|
||||
private String currentToken = null;
|
||||
private long expiredTime = 0;
|
||||
//private static final Object cacheSync = new Object();
|
||||
protected static final HashMap<String, OAuthResponse> cache = new HashMap<String, OAuthResponse>();
|
||||
|
||||
public final String OAuthEndpoint = ToChannelFromBotLoginUrl;
|
||||
public final String OAuthScope = ToChannelFromBotOAuthScope;
|
||||
|
||||
|
||||
public String getTokenCacheKey() {
|
||||
return String.format("%s-cache", this.appId);
|
||||
}
|
||||
private String channelAuthTenant;
|
||||
private AdalAuthenticator authenticator;
|
||||
|
||||
public MicrosoftAppCredentials(String appId, String appPassword) {
|
||||
this.appId = appId;
|
||||
this.appPassword = appPassword;
|
||||
this.client = new OkHttpClient.Builder().build();
|
||||
this.mapper = new ObjectMapper().findAndRegisterModules();
|
||||
}
|
||||
|
||||
public static final MicrosoftAppCredentials Empty = new MicrosoftAppCredentials(null, null);
|
||||
|
||||
public String microsoftAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
public MicrosoftAppCredentials withMicrosoftAppId(String appId) {
|
||||
public MicrosoftAppCredentials(String appId, String appPassword, String channelAuthTenant) throws MalformedURLException {
|
||||
this.appId = appId;
|
||||
return this;
|
||||
this.appPassword = appPassword;
|
||||
setChannelAuthTenant(channelAuthTenant);
|
||||
}
|
||||
|
||||
public String getToken(Request request) throws IOException {
|
||||
if (System.currentTimeMillis() < expiredTime) {
|
||||
return currentToken;
|
||||
}
|
||||
Request reqToken = request.newBuilder()
|
||||
.url(ToChannelFromBotLoginUrl)
|
||||
.post(new FormBody.Builder()
|
||||
.add("grant_type", "client_credentials")
|
||||
.add("client_id", this.appId)
|
||||
.add("client_secret", this.appPassword)
|
||||
.add("scope", ToChannelFromBotOAuthScope)
|
||||
.build()).build();
|
||||
Response response = this.client.newCall(reqToken).execute();
|
||||
if (response.isSuccessful()) {
|
||||
String payload = response.body().string();
|
||||
AuthenticationResponse authResponse = this.mapper.readValue(payload, AuthenticationResponse.class);
|
||||
this.expiredTime = System.currentTimeMillis() + (authResponse.expiresIn * 1000);
|
||||
this.currentToken = authResponse.accessToken;
|
||||
}
|
||||
return this.currentToken;
|
||||
public static MicrosoftAppCredentials empty() {
|
||||
return new MicrosoftAppCredentials(null, null);
|
||||
}
|
||||
|
||||
|
||||
protected boolean ShouldSetToken(String url) {
|
||||
if (isTrustedServiceUrl(url)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) {
|
||||
clientBuilder.interceptors().add(new MicrosoftAppCredentialsInterceptor(this));
|
||||
}
|
||||
|
||||
private static class AuthenticationResponse {
|
||||
@JsonProperty(value = "token_type")
|
||||
String tokenType;
|
||||
@JsonProperty(value = "expires_in")
|
||||
long expiresIn;
|
||||
@JsonProperty(value = "ext_expires_in")
|
||||
long extExpiresIn;
|
||||
@JsonProperty(value = "access_token")
|
||||
String accessToken;
|
||||
}
|
||||
|
||||
|
||||
public static void trustServiceUrl(URI serviceUrl) {
|
||||
trustServiceUrl(serviceUrl.toString(), LocalDateTime.now().plusDays(1));
|
||||
}
|
||||
|
@ -121,8 +70,7 @@ public class MicrosoftAppCredentials implements ServiceClientCredentials {
|
|||
URL url = new URL(serviceUrl);
|
||||
trustServiceUrl(url, expirationTime);
|
||||
} catch (MalformedURLException e) {
|
||||
//TODO: What's missing here?
|
||||
e.printStackTrace();
|
||||
LoggerFactory.getLogger(MicrosoftAppCredentials.class).error("trustServiceUrl", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,6 +83,7 @@ public class MicrosoftAppCredentials implements ServiceClientCredentials {
|
|||
URL url = new URL(serviceUrl);
|
||||
return isTrustedServiceUrl(url);
|
||||
} catch (MalformedURLException e) {
|
||||
LoggerFactory.getLogger(MicrosoftAppCredentials.class).error("trustServiceUrl", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -147,9 +96,77 @@ public class MicrosoftAppCredentials implements ServiceClientCredentials {
|
|||
return !trustHostNames.getOrDefault(url.host(), LocalDateTime.MIN).isBefore(LocalDateTime.now().minusMinutes(5));
|
||||
}
|
||||
|
||||
private static ConcurrentMap<String, LocalDateTime> trustHostNames = new ConcurrentHashMap<>();
|
||||
public String appId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
static {
|
||||
trustHostNames.put("state.botframework.com", LocalDateTime.MAX);
|
||||
public String appPassword() {
|
||||
return this.appPassword;
|
||||
}
|
||||
|
||||
public MicrosoftAppCredentials withAppId(String appId) {
|
||||
this.appId = appId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MicrosoftAppCredentials withAppPassword(String appPassword) {
|
||||
this.appPassword = appPassword;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String channelAuthTenant() {
|
||||
return channelAuthTenant == null ? AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT : channelAuthTenant;
|
||||
}
|
||||
|
||||
public void setChannelAuthTenant(String authTenant) throws MalformedURLException {
|
||||
String originalAuthTenant = channelAuthTenant;
|
||||
try {
|
||||
channelAuthTenant = authTenant;
|
||||
new URL(oAuthEndpoint()).toString();
|
||||
} catch(MalformedURLException e) {
|
||||
channelAuthTenant = originalAuthTenant;
|
||||
}
|
||||
}
|
||||
|
||||
public MicrosoftAppCredentials withChannelAuthTenant(String authTenant) throws MalformedURLException {
|
||||
setChannelAuthTenant(authTenant);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String oAuthEndpoint() {
|
||||
return String.format(AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_TEMPLATE, channelAuthTenant());
|
||||
}
|
||||
|
||||
public String oAuthScope() {
|
||||
return AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE;
|
||||
}
|
||||
|
||||
public Future<AuthenticationResult> getToken() {
|
||||
return getAuthenticator().acquireToken();
|
||||
}
|
||||
|
||||
protected boolean ShouldSetToken(String url) {
|
||||
return isTrustedServiceUrl(url);
|
||||
}
|
||||
|
||||
private AdalAuthenticator getAuthenticator() {
|
||||
try {
|
||||
if (this.authenticator == null) {
|
||||
this.authenticator = new AdalAuthenticator(
|
||||
new ClientCredential(this.appId, this.appPassword),
|
||||
new OAuthConfiguration(oAuthEndpoint(), oAuthScope()));
|
||||
}
|
||||
} catch(MalformedURLException e) {
|
||||
// intentional no-op. This class validates the URL on construction or setChannelAuthTenant.
|
||||
// That is... this will never happen.
|
||||
LoggerFactory.getLogger(MicrosoftAppCredentials.class).error("getAuthenticator", e);
|
||||
}
|
||||
|
||||
return this.authenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) {
|
||||
clientBuilder.interceptors().add(new MicrosoftAppCredentialsInterceptor(this));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,15 @@ class MicrosoftAppCredentialsInterceptor implements Interceptor {
|
|||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
if (MicrosoftAppCredentials.isTrustedServiceUrl(chain.request().url().url().toString())) {
|
||||
String token;
|
||||
try {
|
||||
token = this.credentials.getToken().get().getAccessToken();
|
||||
} catch (Throwable t) {
|
||||
throw new IOException(t);
|
||||
}
|
||||
|
||||
Request newRequest = chain.request().newBuilder()
|
||||
.header("Authorization", "Bearer " + this.credentials.getToken(chain.request()))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.build();
|
||||
return chain.proceed(newRequest);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
/**
|
||||
* MicrosoftGovernmentAppCredentials auth implementation.
|
||||
*/
|
||||
public class MicrosoftGovernmentAppCredentials extends MicrosoftAppCredentials {
|
||||
/**
|
||||
* Initializes a new instance of the MicrosoftGovernmentAppCredentials class.
|
||||
*
|
||||
* @param appId The Microsoft app ID.
|
||||
* @param password The Microsoft app password.
|
||||
*/
|
||||
public MicrosoftGovernmentAppCredentials(String appId, String password) {
|
||||
super(appId, password);
|
||||
}
|
||||
|
||||
public static MicrosoftGovernmentAppCredentials empty() {
|
||||
return new MicrosoftGovernmentAppCredentials(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the OAuth endpoint to use.
|
||||
*
|
||||
* @return The OAuth endpoint to use.
|
||||
*/
|
||||
@Override
|
||||
public String oAuthEndpoint() {
|
||||
return GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the OAuth scope to use.
|
||||
*
|
||||
* @return The OAuth scope to use.
|
||||
*/
|
||||
@Override
|
||||
public String oAuthScope() {
|
||||
return GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import com.microsoft.bot.connector.UserAgent;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import com.microsoft.bot.schema.TokenExchangeState;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import com.microsoft.bot.schema.models.ConversationReference;
|
||||
|
@ -41,13 +45,13 @@ import static java.util.stream.Collectors.joining;
|
|||
* Uses the MicrosoftInterceptor class to add Authorization header from idp.
|
||||
*/
|
||||
public class OAuthClient extends ServiceClient {
|
||||
private final ConnectorClientImpl client;
|
||||
private final RestConnectorClient client;
|
||||
private final String uri;
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
|
||||
public OAuthClient(ConnectorClientImpl client, String uri) throws URISyntaxException, MalformedURLException {
|
||||
public OAuthClient(RestConnectorClient client, String uri) throws URISyntaxException, MalformedURLException {
|
||||
super(client.restClient());
|
||||
URI uriResult = new URI(uri);
|
||||
|
||||
|
@ -69,7 +73,7 @@ public class OAuthClient extends ServiceClient {
|
|||
* @param userId
|
||||
* @param connectionName
|
||||
* @param magicCode
|
||||
* @return CompletableFuture< TokenResponse > on success; otherwise null.
|
||||
* @return CompletableFuture<TokenResponse> on success; otherwise null.
|
||||
*/
|
||||
public CompletableFuture<TokenResponse> GetUserTokenAsync(String userId, String connectionName, String magicCode) throws IOException, URISyntaxException, ExecutionException, InterruptedException {
|
||||
return GetUserTokenAsync(userId, connectionName, magicCode, null);
|
||||
|
@ -77,14 +81,14 @@ public class OAuthClient extends ServiceClient {
|
|||
|
||||
protected URI MakeUri(String uri, HashMap<String, String> queryStrings) throws URISyntaxException {
|
||||
String newUri = queryStrings.keySet().stream()
|
||||
.map(key -> {
|
||||
try {
|
||||
return key + "=" + URLEncoder.encode(queryStrings.get(key), StandardCharsets.UTF_8.toString());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.collect(joining("&", uri.endsWith("?") ? uri : uri + "?", ""));
|
||||
.map(key -> {
|
||||
try {
|
||||
return key + "=" + URLEncoder.encode(queryStrings.get(key), StandardCharsets.UTF_8.toString());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.collect(joining("&", uri.endsWith("?") ? uri : uri + "?", ""));
|
||||
return new URI(newUri);
|
||||
|
||||
|
||||
|
@ -97,7 +101,7 @@ public class OAuthClient extends ServiceClient {
|
|||
* @param connectionName
|
||||
* @param magicCode
|
||||
* @param customHeaders
|
||||
* @return CompletableFuture< TokenResponse > on success; null otherwise.
|
||||
* @return CompletableFuture<TokenResponse> on success; null otherwise.
|
||||
*/
|
||||
public CompletableFuture<TokenResponse> GetUserTokenAsync(String userId, String connectionName, String magicCode, Map<String, ArrayList<String>> customHeaders) throws IllegalArgumentException {
|
||||
if (StringUtils.isEmpty(userId)) {
|
||||
|
@ -132,13 +136,13 @@ public class OAuthClient extends ServiceClient {
|
|||
|
||||
// Later: Use client in clientimpl?
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials))
|
||||
.build();
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials))
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.build();
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.build();
|
||||
|
||||
Response response = null;
|
||||
try {
|
||||
|
@ -158,7 +162,7 @@ public class OAuthClient extends ServiceClient {
|
|||
response.body().close();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -198,14 +202,14 @@ public class OAuthClient extends ServiceClient {
|
|||
|
||||
// Later: Use client in clientimpl?
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials))
|
||||
.build();
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials))
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.delete()
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.build();
|
||||
.delete()
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.build();
|
||||
|
||||
Response response = null;
|
||||
try {
|
||||
|
@ -218,7 +222,7 @@ public class OAuthClient extends ServiceClient {
|
|||
}
|
||||
return false;
|
||||
|
||||
});
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
|
||||
|
@ -238,15 +242,15 @@ public class OAuthClient extends ServiceClient {
|
|||
}
|
||||
final MicrosoftAppCredentials creds = (MicrosoftAppCredentials) this.client.restClient().credentials();
|
||||
TokenExchangeState tokenExchangeState = new TokenExchangeState()
|
||||
.withConnectionName(connectionName)
|
||||
.withConversation(new ConversationReference()
|
||||
.withActivityId(activity.id())
|
||||
.withBot(activity.recipient())
|
||||
.withChannelId(activity.channelId())
|
||||
.withConversation(activity.conversation())
|
||||
.withServiceUrl(activity.serviceUrl())
|
||||
.withUser(activity.from()))
|
||||
.withMsAppId((creds == null) ? null : creds.microsoftAppId());
|
||||
.withConnectionName(connectionName)
|
||||
.withConversation(new ConversationReference()
|
||||
.withActivityId(activity.id())
|
||||
.withBot(activity.recipient())
|
||||
.withChannelId(activity.channelId())
|
||||
.withConversation(activity.conversation())
|
||||
.withServiceUrl(activity.serviceUrl())
|
||||
.withUser(activity.from()))
|
||||
.withMsAppId((creds == null) ? null : creds.appId());
|
||||
|
||||
String serializedState = this.mapper.writeValueAsString(tokenExchangeState);
|
||||
|
||||
|
@ -266,13 +270,13 @@ public class OAuthClient extends ServiceClient {
|
|||
|
||||
// Later: Use client in clientimpl?
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(creds))
|
||||
.build();
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(creds))
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.build();
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.build();
|
||||
|
||||
Response response = null;
|
||||
try {
|
||||
|
@ -284,7 +288,7 @@ public class OAuthClient extends ServiceClient {
|
|||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -313,14 +317,14 @@ public class OAuthClient extends ServiceClient {
|
|||
|
||||
// Later: Use client in clientimpl?
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials))
|
||||
.build();
|
||||
.addInterceptor(new MicrosoftAppCredentialsInterceptor(appCredentials))
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.post(body)
|
||||
.build();
|
||||
.url(tokenUrl.toString())
|
||||
.header("User-Agent", UserAgent.value())
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
Response response = null;
|
||||
try {
|
||||
|
@ -336,6 +340,6 @@ public class OAuthClient extends ServiceClient {
|
|||
// Apparently swallow any results
|
||||
return;
|
||||
|
||||
});
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for
|
||||
// license information.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
/**
|
||||
* Configuration for OAuth client credential authentication.
|
||||
*/
|
||||
public class OAuthConfiguration {
|
||||
private String scope;
|
||||
private String authority;
|
||||
|
||||
public OAuthConfiguration(String authority, String scope) {
|
||||
this.authority = authority;
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets oAuth Authority for authentication.
|
||||
*
|
||||
* @param authority
|
||||
* @return This OAuthConfiguration object.
|
||||
*/
|
||||
public OAuthConfiguration withAuthority(String authority) {
|
||||
this.authority = authority;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets oAuth Authority for authentication.
|
||||
*
|
||||
* @return OAuth Authority for authentication.
|
||||
*/
|
||||
public String authority() {
|
||||
return authority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets oAuth scope for authentication.
|
||||
*
|
||||
* @param scope
|
||||
* @return This OAuthConfiguration object.
|
||||
*/
|
||||
public OAuthConfiguration withScope(String scope) {
|
||||
this.scope = scope;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets oAuth scope for authentication.
|
||||
*
|
||||
* @return OAuth scope for authentication.
|
||||
*/
|
||||
public String scope() {
|
||||
return scope;
|
||||
}
|
||||
}
|
|
@ -16,21 +16,21 @@ import java.net.URL;
|
|||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class OpenIdMetadata {
|
||||
private static final Logger LOGGER = Logger.getLogger( OpenIdMetadata.class.getName() );
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdMetadata.class);
|
||||
|
||||
private String url;
|
||||
private long lastUpdated;
|
||||
private JwkProvider cacheKeys;
|
||||
private ObjectMapper mapper;
|
||||
private String url;
|
||||
private long lastUpdated;
|
||||
private JwkProvider cacheKeys;
|
||||
private ObjectMapper mapper;
|
||||
|
||||
OpenIdMetadata(String url) {
|
||||
this.url = url;
|
||||
this.mapper = new ObjectMapper().findAndRegisterModules();
|
||||
}
|
||||
OpenIdMetadata(String url) {
|
||||
this.url = url;
|
||||
this.mapper = new ObjectMapper().findAndRegisterModules();
|
||||
}
|
||||
|
||||
public OpenIdMetadataKey getKey(String keyId) {
|
||||
// If keys are more than 5 days old, refresh them
|
||||
|
@ -52,7 +52,7 @@ class OpenIdMetadata {
|
|||
return IOUtils.toString(keysUrl);
|
||||
} catch (IOException e) {
|
||||
String errorDescription = String.format("Failed to load openID config: %s", e.getMessage());
|
||||
LOGGER.log(Level.WARNING, errorDescription);
|
||||
LOGGER.warn(errorDescription);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class OpenIdMetadata {
|
|||
return key;
|
||||
} catch (JwkException e) {
|
||||
String errorDescription = String.format("Failed to load keys: %s", e.getMessage());
|
||||
LOGGER.log(Level.WARNING, errorDescription);
|
||||
LOGGER.warn(errorDescription);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ResponseFuture implements Callback {
|
||||
public final CompletableFuture<Response> future = new CompletableFuture<Response>();
|
||||
public Call call;
|
||||
|
||||
public ResponseFuture(Call call) {
|
||||
this.call = call;
|
||||
}
|
||||
|
||||
@Override public void onFailure(Call call, IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
|
||||
@Override public void onResponse(Call call, Response response) throws IOException {
|
||||
future.complete(response);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class SimpleChannelProvider implements ChannelProvider {
|
||||
private String channelService;
|
||||
|
||||
/**
|
||||
* Creates a SimpleChannelProvider with no ChannelService which will use Public Azure.
|
||||
*/
|
||||
public SimpleChannelProvider() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SimpleChannelProvider with the specified ChannelService.
|
||||
*
|
||||
* @param channelService The ChannelService to use. Null or empty strings represent Public Azure,
|
||||
* the string 'https://botframework.us' represents US Government Azure, and
|
||||
* other values are for private channels.
|
||||
*/
|
||||
public SimpleChannelProvider(String channelService) {
|
||||
this.channelService = channelService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> getChannelService() {
|
||||
return CompletableFuture.completedFuture(channelService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGovernment() {
|
||||
return GovernmentAuthenticationConstants.CHANNELSERVICE.equalsIgnoreCase(channelService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPublicAzure() {
|
||||
return StringUtils.isEmpty(channelService);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,31 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* A simple implementation of the CredentialProvider interface.
|
||||
*/
|
||||
public class SimpleCredentialProvider implements CredentialProvider {
|
||||
private String appId;
|
||||
private String password;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes a new instance with empty credentials.
|
||||
*/
|
||||
public SimpleCredentialProvider() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance with the provided credentials.
|
||||
*
|
||||
* @param appId The app ID.
|
||||
* @param password The app password.
|
||||
*/
|
||||
public SimpleCredentialProvider(String appId, String password) {
|
||||
this.appId = appId;
|
||||
this.password = password;
|
||||
|
@ -18,6 +34,7 @@ public class SimpleCredentialProvider implements CredentialProvider {
|
|||
public String getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
this.appId = appId;
|
||||
}
|
||||
|
@ -25,24 +42,42 @@ public class SimpleCredentialProvider implements CredentialProvider {
|
|||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an app ID.
|
||||
*
|
||||
* @param appId The app ID to validate.
|
||||
* @return If the task is successful, the result is true if appId is valid for the controller; otherwise, false.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<Boolean> isValidAppIdAsync(String appId) {
|
||||
return CompletableFuture.completedFuture(appId == this.appId);
|
||||
return CompletableFuture.completedFuture(StringUtils.equals(appId, this.appId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the app password for a given bot app ID.
|
||||
*
|
||||
* @param appId The ID of the app to get the password for.
|
||||
* @return If the task is successful and the app ID is valid, the result
|
||||
* contains the password; otherwise, null.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<String> getAppPasswordAsync(String appId) {
|
||||
return CompletableFuture.completedFuture((appId == this.appId) ? this.password : null);
|
||||
return CompletableFuture.completedFuture(StringUtils.equals(appId, this.appId) ? this.password : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether bot authentication is disabled.
|
||||
*
|
||||
* @return A task that represents the work queued to execute If the task is successful and bot authentication
|
||||
* is disabled, the result is true; otherwise, false.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<Boolean> isAuthenticationDisabledAsync() {
|
||||
return CompletableFuture.completedFuture(StringUtils.isEmpty(this.appId));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -4,11 +4,8 @@
|
|||
package com.microsoft.bot.connector.authentication;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.microsoft.bot.connector.authentication.AuthenticationConstants.ToBotFromChannelTokenIssuer;
|
||||
|
||||
public class TokenValidationParameters {
|
||||
public boolean validateIssuer;
|
||||
public List<String> validIssuers;
|
||||
|
@ -32,33 +29,4 @@ public class TokenValidationParameters {
|
|||
this.clockSkew = clockSkew;
|
||||
this.requireSignedTokens = requireSignedTokens;
|
||||
}
|
||||
|
||||
static TokenValidationParameters toBotFromEmulatorTokenValidationParameters() {
|
||||
return new TokenValidationParameters() {{
|
||||
this.validateIssuer = true;
|
||||
this.validIssuers = new ArrayList<String>() {{
|
||||
add("https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/");
|
||||
add("https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0");
|
||||
add("https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/");
|
||||
add("https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0");
|
||||
}};
|
||||
this.validateAudience = false;
|
||||
this.validateLifetime = true;
|
||||
this.clockSkew = Duration.ofMinutes(5);
|
||||
this.requireSignedTokens = true;
|
||||
}};
|
||||
}
|
||||
|
||||
static TokenValidationParameters toBotFromChannelTokenValidationParameters() {
|
||||
return new TokenValidationParameters() {{
|
||||
this.validateIssuer = true;
|
||||
this.validIssuers = new ArrayList<String>() {{
|
||||
add(ToBotFromChannelTokenIssuer);
|
||||
}};
|
||||
this.validateAudience = false;
|
||||
this.validateLifetime = true;
|
||||
this.clockSkew = Duration.ofMinutes(5);
|
||||
this.requireSignedTokens = true;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for
|
||||
// license information.
|
||||
|
||||
/**
|
||||
* This package contains the implementation auth classes for ConnectorClient.
|
||||
*/
|
||||
package com.microsoft.bot.connector.authentication;
|
|
@ -1,2 +0,0 @@
|
|||
package com.microsoft.bot.connector.implementation;
|
||||
|
|
@ -1,25 +1,8 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for
|
||||
// license information.
|
||||
//
|
||||
// Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
// Changes may cause incorrect behavior and will be lost if the code is
|
||||
// regenerated.
|
||||
|
||||
/**
|
||||
* This package contains the classes for ConnectorClient.
|
||||
* The Bot Connector REST API allows your bot to send and receive messages to channels configured in the
|
||||
[Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses industry-standard REST
|
||||
and JSON over HTTPS.
|
||||
Client libraries for this REST API are available. See below for a list.
|
||||
Many bots will use both the Bot Connector REST API and the associated [Bot State REST API](/en-us/restapi/state). The
|
||||
Bot State REST API allows a bot to store and retrieve state associated with users and conversations.
|
||||
Authentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is
|
||||
described in detail in the [Connector Authentication](/en-us/restapi/authentication) document.
|
||||
# Client Libraries for the Bot Connector REST API
|
||||
* [Bot Builder for C#](/en-us/csharp/builder/sdkreference/)
|
||||
* [Bot Builder for Node.js](/en-us/node/builder/overview/)
|
||||
* Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json)
|
||||
© 2016 Microsoft.
|
||||
* This package contains the classes for Bot-Connector.
|
||||
*/
|
||||
package com.microsoft.bot.connector;
|
||||
|
|
|
@ -2,13 +2,9 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for
|
||||
* license information.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
*/
|
||||
|
||||
package com.microsoft.bot.connector.models;
|
||||
package com.microsoft.bot.connector.rest;
|
||||
|
||||
import com.microsoft.rest.RestException;import com.microsoft.bot.schema.models.ErrorResponse;
|
||||
import okhttp3.ResponseBody;
|
|
@ -2,19 +2,14 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for
|
||||
* license information.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
*/
|
||||
|
||||
package com.microsoft.bot.connector.implementation;
|
||||
package com.microsoft.bot.connector.rest;
|
||||
|
||||
import retrofit2.Retrofit;
|
||||
import com.microsoft.bot.connector.Attachments;
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.microsoft.bot.schema.models.AttachmentInfo;
|
||||
import com.microsoft.bot.connector.models.ErrorResponseException;
|
||||
import com.microsoft.rest.ServiceCallback;
|
||||
import com.microsoft.rest.ServiceFuture;
|
||||
import com.microsoft.rest.ServiceResponse;
|
||||
|
@ -34,11 +29,11 @@ import rx.Observable;
|
|||
* An instance of this class provides access to all the operations defined
|
||||
* in Attachments.
|
||||
*/
|
||||
public class AttachmentsImpl implements Attachments {
|
||||
public class RestAttachments implements Attachments {
|
||||
/** The Retrofit service to perform REST calls. */
|
||||
private AttachmentsService service;
|
||||
/** The service client containing this operation class. */
|
||||
private ConnectorClientImpl client;
|
||||
private RestConnectorClient client;
|
||||
|
||||
/**
|
||||
* Initializes an instance of AttachmentsImpl.
|
||||
|
@ -46,7 +41,7 @@ public class AttachmentsImpl implements Attachments {
|
|||
* @param retrofit the Retrofit instance built from a Retrofit Builder.
|
||||
* @param client the instance of the service client containing this operation class.
|
||||
*/
|
||||
public AttachmentsImpl(Retrofit retrofit, ConnectorClientImpl client) {
|
||||
RestAttachments(Retrofit retrofit, RestConnectorClient client) {
|
||||
this.service = retrofit.create(AttachmentsService.class);
|
||||
this.client = client;
|
||||
}
|
|
@ -2,10 +2,9 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for
|
||||
* license information.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.bot.connector.implementation;
|
||||
package com.microsoft.bot.connector.rest;
|
||||
|
||||
import com.microsoft.azure.AzureClient;
|
||||
import com.microsoft.azure.AzureResponseBuilder;
|
||||
|
@ -24,13 +23,28 @@ import java.io.InputStream;
|
|||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the ConnectorClientImpl class.
|
||||
* The Bot Connector REST API allows your bot to send and receive messages
|
||||
* to channels configured in the
|
||||
* [Bot Framework Developer Portal](https://dev.botframework.com). The
|
||||
* Connector service uses industry-standard REST
|
||||
* and JSON over HTTPS.
|
||||
*
|
||||
* Client libraries for this REST API are available. See below for a list.
|
||||
*
|
||||
* Many bots will use both the Bot Connector REST API and the associated
|
||||
* [Bot State REST API](/en-us/restapi/state). The
|
||||
* Bot State REST API allows a bot to store and retrieve state associated
|
||||
* with users and conversations.
|
||||
*
|
||||
* Authentication for both the Bot Connector and Bot State REST APIs is
|
||||
* accomplished with JWT Bearer tokens, and is
|
||||
* described in detail in the [Connector
|
||||
* Authentication](/en-us/restapi/authentication) document.
|
||||
*/
|
||||
public class ConnectorClientImpl extends AzureServiceClient implements ConnectorClient {
|
||||
public class RestConnectorClient extends AzureServiceClient implements ConnectorClient {
|
||||
/** the {@link AzureClient} used for long running operations. */
|
||||
private AzureClient azureClient;
|
||||
|
||||
|
||||
/**
|
||||
* Gets the {@link AzureClient} used for long running operations.
|
||||
* @return the azure client;
|
||||
|
@ -58,7 +72,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
* @param acceptLanguage the acceptLanguage value.
|
||||
* @return the service client itself
|
||||
*/
|
||||
public ConnectorClientImpl withAcceptLanguage(String acceptLanguage) {
|
||||
public RestConnectorClient withAcceptLanguage(String acceptLanguage) {
|
||||
this.acceptLanguage = acceptLanguage;
|
||||
return this;
|
||||
}
|
||||
|
@ -68,7 +82,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
* TODO: Use this.
|
||||
*/
|
||||
private RetryStrategy retryStrategy = null;
|
||||
public ConnectorClientImpl withRestRetryStrategy(RetryStrategy retryStrategy) {
|
||||
public RestConnectorClient withRestRetryStrategy(RetryStrategy retryStrategy) {
|
||||
this.retryStrategy = retryStrategy;
|
||||
return this;
|
||||
}
|
||||
|
@ -94,7 +108,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
* @param longRunningOperationRetryTimeout the longRunningOperationRetryTimeout value.
|
||||
* @return the service client itself
|
||||
*/
|
||||
public ConnectorClientImpl withLongRunningOperationRetryTimeout(int longRunningOperationRetryTimeout) {
|
||||
public RestConnectorClient withLongRunningOperationRetryTimeout(int longRunningOperationRetryTimeout) {
|
||||
this.longRunningOperationRetryTimeout = longRunningOperationRetryTimeout;
|
||||
return this;
|
||||
}
|
||||
|
@ -117,7 +131,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
* @param generateClientRequestId the generateClientRequestId value.
|
||||
* @return the service client itself
|
||||
*/
|
||||
public ConnectorClientImpl withGenerateClientRequestId(boolean generateClientRequestId) {
|
||||
public RestConnectorClient withGenerateClientRequestId(boolean generateClientRequestId) {
|
||||
this.generateClientRequestId = generateClientRequestId;
|
||||
return this;
|
||||
}
|
||||
|
@ -138,14 +152,14 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
/**
|
||||
* The Conversations object to access its operations.
|
||||
*/
|
||||
private ConversationsImpl conversations;
|
||||
private RestConversations conversations;
|
||||
|
||||
/**
|
||||
* Gets the Conversations object to access its operations.
|
||||
* @return the Conversations object.
|
||||
*/
|
||||
@Override
|
||||
public ConversationsImpl conversations() {
|
||||
public RestConversations conversations() {
|
||||
return this.conversations;
|
||||
}
|
||||
|
||||
|
@ -154,7 +168,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
*
|
||||
* @param credentials the management credentials for Azure
|
||||
*/
|
||||
public ConnectorClientImpl(ServiceClientCredentials credentials) {
|
||||
public RestConnectorClient(ServiceClientCredentials credentials) {
|
||||
this("https://api.botframework.com", credentials);
|
||||
}
|
||||
|
||||
|
@ -164,7 +178,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
* @param baseUrl the base URL of the host
|
||||
* @param credentials the management credentials for Azure
|
||||
*/
|
||||
public ConnectorClientImpl(String baseUrl, ServiceClientCredentials credentials) {
|
||||
public RestConnectorClient(String baseUrl, ServiceClientCredentials credentials) {
|
||||
super(baseUrl, credentials);
|
||||
initialize();
|
||||
}
|
||||
|
@ -174,7 +188,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
*
|
||||
* @param restClient the REST client to connect to Azure.
|
||||
*/
|
||||
public ConnectorClientImpl(RestClient restClient){
|
||||
public RestConnectorClient(RestClient restClient){
|
||||
super(restClient);
|
||||
initialize();
|
||||
}
|
||||
|
@ -183,8 +197,8 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
this.acceptLanguage = "en-US";
|
||||
this.longRunningOperationRetryTimeout = 30;
|
||||
this.generateClientRequestId = true;
|
||||
this.attachments = new AttachmentsImpl(restClient().retrofit(), this);
|
||||
this.conversations = new ConversationsImpl(restClient().retrofit(), this);
|
||||
this.attachments = new RestAttachments(restClient().retrofit(), this);
|
||||
this.conversations = new RestConversations(restClient().retrofit(), this);
|
||||
this.azureClient = new AzureClient(this);
|
||||
|
||||
|
||||
|
@ -192,7 +206,7 @@ public class ConnectorClientImpl extends AzureServiceClient implements Connector
|
|||
String build_version;
|
||||
final Properties properties = new Properties();
|
||||
try {
|
||||
InputStream propStream = ConnectorClientImpl.class.getClassLoader().getResourceAsStream("project.properties");
|
||||
InputStream propStream = RestConnectorClient.class.getClassLoader().getResourceAsStream("project.properties");
|
||||
properties.load(propStream);
|
||||
build_version = properties.getProperty("version");
|
||||
} catch (IOException e) {
|
|
@ -2,12 +2,9 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for
|
||||
* license information.
|
||||
*
|
||||
* NOT GENERATED.
|
||||
* This uses Java 8 CompletionStage for async processing instead of JavaRX/Guava
|
||||
*/
|
||||
|
||||
package com.microsoft.bot.connector.implementation;
|
||||
package com.microsoft.bot.connector.rest;
|
||||
|
||||
import retrofit2.Retrofit;
|
||||
import com.microsoft.bot.connector.Conversations;
|
||||
|
@ -19,7 +16,6 @@ import com.microsoft.bot.schema.models.ConversationParameters;
|
|||
import com.microsoft.bot.schema.models.ConversationResourceResponse;
|
||||
import com.microsoft.bot.schema.models.ConversationsResult;
|
||||
import com.microsoft.bot.schema.models.PagedMembersResult;
|
||||
import com.microsoft.bot.connector.models.ErrorResponseException;
|
||||
import com.microsoft.bot.schema.models.ResourceResponse;
|
||||
import com.microsoft.bot.schema.models.Transcript;
|
||||
import com.microsoft.rest.ServiceCallback;
|
||||
|
@ -49,11 +45,11 @@ import rx.Observable;
|
|||
* An instance of this class provides access to all the operations defined
|
||||
* in Conversations.
|
||||
*/
|
||||
public class ConversationsImpl implements Conversations {
|
||||
public class RestConversations implements Conversations {
|
||||
/** The Retrofit service to perform REST calls. */
|
||||
private ConversationsService service;
|
||||
/** The service client containing this operation class. */
|
||||
private ConnectorClientImpl client;
|
||||
private RestConnectorClient client;
|
||||
|
||||
/**
|
||||
* Initializes an instance of ConversationsImpl.
|
||||
|
@ -61,7 +57,7 @@ public class ConversationsImpl implements Conversations {
|
|||
* @param retrofit the Retrofit instance built from a Retrofit Builder.
|
||||
* @param client the instance of the service client containing this operation class.
|
||||
*/
|
||||
public ConversationsImpl(Retrofit retrofit, ConnectorClientImpl client) {
|
||||
RestConversations(Retrofit retrofit, RestConnectorClient client) {
|
||||
this.service = retrofit.create(ConversationsService.class);
|
||||
this.client = client;
|
||||
}
|
||||
|
@ -284,8 +280,8 @@ public class ConversationsImpl implements Conversations {
|
|||
});
|
||||
}
|
||||
|
||||
// FIXME: This isn't really a reasonable return value in this case.
|
||||
// I know what it said about converting Observable to CompletableFuture, but this particular
|
||||
// FIXME: This isn't really a reasonable return value in this case.
|
||||
// I know what it said about converting Observable to CompletableFuture, but this particular
|
||||
// conversion returns a result that changes the meaning. The response is not, and is never, a list.
|
||||
public CompletableFuture<List<ConversationResourceResponse>> CreateConversationAsync(ConversationParameters parameters) {
|
||||
CompletableFuture<List<ConversationResourceResponse>> future_result = completableFutureFromObservable(createConversationAsync(parameters));
|
||||
|
@ -719,7 +715,7 @@ public class ConversationsImpl implements Conversations {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DeleteConversationMemberFuture
|
||||
* Deletes a member from a converstion.
|
|
@ -22,4 +22,4 @@
|
|||
* Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json)
|
||||
© 2016 Microsoft.
|
||||
*/
|
||||
package com.microsoft.bot.connector.implementation;
|
||||
package com.microsoft.bot.connector.rest;
|
|
@ -1,13 +1,7 @@
|
|||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.schema.models.TokenResponse;
|
||||
import com.microsoft.rest.credentials.ServiceClientCredentials;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Response;
|
||||
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
|
||||
public class BotAccessTokenStub extends MicrosoftAppCredentials {
|
||||
private final String token;
|
||||
|
@ -26,8 +20,4 @@ public class BotAccessTokenStub extends MicrosoftAppCredentials {
|
|||
public void applyCredentialsFilter(OkHttpClient.Builder clientBuilder) {
|
||||
clientBuilder.interceptors().add(new TestBearerTokenInterceptor(this.token));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.authentication.*;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import okhttp3.Request;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class BotAuthenticatorTest {
|
||||
|
||||
private static final String AppId = "2cd87869-38a0-4182-9251-d056e8f0ac24";
|
||||
private static final String AppPassword = "2.30Vs3VQLKt974F";
|
||||
|
||||
@Test
|
||||
public void ConnectorAuthHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new CredentialProviderImpl(AppId, "");
|
||||
ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(header, credentials, "", "https://webchat.botframework.com/").get();
|
||||
|
||||
Assert.assertTrue(identity.isAuthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ConnectorAuthHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new CredentialProviderImpl("00000000-0000-0000-0000-000000000000", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get();
|
||||
} catch (AuthenticationException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ConnectorAuthHeaderBotWithNoCredentialsShouldNotValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
// token received and auth disabled
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new CredentialProviderImpl("", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get();
|
||||
} catch (AuthenticationException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EmptyHeaderBotWithNoCredentialsShouldThrow() throws ExecutionException, InterruptedException {
|
||||
String header = "";
|
||||
CredentialProvider credentials = new CredentialProviderImpl("", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get();
|
||||
} catch (IllegalArgumentException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("authHeader"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EmulatorMsaHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new CredentialProviderImpl(AppId, "");
|
||||
ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(header, credentials, "", "https://webchat.botframework.com/").get();
|
||||
|
||||
Assert.assertTrue(identity.isAuthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EmulatorMsaHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new CredentialProviderImpl("00000000-0000-0000-0000-000000000000", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(header, credentials, "", null).get();
|
||||
} catch (AuthenticationException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("Invalid AppId passed on token"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelMsaHeaderValidServiceUrlShouldBeTrusted() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new CredentialProviderImpl(AppId, "");
|
||||
JwtTokenValidation.authenticateRequest(
|
||||
new Activity().withServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"),
|
||||
header,
|
||||
credentials);
|
||||
|
||||
Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a valid Token and invalid service url; and ensures that Service url is NOT added to Trusted service url list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelMsaHeaderInvalidServiceUrlShouldNotBeTrusted() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new CredentialProviderImpl("7f74513e-6f96-4dbc-be9d-9a81fea22b88", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.authenticateRequest(
|
||||
new Activity().withServiceUrl("https://webchat.botframework.com/"),
|
||||
header,
|
||||
credentials);
|
||||
Assert.fail("Should have thrown AuthenticationException");
|
||||
} catch (AuthenticationException ex) {
|
||||
Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with no authentication header and makes sure the service URL is not added to the trusted list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelAuthenticationDisabledShouldBeAnonymous() throws ExecutionException, InterruptedException {
|
||||
String header = "";
|
||||
CredentialProvider credentials = new CredentialProviderImpl("", "");
|
||||
|
||||
ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(new Activity().withServiceUrl("https://webchat.botframework.com/"), header, credentials).get();
|
||||
Assert.assertEquals("anonymous", identity.getIssuer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with no authentication header and makes sure the service URL is not added to the trusted list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelAuthenticationDisabledServiceUrlShouldNotBeTrusted() throws ExecutionException, InterruptedException {
|
||||
String header = "";
|
||||
CredentialProvider credentials = new CredentialProviderImpl("", "");
|
||||
|
||||
ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(new Activity().withServiceUrl("https://webchat.botframework.com/"), header, credentials).get();
|
||||
Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/"));
|
||||
}
|
||||
|
||||
private static String getHeaderToken() throws IOException {
|
||||
Request request = new Request.Builder().url(AuthenticationConstants.ToChannelFromBotLoginUrl).build();
|
||||
return String.format("Bearer %s", new MicrosoftAppCredentials(AppId, AppPassword).getToken(request));
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.base.TestBase;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import com.microsoft.bot.schema.models.ChannelAccount;
|
||||
import com.microsoft.rest.RestClient;
|
||||
|
||||
public class BotConnectorTestBase extends TestBase {
|
||||
protected ConnectorClientImpl connector;
|
||||
protected ConnectorClient connector;
|
||||
protected ChannelAccount bot;
|
||||
protected ChannelAccount user;
|
||||
|
||||
|
@ -20,7 +20,7 @@ public class BotConnectorTestBase extends TestBase {
|
|||
|
||||
@Override
|
||||
protected void initializeClients(RestClient restClient, String botId, String userId) {
|
||||
connector = new ConnectorClientImpl(restClient);
|
||||
connector = new RestConnectorClient(restClient);
|
||||
bot = new ChannelAccount().withId(botId);
|
||||
user = new ChannelAccount().withId(userId);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.models.ErrorResponseException;
|
||||
import com.microsoft.bot.connector.rest.ErrorResponseException;
|
||||
import com.microsoft.bot.schema.models.*;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.EmulatorValidation;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class EmulatorValidationTests {
|
||||
@Test
|
||||
public void NoSchemeTokenIsNotFromEmulator() {
|
||||
Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("AbCdEf123456"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void OnePartTokenIsNotFromEmulator() {
|
||||
Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("Bearer AbCdEf123456"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void NoIssuerIsNotFromEmulator() {
|
||||
Assert.assertFalse(EmulatorValidation.isTokenFromEmulator("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXNzYWdlIjoiSldUIFJ1bGVzISIsImlhdCI6MTQ1OTQ0ODExOSwiZXhwIjoxNDU5NDU0NTE5fQ.-yIVBD5b73C75osbmwwshQNRC7frWUYrqaTjTpza2y4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ValidTokenSuccess() {
|
||||
String emToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCIsImtpZCI6ImllX3FXQ1hoWHh0MXpJRXN1NGM3YWNRVkduNCJ9.eyJhdWQiOiI5YzI4NmUyZi1lMDcwLTRhZjUtYTNmMS0zNTBkNjY2MjE0ZWQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwiaWF0IjoxNTY2NDIyMTY3LCJuYmYiOjE1NjY0MjIxNjcsImV4cCI6MTU2NjQyNjA2NywiYWlvIjoiNDJGZ1lKaDBoK0VmRDAvNnVWaUx6NHZuL25UK0RnQT0iLCJhcHBpZCI6IjljMjg2ZTJmLWUwNzAtNGFmNS1hM2YxLTM1MGQ2NjYyMTRlZCIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2Q2ZDQ5NDIwLWYzOWItNGRmNy1hMWRjLWQ1OWE5MzU4NzFkYi8iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJPUXNSLWExUlpFS2tJcG9seUNJUUFBIiwidmVyIjoiMS4wIn0.J9qHO11oZlrpDU3MJcTJe3ErUqj0kw-ZQioYKbkwZ7ZpAx5hl01BETts-LOaE14tImqYqM2K86ZyX5LuAp2snru9LJ4S6-cVZ1_lp_IY4r61UuUJRiVUzn25kRZEN-TFi8Aj1iyL-ueeNr52MM1Sr2UUH73fwrferH8_0qa1IYc7affhjlFEWxSte0SN7iT5WaYK32d_nsgzJdZiCMZJPCpG39U2FYnSI8q7vvYjNbp8wDJc46Q4Jdd3zXYRgHWRBGL_EEkzzk9IFpHN7WoVaqNtgMiA4Vf8bde3eAS5lBBtE5VZ0F6fG4Qeg6zjOAxPBZqvAASMpgyDlSQMknevOQ";
|
||||
Assert.assertTrue(EmulatorValidation.isTokenFromEmulator(emToken));
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.EndorsementsValidator;
|
||||
|
|
|
@ -1,18 +1,615 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.authentication.*;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class JwtTokenValidationTests {
|
||||
|
||||
private static final String APPID = "2cd87869-38a0-4182-9251-d056e8f0ac24";
|
||||
private static final String APPPASSWORD = "2.30Vs3VQLKt974F";
|
||||
|
||||
private static String getHeaderToken() throws ExecutionException, InterruptedException {
|
||||
return String.format("Bearer %s", new MicrosoftAppCredentials(APPID, APPPASSWORD).getToken().get().getAccessToken());
|
||||
}
|
||||
|
||||
private static String getGovHeaderToken() throws ExecutionException, InterruptedException {
|
||||
return String.format("Bearer %s", new MicrosoftGovernmentAppCredentials(APPID, APPPASSWORD).getToken().get().getAccessToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Connector_AuthHeader_CorrectAppIdAndServiceUrl_ShouldValidate()
|
||||
{
|
||||
// MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F");
|
||||
// String header = "Bearer " + credentials.
|
||||
// var credentials = new SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", string.Empty);
|
||||
// var result = await JwtTokenValidation.ValidateAuthHeader(header, credentials, new SimpleChannelProvider(), string.Empty, "https://webchat.botframework.com/", client);
|
||||
//
|
||||
// Assert.True(result.IsAuthenticated);
|
||||
public void ConnectorAuthHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(APPID, "");
|
||||
ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider(),
|
||||
"",
|
||||
"https://webchat.botframework.com/").join();
|
||||
|
||||
Assert.assertTrue(identity.isAuthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Connector_AuthHeader_CorrectAppIdAndServiceUrl_WithGovChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(
|
||||
APPID,
|
||||
APPPASSWORD,
|
||||
GovernmentAuthenticationConstants.CHANNELSERVICE
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ConnectorAuthHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new SimpleCredentialProvider("00000000-0000-0000-0000-000000000000", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider(),
|
||||
"",
|
||||
null).join();
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ConnectorAuthHeaderBotWithNoCredentialsShouldNotValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
// token received and auth disabled
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new SimpleCredentialProvider("", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider(),
|
||||
"",
|
||||
null).join();
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EmptyHeaderBotWithNoCredentialsShouldThrow() throws ExecutionException, InterruptedException {
|
||||
String header = "";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider("", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider(),
|
||||
"",
|
||||
null).join();
|
||||
Assert.fail("Should have thrown IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("authHeader"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EmulatorMsaHeaderCorrectAppIdAndServiceUrlShouldValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(APPID, "");
|
||||
ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider(),
|
||||
"",
|
||||
"https://webchat.botframework.com/").join();
|
||||
|
||||
Assert.assertTrue(identity.isAuthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EmulatorMsaHeaderBotAppIdDiffersShouldNotValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new SimpleCredentialProvider("00000000-0000-0000-0000-000000000000", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider(),
|
||||
"",
|
||||
null).join();
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Emulator_AuthHeader_CorrectAppIdAndServiceUrl_WithGovChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(
|
||||
"2cd87869-38a0-4182-9251-d056e8f0ac24", // emulator creds
|
||||
"2.30Vs3VQLKt974F",
|
||||
GovernmentAuthenticationConstants.CHANNELSERVICE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Emulator_AuthHeader_CorrectAppIdAndServiceUrl_WithPrivateChannelService_ShouldValidate() throws IOException, ExecutionException, InterruptedException {
|
||||
JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(
|
||||
"2cd87869-38a0-4182-9251-d056e8f0ac24", // emulator creds
|
||||
"2.30Vs3VQLKt974F",
|
||||
"TheChannel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelMsaHeaderValidServiceUrlShouldBeTrusted() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(APPID, "");
|
||||
JwtTokenValidation.authenticateRequest(
|
||||
new Activity().withServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"),
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider()).join();
|
||||
|
||||
Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with a valid Token and invalid service url; and ensures that Service url is NOT added to Trusted service url list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelMsaHeaderInvalidServiceUrlShouldNotBeTrusted() throws IOException, ExecutionException, InterruptedException {
|
||||
String header = getHeaderToken();
|
||||
CredentialProvider credentials = new SimpleCredentialProvider("7f74513e-6f96-4dbc-be9d-9a81fea22b88", "");
|
||||
|
||||
try {
|
||||
JwtTokenValidation.authenticateRequest(
|
||||
new Activity().withServiceUrl("https://webchat.botframework.com/"),
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider()).join();
|
||||
Assert.fail("Should have thrown AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with no authentication header and makes sure the service URL is not added to the trusted list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelAuthenticationDisabledShouldBeAnonymous() throws ExecutionException, InterruptedException {
|
||||
String header = "";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider("", "");
|
||||
|
||||
ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(
|
||||
new Activity().withServiceUrl("https://webchat.botframework.com/"),
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider()).join();
|
||||
Assert.assertEquals("anonymous", identity.getIssuer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ChannelNoHeaderAuthenticationEnabledShouldThrow() throws IOException, ExecutionException, InterruptedException {
|
||||
try {
|
||||
String header = "";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(APPID, APPPASSWORD);
|
||||
JwtTokenValidation.authenticateRequest(
|
||||
new Activity().withServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"),
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider()).join();
|
||||
Assert.fail("Should have thrown AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
|
||||
Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with no authentication header and makes sure the service URL is not added to the trusted list.
|
||||
*/
|
||||
@Test
|
||||
public void ChannelAuthenticationDisabledServiceUrlShouldNotBeTrusted() throws ExecutionException, InterruptedException {
|
||||
String header = "";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider("", "");
|
||||
|
||||
ClaimsIdentity identity = JwtTokenValidation.authenticateRequest(
|
||||
new Activity().withServiceUrl("https://webchat.botframework.com/"),
|
||||
header,
|
||||
credentials,
|
||||
new SimpleChannelProvider()).join();
|
||||
Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://webchat.botframework.com/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_Succeeds() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
} catch (CompletionException e) {
|
||||
Assert.fail("Should not have thrown " + e.getCause().getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_NoAuthentication_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(null, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_NoAudienceClaim_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_NoAudienceClaimValue_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, "");
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_WrongAudienceClaim_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, "abc");
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_NoServiceClaim_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_NoServiceClaimValue_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, "");
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void EnterpriseChannelValidation_WrongServiceClaim_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, null);
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, "other");
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
EnterpriseChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an AuthenticationException");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_Succeeds() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Should not have thrown " + e.getCause().getClass().getName() + ": " + e.getCause().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_NoAuthentication_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(null, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_NoAudienceClaim_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_NoAudienceClaimValue_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, "");
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_WrongAudienceClaim_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, "abc");
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_WrongAudienceClaimIssuer_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, serviceUrl);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity("https://wrongissuer.com", claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_NoServiceClaim_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_NoServiceClaimValue_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, "");
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelValidation_WrongServiceClaimValue_Fails() {
|
||||
String appId = "1234567890";
|
||||
String serviceUrl = "https://webchat.botframework.com/";
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, "");
|
||||
|
||||
Map<String, String> claims = new HashMap<String, String>() {{
|
||||
put(AuthenticationConstants.AUDIENCE_CLAIM, appId);
|
||||
put(AuthenticationConstants.SERVICE_URL_CLAIM, "other");
|
||||
}};
|
||||
ClaimsIdentity identity = new ClaimsIdentity(GovernmentAuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER, claims);
|
||||
|
||||
try {
|
||||
GovernmentChannelValidation.validateIdentity(identity, credentials, serviceUrl).join();
|
||||
Assert.fail("Should have thrown an Authorization exception");
|
||||
} catch (CompletionException e) {
|
||||
Assert.assertTrue(e.getCause() instanceof AuthenticationException);
|
||||
}
|
||||
}
|
||||
|
||||
private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(String appId, String pwd, String channelService) throws IOException, ExecutionException, InterruptedException {
|
||||
ChannelProvider channel = new SimpleChannelProvider(channelService);
|
||||
String header = channel.isGovernment() ? getGovHeaderToken() : getHeaderToken();
|
||||
|
||||
JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(header, appId, pwd, channel);
|
||||
}
|
||||
|
||||
private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(String header, String appId, String pwd, ChannelProvider channel) {
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, pwd);
|
||||
|
||||
try {
|
||||
ClaimsIdentity identity = JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
channel,
|
||||
"",
|
||||
"https://webchat.botframework.com/").join();
|
||||
|
||||
Assert.assertTrue(identity.isAuthenticated());
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Should not have thrown " + e.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void JwtTokenValidation_ValidateAuthHeader_WithChannelService_Throws(String header, String appId, String pwd, String channelService) throws ExecutionException, InterruptedException {
|
||||
CredentialProvider credentials = new SimpleCredentialProvider(appId, pwd);
|
||||
ChannelProvider channel = new SimpleChannelProvider(channelService);
|
||||
|
||||
try {
|
||||
JwtTokenValidation.validateAuthHeader(
|
||||
header,
|
||||
credentials,
|
||||
channel,
|
||||
"",
|
||||
"https://webchat.botframework.com/").join();
|
||||
Assert.fail("Should have thrown AuthenticationException");
|
||||
} catch (AuthenticationException e) {
|
||||
Assert.assertTrue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationResult;
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class MicrosoftAppCredentialsTests {
|
||||
@Test
|
||||
public void ValidUrlTrusted() {
|
||||
MicrosoftAppCredentials.trustServiceUrl("https://goodurl.com");
|
||||
Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://goodurl.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void InvalidUrlTrusted() {
|
||||
MicrosoftAppCredentials.trustServiceUrl("badurl");
|
||||
Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("badurl"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void TrustedUrlExpiration() throws InterruptedException {
|
||||
// There is a +5 minute window for an expired url
|
||||
MicrosoftAppCredentials.trustServiceUrl("https://goodurl.com", LocalDateTime.now().minusMinutes(6));
|
||||
Assert.assertFalse(MicrosoftAppCredentials.isTrustedServiceUrl("https://goodurl.com"));
|
||||
|
||||
MicrosoftAppCredentials.trustServiceUrl("https://goodurl.com", LocalDateTime.now().minusMinutes(4));
|
||||
Assert.assertTrue(MicrosoftAppCredentials.isTrustedServiceUrl("https://goodurl.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ValidateAuthEndpoint() {
|
||||
try {
|
||||
// In Java, about the only thing that can cause a MalformedURLException in a missing or unknown protocol.
|
||||
// At any rate, this should validate someone didn't mess up the oAuth Endpoint for the class.
|
||||
MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F");
|
||||
new URL(credentials.oAuthEndpoint());
|
||||
|
||||
credentials.setChannelAuthTenant("tenant.com");
|
||||
|
||||
MicrosoftAppCredentials credentialsWithTenant =
|
||||
new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F", "tenant.com");
|
||||
|
||||
} catch(MalformedURLException e) {
|
||||
Assert.fail("Should not have thrown MalformedURLException");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GetToken() throws InterruptedException, ExecutionException {
|
||||
MicrosoftAppCredentials credentials = new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F");
|
||||
AuthenticationResult token = credentials.getToken().get();
|
||||
Assert.assertFalse(StringUtils.isEmpty(token.getAccessToken()));
|
||||
}
|
||||
}
|
|
@ -1,33 +1,21 @@
|
|||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.aad.adal4j.ClientCredential;
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.connector.authentication.OAuthClient;
|
||||
import com.microsoft.bot.connector.base.TestBase;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.schema.models.TokenResponse;
|
||||
import com.microsoft.rest.RestClient;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
|
||||
|
||||
public class OAuthConnectorTest extends OAuthTestBase {
|
||||
|
||||
|
||||
private ConnectorClientImpl mockConnectorClient;
|
||||
private RestConnectorClient mockConnectorClient;
|
||||
private MicrosoftAppCredentials credentials;
|
||||
|
||||
public OAuthConnectorTest() throws IOException, ExecutionException, InterruptedException, URISyntaxException {
|
||||
|
|
|
@ -5,22 +5,17 @@ import com.microsoft.bot.connector.authentication.AuthenticationConstants;
|
|||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.connector.authentication.OAuthClient;
|
||||
import com.microsoft.bot.connector.base.TestBase;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import com.microsoft.bot.schema.models.ChannelAccount;
|
||||
import com.microsoft.rest.RestClient;
|
||||
import okhttp3.Request;
|
||||
import org.apache.commons.io.FileSystemUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
|
||||
|
||||
public class OAuthTestBase extends TestBase
|
||||
{
|
||||
|
@ -32,7 +27,7 @@ public class OAuthTestBase extends TestBase
|
|||
|
||||
|
||||
private String token;
|
||||
protected ConnectorClientImpl connector;
|
||||
protected RestConnectorClient connector;
|
||||
|
||||
private ChannelAccount bot;
|
||||
public ChannelAccount getBot() {
|
||||
|
@ -70,11 +65,11 @@ public class OAuthTestBase extends TestBase
|
|||
}
|
||||
}
|
||||
|
||||
this.connector = new ConnectorClientImpl(restClient);
|
||||
this.connector = new RestConnectorClient(restClient);
|
||||
if (this.clientId != null && this.clientSecret != null) {
|
||||
MicrosoftAppCredentials credentials = new MicrosoftAppCredentials(this.clientId, this.clientSecret);
|
||||
|
||||
this.token = credentials.getToken(new Request.Builder().build());
|
||||
this.token = credentials.getToken().get().getAccessToken();
|
||||
}
|
||||
else {
|
||||
this.token = null;
|
||||
|
@ -116,14 +111,14 @@ public class OAuthTestBase extends TestBase
|
|||
return CompletableFuture.runAsync(()->{
|
||||
OAuthClient oauthClient = null;
|
||||
try {
|
||||
oauthClient = new OAuthClient(this.connector, AuthenticationConstants.OAuthUrl);
|
||||
oauthClient = new OAuthClient(this.connector, AuthenticationConstants.OAUTH_URL);
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
doTest.apply(oauthClient);
|
||||
});
|
||||
}, ExecutorFactory.getExecutor());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants;
|
||||
import com.microsoft.bot.connector.authentication.SimpleChannelProvider;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SimpleChannelProviderTests {
|
||||
@Test
|
||||
public void PublicChannelProvider() {
|
||||
SimpleChannelProvider channel = new SimpleChannelProvider();
|
||||
Assert.assertTrue(channel.isPublicAzure());
|
||||
Assert.assertFalse(channel.isGovernment());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GovernmentChannelProvider() {
|
||||
SimpleChannelProvider channel = new SimpleChannelProvider(GovernmentAuthenticationConstants.CHANNELSERVICE);
|
||||
Assert.assertFalse(channel.isPublicAzure());
|
||||
Assert.assertTrue(channel.isGovernment());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void GetChannelService() {
|
||||
try {
|
||||
SimpleChannelProvider channel = new SimpleChannelProvider(GovernmentAuthenticationConstants.CHANNELSERVICE);
|
||||
String service = channel.getChannelService().join();
|
||||
Assert.assertEquals(service, GovernmentAuthenticationConstants.CHANNELSERVICE);
|
||||
} catch (Throwable t) {
|
||||
Assert.fail("Should not have thrown " + t.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.connector;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.SimpleCredentialProvider;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SimpleCredentialProviderTests {
|
||||
@Test
|
||||
public void ValidAppIdAsync() {
|
||||
SimpleCredentialProvider credentialProvider = new SimpleCredentialProvider("appid", "pwd");
|
||||
|
||||
Assert.assertTrue(credentialProvider.isValidAppIdAsync("appid").join());
|
||||
Assert.assertFalse(credentialProvider.isValidAppIdAsync("wrongappid").join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void AppPasswordAsync() {
|
||||
SimpleCredentialProvider credentialProvider = new SimpleCredentialProvider("appid", "pwd");
|
||||
|
||||
Assert.assertEquals(credentialProvider.getAppPasswordAsync("appid").join(), "pwd");
|
||||
Assert.assertNull(credentialProvider.getAppPasswordAsync("wrongappid").join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void AuthenticationDisabledAsync() {
|
||||
Assert.assertFalse(new SimpleCredentialProvider("appid", "pwd").isAuthenticationDisabledAsync().join());
|
||||
Assert.assertTrue(new SimpleCredentialProvider(null, null).isAuthenticationDisabledAsync().join());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.microsoft.bot</groupId>
|
||||
<artifactId>bot-java</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>bot-integration-core</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
|
||||
<name>${project.groupId}:${project.artifactId}</name>
|
||||
<description>Bot Framework Integration Core</description>
|
||||
<url>https://dev.botframework.com/</url>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>http://www.opensource.org/licenses/mit-license.php</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Bot Framework Development</name>
|
||||
<email></email>
|
||||
<organization>Microsoft</organization>
|
||||
<organizationUrl>https://dev.botframework.com/</organizationUrl>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:https://github.com/Microsoft/botbuilder-java</connection>
|
||||
<developerConnection>scm:git:https://github.com/Microsoft/botbuilder-java</developerConnection>
|
||||
<url>https://github.com/Microsoft/botbuilder-java</url>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.deploy.skip>false</maven.deploy.skip>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.bot</groupId>
|
||||
<artifactId>bot-schema</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.bot</groupId>
|
||||
<artifactId>bot-connector</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>MyGet</id>
|
||||
<url>${repo.url}</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>MyGet</id>
|
||||
<url>${repo.url}</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>build</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-pmd-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.eluder.coveralls</groupId>
|
||||
<artifactId>coveralls-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<repoToken>yourcoverallsprojectrepositorytoken</repoToken>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>cobertura-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<outputDirectory>../../cobertura-report/bot-connector</outputDirectory>
|
||||
<format>xml</format>
|
||||
<maxmem>256m</maxmem>
|
||||
<!-- aggregated reports for multi-module projects -->
|
||||
<aggregate>true</aggregate>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>publish</id>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
|
@ -0,0 +1,30 @@
|
|||
package com.microsoft.bot.integration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Provides access to properties defined in a Properties file located on the classpath.
|
||||
*/
|
||||
public class ClasspathPropertiesConfiguration implements Configuration {
|
||||
private Properties properties;
|
||||
|
||||
/**
|
||||
* Loads properties from the 'application.properties' file.
|
||||
* @throws IOException
|
||||
*/
|
||||
public ClasspathPropertiesConfiguration() {
|
||||
try {
|
||||
properties = new Properties();
|
||||
properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("application.properties"));
|
||||
}
|
||||
catch (IOException e) {
|
||||
(LoggerFactory.getLogger(ClasspathPropertiesConfiguration.class)).error("Unable to load properties", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getProperty(String key) {
|
||||
return properties.getProperty(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.integration;
|
||||
|
||||
/**
|
||||
* Provides read-only access to configuration properties.
|
||||
*/
|
||||
public interface Configuration {
|
||||
String getProperty(String key);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.integration;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.SimpleChannelProvider;
|
||||
|
||||
/**
|
||||
* Channel provider which uses Configuration to lookup the channel service property.
|
||||
*
|
||||
* This will populate the SimpleChannelProvider.ChannelService from a configuration entry with
|
||||
* the key of "ChannelService".
|
||||
*/
|
||||
public class ConfigurationChannelProvider extends SimpleChannelProvider {
|
||||
public ConfigurationChannelProvider(Configuration configuration) {
|
||||
super(configuration.getProperty("ChannelService"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.integration;
|
||||
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.connector.authentication.SimpleCredentialProvider;
|
||||
|
||||
/**
|
||||
* Credential provider which uses Configuration to lookup appId and password.
|
||||
*/
|
||||
public class ConfigurationCredentialProvider extends SimpleCredentialProvider {
|
||||
public ConfigurationCredentialProvider(Configuration configuration) {
|
||||
setAppId(configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPID));
|
||||
setPassword(configuration.getProperty(MicrosoftAppCredentials.MICROSOFTAPPPASSWORD));
|
||||
}
|
||||
}
|
20
pom.xml
20
pom.xml
|
@ -66,27 +66,27 @@
|
|||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-parameter-names</artifactId>
|
||||
<version>2.9.8</version>
|
||||
<version>2.9.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||
<version>2.9.8</version>
|
||||
<version>2.9.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>2.9.8</version>
|
||||
<version>2.9.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<version>3.8.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>jwks-rsa</artifactId>
|
||||
<version>0.3.0</version>
|
||||
<version>0.8.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
|
@ -101,7 +101,12 @@
|
|||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.4</version>
|
||||
<version>3.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.6</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
@ -130,6 +135,7 @@
|
|||
<module>libraries/bot-schema</module>
|
||||
<module>libraries/bot-builder</module>
|
||||
<module>libraries/bot-connector</module>
|
||||
<module>libraries/bot-integration-core</module>
|
||||
<module>samples/bot-connector-sample</module>
|
||||
<module>samples/servlet-echo</module>
|
||||
<module>samples/spring-echo</module>
|
||||
|
@ -183,7 +189,7 @@
|
|||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>${checkstyle.version}</version>
|
||||
<configuration>
|
||||
<!-- <configLocation>checkstyle.xml</configLocation>-->
|
||||
<configLocation>./etc/bot-checkstyle.xml</configLocation>
|
||||
<encoding>UTF-8</encoding>
|
||||
<consoleOutput>false</consoleOutput>
|
||||
<failsOnError>false</failsOnError>
|
||||
|
|
|
@ -6,11 +6,9 @@ package com.microsoft.bot.connector.sample;
|
|||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.authentication.CredentialProvider;
|
||||
import com.microsoft.bot.connector.authentication.CredentialProviderImpl;
|
||||
import com.microsoft.bot.connector.authentication.JwtTokenValidation;
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.ConnectorClient;
|
||||
import com.microsoft.bot.connector.authentication.*;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import com.microsoft.bot.schema.models.ActivityTypes;
|
||||
import com.microsoft.bot.schema.models.ResourceResponse;
|
||||
|
@ -30,7 +28,7 @@ public class App {
|
|||
private static String appPassword = ""; // <-- app password -->
|
||||
|
||||
public static void main( String[] args ) throws IOException {
|
||||
CredentialProvider credentialProvider = new CredentialProviderImpl(appId, appPassword);
|
||||
CredentialProvider credentialProvider = new SimpleCredentialProvider(appId, appPassword);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(3978), 0);
|
||||
server.createContext("/api/messages", new MessageHandle(credentialProvider));
|
||||
server.setExecutor(null);
|
||||
|
@ -55,7 +53,7 @@ public class App {
|
|||
Activity activity = getActivity(httpExchange);
|
||||
String authHeader = httpExchange.getRequestHeaders().getFirst("Authorization");
|
||||
try {
|
||||
JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider);
|
||||
JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider, new SimpleChannelProvider());
|
||||
|
||||
// send ack to user activity
|
||||
httpExchange.sendResponseHeaders(202, 0);
|
||||
|
@ -63,7 +61,7 @@ public class App {
|
|||
|
||||
if (activity.type().equals(ActivityTypes.MESSAGE)) {
|
||||
// reply activity with the same text
|
||||
ConnectorClientImpl connector = new ConnectorClientImpl(activity.serviceUrl(), this.credentials);
|
||||
ConnectorClient connector = new RestConnectorClient(activity.serviceUrl(), this.credentials);
|
||||
ResourceResponse response = connector.conversations().sendToConversation(activity.conversation().id(),
|
||||
new Activity()
|
||||
.withType(ActivityTypes.MESSAGE)
|
||||
|
|
|
@ -8,12 +8,10 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
|||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.authentication.ClaimsIdentity;
|
||||
import com.microsoft.bot.connector.authentication.CredentialProvider;
|
||||
import com.microsoft.bot.connector.authentication.CredentialProviderImpl;
|
||||
import com.microsoft.bot.connector.authentication.JwtTokenValidation;
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.ConnectorClient;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import com.microsoft.bot.connector.authentication.*;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import com.microsoft.bot.schema.models.ActivityTypes;
|
||||
import javax.servlet.*;
|
||||
|
@ -24,6 +22,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
@ -53,7 +52,7 @@ public class EchoServlet extends HttpServlet {
|
|||
String appId = p.getProperty("MicrosoftAppId");
|
||||
String appPassword = p.getProperty("MicrosoftAppPassword");
|
||||
|
||||
this.credentialProvider = new CredentialProviderImpl(appId, appPassword);
|
||||
this.credentialProvider = new SimpleCredentialProvider(appId, appPassword);
|
||||
this.credentials = new MicrosoftAppCredentials(appId, appPassword);
|
||||
}
|
||||
catch(IOException ioe){
|
||||
|
@ -67,11 +66,11 @@ public class EchoServlet extends HttpServlet {
|
|||
final Activity activity = getActivity(request);
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
|
||||
CompletableFuture<ClaimsIdentity> authenticateRequest = JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider);
|
||||
CompletableFuture<ClaimsIdentity> authenticateRequest = JwtTokenValidation.authenticateRequest(activity, authHeader, credentialProvider, new SimpleChannelProvider());
|
||||
authenticateRequest.thenRunAsync(() -> {
|
||||
if (activity.type().equals(ActivityTypes.MESSAGE)) {
|
||||
// reply activity with the same text
|
||||
ConnectorClientImpl connector = new ConnectorClientImpl(activity.serviceUrl(), this.credentials);
|
||||
ConnectorClient connector = new RestConnectorClient(activity.serviceUrl(), this.credentials);
|
||||
connector.conversations().sendToConversation(activity.conversation().id(),
|
||||
new Activity()
|
||||
.withType(ActivityTypes.MESSAGE)
|
||||
|
@ -80,13 +79,21 @@ public class EchoServlet extends HttpServlet {
|
|||
.withFrom(activity.recipient())
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (AuthenticationException ex) {
|
||||
response.setStatus(401);
|
||||
LOGGER.log(Level.WARNING, "Auth failed!", ex);
|
||||
}, ExecutorFactory.getExecutor()).join();
|
||||
|
||||
response.setStatus(200);
|
||||
} catch (CompletionException ex) {
|
||||
if (ex.getCause() instanceof AuthenticationException) {
|
||||
LOGGER.log(Level.WARNING, "Auth failed!", ex);
|
||||
response.setStatus(401);
|
||||
}
|
||||
else {
|
||||
LOGGER.log(Level.WARNING, "Execution failed", ex);
|
||||
response.setStatus(500);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
response.setStatus(500);
|
||||
LOGGER.log(Level.WARNING, "Execution failed", ex);
|
||||
response.setStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.sample.spring;
|
||||
import com.microsoft.bot.connector.ConnectorClient;
|
||||
import com.microsoft.bot.connector.ExecutorFactory;
|
||||
import com.microsoft.bot.connector.authentication.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
@ -15,16 +18,11 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
import javax.annotation.PostConstruct;
|
||||
|
||||
import com.microsoft.aad.adal4j.AuthenticationException;
|
||||
import com.microsoft.bot.connector.authentication.ClaimsIdentity;
|
||||
import com.microsoft.bot.connector.authentication.CredentialProvider;
|
||||
import com.microsoft.bot.connector.authentication.CredentialProviderImpl;
|
||||
import com.microsoft.bot.connector.authentication.JwtTokenValidation;
|
||||
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
|
||||
import com.microsoft.bot.connector.implementation.ConnectorClientImpl;
|
||||
import com.microsoft.bot.connector.rest.RestConnectorClient;
|
||||
import com.microsoft.bot.schema.models.Activity;
|
||||
import com.microsoft.bot.schema.models.ActivityTypes;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
/**
|
||||
* This is the controller that will receive incoming Channel Activity messages.
|
||||
|
@ -51,7 +49,7 @@ public class BotController {
|
|||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
_credentialProvider = new CredentialProviderImpl(appId, appPassword);
|
||||
_credentialProvider = new SimpleCredentialProvider(appId, appPassword);
|
||||
_credentials = new MicrosoftAppCredentials(appId, appPassword);
|
||||
}
|
||||
|
||||
|
@ -66,20 +64,25 @@ public class BotController {
|
|||
public ResponseEntity<Object> incoming(@RequestBody Activity activity,
|
||||
@RequestHeader(value = "Authorization", defaultValue = "") String authHeader) {
|
||||
try {
|
||||
CompletableFuture<ClaimsIdentity> authenticateRequest = JwtTokenValidation.authenticateRequest(activity, authHeader, _credentialProvider);
|
||||
authenticateRequest.thenRunAsync(() -> {
|
||||
if (activity.type().equals(ActivityTypes.MESSAGE)) {
|
||||
logger.info("Received: " + activity.text());
|
||||
JwtTokenValidation.authenticateRequest(activity, authHeader, _credentialProvider, new SimpleChannelProvider())
|
||||
.thenRunAsync(() -> {
|
||||
if (activity.type().equals(ActivityTypes.MESSAGE)) {
|
||||
logger.info("Received: " + activity.text());
|
||||
|
||||
// reply activity with the same text
|
||||
ConnectorClientImpl connector = new ConnectorClientImpl(activity.serviceUrl(), _credentials);
|
||||
connector.conversations().sendToConversation(activity.conversation().id(),
|
||||
new Activity().withType(ActivityTypes.MESSAGE).withText("Echo: " + activity.text())
|
||||
.withRecipient(activity.from()).withFrom(activity.recipient()));
|
||||
}
|
||||
});
|
||||
} catch (AuthenticationException ex) {
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
// reply activity with the same text
|
||||
ConnectorClient connector = new RestConnectorClient(activity.serviceUrl(), _credentials);
|
||||
connector.conversations().sendToConversation(activity.conversation().id(),
|
||||
new Activity().withType(ActivityTypes.MESSAGE).withText("Echo: " + activity.text())
|
||||
.withRecipient(activity.from()).withFrom(activity.recipient()));
|
||||
}
|
||||
}, ExecutorFactory.getExecutor()).join();
|
||||
} catch (CompletionException ex) {
|
||||
if (ex.getCause() instanceof AuthenticationException) {
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
else {
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
MicrosoftAppId=9c286e2f-e070-4af5-a3f1-350d666214ed
|
||||
MicrosoftAppPassword=botframework_secret_goes_here
|
||||
MicrosoftAppId=
|
||||
MicrosoftAppPassword=
|
||||
|
|
Загрузка…
Ссылка в новой задаче