Skip to content
This repository was archived by the owner on Dec 4, 2023. It is now read-only.

Commit af273cb

Browse files
authored
Implemented remaining SkillValidation methods and tests (#972)
1 parent c057023 commit af273cb

File tree

8 files changed

+343
-21
lines changed

8 files changed

+343
-21
lines changed

libraries/bot-connector/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
<groupId>junit</groupId>
5050
<artifactId>junit</artifactId>
5151
</dependency>
52+
<dependency>
53+
<groupId>org.mockito</groupId>
54+
<artifactId>mockito-core</artifactId>
55+
</dependency>
5256

5357
<!-- to support ms rest consumption -->
5458
<dependency>

libraries/bot-connector/src/main/java/com/microsoft/bot/connector/Async.java

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package com.microsoft.bot.connector;
55

66
import java.util.concurrent.CompletableFuture;
7-
import java.util.concurrent.CompletionException;
87

98
/**
109
* Asyc and CompletableFuture helpers methods.
@@ -14,23 +13,6 @@ private Async() {
1413

1514
}
1615

17-
/**
18-
* Executes a block and throws a completion exception if needed.
19-
*
20-
* @param supplier The block to execute.
21-
* @param <T> The type of the return value.
22-
* @return The return value.
23-
*/
24-
public static <T> T tryThrow(ThrowSupplier<T> supplier) {
25-
try {
26-
return supplier.get();
27-
} catch (CompletionException ce) {
28-
throw ce;
29-
} catch (Throwable t) {
30-
throw new CompletionException(t);
31-
}
32-
}
33-
3416
/**
3517
* Executes a block and returns a CompletableFuture with either the return
3618
* value or the exception (completeExceptionally).

libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConstants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ private AuthenticationConstants() {
146146
*/
147147
public static final String ANONYMOUS_SKILL_APPID = "AnonymousSkill";
148148

149+
/**
150+
* Indicates anonymous (no app Id and password were provided).
151+
*/
152+
public static final String ANONYMOUS_AUTH_TYPE = "anonymous";
153+
149154
/**
150155
* The default clock skew in minutes.
151156
*/

libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsIdentity.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
public class ClaimsIdentity {
1515
private String issuer;
16+
private String type;
1617
private Map<String, String> claims;
1718

1819
private ClaimsIdentity() {
@@ -35,8 +36,20 @@ public ClaimsIdentity(String withAuthIssuer) {
3536
* @param withClaims A Map of claims.
3637
*/
3738
public ClaimsIdentity(String withAuthIssuer, Map<String, String> withClaims) {
38-
this.issuer = withAuthIssuer;
39-
this.claims = withClaims;
39+
this(withAuthIssuer, null, withClaims);
40+
}
41+
42+
/**
43+
* Manually construct with issuer and claims.
44+
*
45+
* @param withAuthIssuer The auth issuer.
46+
* @param withType The auth type.
47+
* @param withClaims A Map of claims.
48+
*/
49+
public ClaimsIdentity(String withAuthIssuer, String withType, Map<String, String> withClaims) {
50+
issuer = withAuthIssuer;
51+
type = withType;
52+
claims = withClaims;
4053
}
4154

4255
/**
@@ -50,6 +63,7 @@ public ClaimsIdentity(DecodedJWT jwt) {
5063
jwt.getClaims().forEach((k, v) -> claims.put(k, v.asString()));
5164
}
5265
issuer = jwt.getIssuer();
66+
type = jwt.getType();
5367
}
5468

5569
/**
@@ -78,4 +92,13 @@ public Map<String, String> claims() {
7892
public String getIssuer() {
7993
return issuer;
8094
}
95+
96+
/**
97+
* The type.
98+
*
99+
* @return The type.
100+
*/
101+
public String getType() {
102+
return type;
103+
}
81104
}

libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,34 @@ public static String getAppIdFromClaims(Map<String, String> claims) throws Illeg
202202

203203
return appId;
204204
}
205+
206+
/**
207+
* Internal helper to check if the token has the shape we expect "Bearer [big long string]".
208+
*
209+
* @param authHeader >A string containing the token header.
210+
* @return True if the token is valid, false if not.
211+
*/
212+
public static boolean isValidTokenFormat(String authHeader) {
213+
if (StringUtils.isEmpty(authHeader)) {
214+
// No token, not valid.
215+
return false;
216+
}
217+
218+
String[] parts = authHeader.split(" ");
219+
if (parts.length != 2) {
220+
// Tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not a valid token
221+
return false;
222+
}
223+
224+
// We now have an array that should be:
225+
// [0] = "Bearer"
226+
// [1] = "[Big Long String]"
227+
String authScheme = parts[0];
228+
if (!StringUtils.equals(authScheme, "Bearer")) {
229+
// The scheme MUST be "Bearer"
230+
return false;
231+
}
232+
233+
return true;
234+
}
205235
}

libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33

44
package com.microsoft.bot.connector.authentication;
55

6+
import com.auth0.jwt.JWT;
7+
import com.microsoft.bot.connector.Async;
68
import java.time.Duration;
9+
import java.util.HashMap;
710
import java.util.Map;
811
import java.util.Optional;
12+
import java.util.concurrent.CompletableFuture;
913
import java.util.stream.Collectors;
1014
import java.util.stream.Stream;
1115

@@ -97,7 +101,104 @@ public static Boolean isSkillClaim(Map<String, String> claims) {
97101

98102
// Skill claims must contain and app ID and the AppID must be different than the
99103
// audience.
100-
return appId != audience.get().getValue();
104+
return !StringUtils.equals(appId, audience.get().getValue());
101105
}
102106

107+
/**
108+
* Determines if a given Auth header is from from a skill to bot or bot to skill request.
109+
*
110+
* @param authHeader Bearer Token, in the "Bearer [Long String]" Format.
111+
* @return True, if the token was issued for a skill to bot communication. Otherwise, false.
112+
*/
113+
public static boolean isSkillToken(String authHeader) {
114+
if (!JwtTokenValidation.isValidTokenFormat(authHeader)) {
115+
return false;
116+
}
117+
118+
// We know is a valid token, split it and work with it:
119+
// [0] = "Bearer"
120+
// [1] = "[Big Long String]"
121+
String bearerToken = authHeader.split(" ")[1];
122+
123+
// Parse token
124+
ClaimsIdentity identity = new ClaimsIdentity(JWT.decode(bearerToken));
125+
126+
return isSkillClaim(identity.claims());
127+
}
128+
129+
/**
130+
* Helper to validate a skills ClaimsIdentity.
131+
*
132+
* @param identity The ClaimsIdentity to validate.
133+
* @param credentials The CredentialProvider.
134+
* @return Nothing if success, otherwise a CompletionException
135+
*/
136+
public static CompletableFuture<Void> validateIdentity(
137+
ClaimsIdentity identity,
138+
CredentialProvider credentials
139+
) {
140+
if (identity == null) {
141+
// No valid identity. Not Authorized.
142+
return Async.completeExceptionally(new AuthenticationException("Invalid Identity"));
143+
}
144+
145+
if (!identity.isAuthenticated()) {
146+
// The token is in some way invalid. Not Authorized.
147+
return Async.completeExceptionally(new AuthenticationException("Token Not Authenticated"));
148+
}
149+
150+
Optional<Map.Entry<String, String>> versionClaim = identity.claims().entrySet().stream()
151+
.filter(item -> StringUtils.equals(AuthenticationConstants.VERSION_CLAIM, item.getKey()))
152+
.findFirst();
153+
if (!versionClaim.isPresent()) {
154+
// No version claim
155+
return Async.completeExceptionally(
156+
new AuthenticationException(
157+
AuthenticationConstants.VERSION_CLAIM + " claim is required on skill Tokens."
158+
)
159+
);
160+
}
161+
162+
// Look for the "aud" claim, but only if issued from the Bot Framework
163+
Optional<Map.Entry<String, String>> audienceClaim = identity.claims().entrySet().stream()
164+
.filter(item -> StringUtils.equals(AuthenticationConstants.AUDIENCE_CLAIM, item.getKey()))
165+
.findFirst();
166+
if (!audienceClaim.isPresent() || StringUtils.isEmpty(audienceClaim.get().getValue())) {
167+
// Claim is not present or doesn't have a value. Not Authorized.
168+
return Async.completeExceptionally(
169+
new AuthenticationException(
170+
AuthenticationConstants.AUDIENCE_CLAIM + " claim is required on skill Tokens."
171+
)
172+
);
173+
}
174+
175+
String appId = JwtTokenValidation.getAppIdFromClaims(identity.claims());
176+
if (StringUtils.isEmpty(appId)) {
177+
return Async.completeExceptionally(new AuthenticationException("Invalid appId."));
178+
}
179+
180+
return credentials.isValidAppId(audienceClaim.get().getValue())
181+
.thenApply(isValid -> {
182+
if (!isValid) {
183+
throw new AuthenticationException("Invalid audience.");
184+
}
185+
return null;
186+
});
187+
}
188+
189+
/**
190+
* Creates a ClaimsIdentity for an anonymous (unauthenticated) skill.
191+
*
192+
* @return A ClaimsIdentity instance with authentication type set to
193+
* AuthenticationConstants.AnonymousAuthType and a reserved
194+
* AuthenticationConstants.AnonymousSkillAppId claim.
195+
*/
196+
public static ClaimsIdentity createAnonymousSkillClaim() {
197+
Map<String, String> claims = new HashMap<>();
198+
claims.put(AuthenticationConstants.APPID_CLAIM, AuthenticationConstants.ANONYMOUS_SKILL_APPID);
199+
return new ClaimsIdentity(
200+
AuthenticationConstants.ANONYMOUS_AUTH_TYPE,
201+
AuthenticationConstants.ANONYMOUS_AUTH_TYPE,
202+
claims);
203+
}
103204
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.microsoft.bot.connector;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
import java.util.concurrent.CompletionException;
5+
import org.junit.Assert;
6+
import org.junit.Test;
7+
8+
public class AsyncTests {
9+
@Test()
10+
public void AsyncTryCompletionShouldCompleteExceptionally() {
11+
CompletableFuture<Void> result = Async.tryCompletable(() -> {
12+
throw new IllegalArgumentException("test");
13+
});
14+
15+
Assert.assertTrue(result.isCompletedExceptionally());
16+
}
17+
18+
@Test
19+
public void AsyncTryCompletionShouldComplete() {
20+
CompletableFuture<Boolean> result = Async.tryCompletable(() -> CompletableFuture.completedFuture(true));
21+
Assert.assertTrue(result.join());
22+
}
23+
24+
@Test
25+
public void AsyncWrapBlockShouldCompleteExceptionally() {
26+
CompletableFuture<Void> result = Async.wrapBlock(() -> {
27+
throw new IllegalArgumentException("test");
28+
});
29+
30+
Assert.assertTrue(result.isCompletedExceptionally());
31+
}
32+
33+
@Test
34+
public void AsyncWrapBlockShouldComplete() {
35+
CompletableFuture<Boolean> result = Async.wrapBlock(() -> true);
36+
Assert.assertTrue(result.join());
37+
}
38+
}

0 commit comments

Comments
 (0)