Skip to content

Commit 920d5d2

Browse files
authored
Merge pull request #886 from AzureAD/avdunn/tenant-override-fix
Pass optional tenant override to internal silent call
2 parents 95b5efc + a1a394a commit 920d5d2

File tree

6 files changed

+248
-14
lines changed

6 files changed

+248
-14
lines changed

msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/OnBehalfOfIT.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ void acquireTokenWithOBO_Managed(String environment) throws Exception {
3939
new UserAssertion(accessToken)).build()).
4040
get();
4141

42-
assertNotNull(result);
43-
assertNotNull(result.accessToken());
42+
assertResultNotNull(result);
4443
}
4544

4645
@ParameterizedTest
@@ -63,8 +62,7 @@ void acquireTokenWithOBO_testCache(String environment) throws Exception {
6362
new UserAssertion(accessToken)).build()).
6463
get();
6564

66-
assertNotNull(result1);
67-
assertNotNull(result1.accessToken());
65+
assertResultNotNull(result1);
6866

6967
// Same scope and userAssertion, should return cached tokens
7068
IAuthenticationResult result2 =
@@ -82,8 +80,7 @@ void acquireTokenWithOBO_testCache(String environment) throws Exception {
8280
new UserAssertion(accessToken)).build()).
8381
get();
8482

85-
assertNotNull(result3);
86-
assertNotNull(result3.accessToken());
83+
assertResultNotNull(result3);
8784
assertNotEquals(result2.accessToken(), result3.accessToken());
8885

8986
// Scope 2, should return cached token
@@ -105,8 +102,7 @@ void acquireTokenWithOBO_testCache(String environment) throws Exception {
105102
.build()).
106103
get();
107104

108-
assertNotNull(result5);
109-
assertNotNull(result5.accessToken());
105+
assertResultNotNull(result5);
110106
assertNotEquals(result5.accessToken(), result4.accessToken());
111107
assertNotEquals(result5.accessToken(), result2.accessToken());
112108

@@ -121,13 +117,17 @@ void acquireTokenWithOBO_testCache(String environment) throws Exception {
121117
.build()).
122118
get();
123119

124-
assertNotNull(result6);
125-
assertNotNull(result6.accessToken());
120+
assertResultNotNull(result6);
126121
assertNotEquals(result6.accessToken(), result5.accessToken());
127122
assertNotEquals(result6.accessToken(), result4.accessToken());
128123
assertNotEquals(result6.accessToken(), result2.accessToken());
129124
}
130125

126+
private void assertResultNotNull(IAuthenticationResult result) {
127+
assertNotNull(result);
128+
assertNotNull(result.accessToken());
129+
}
130+
131131
private String getAccessToken() throws Exception {
132132

133133
LabUserProvider labUserProvider = LabUserProvider.getInstance();

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByClientCredentialSupplier.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ AuthenticationResult execute() throws Exception {
2626
SilentParameters parameters = SilentParameters
2727
.builder(this.clientCredentialRequest.parameters.scopes())
2828
.claims(this.clientCredentialRequest.parameters.claims())
29+
.tenant(this.clientCredentialRequest.parameters.tenant())
2930
.build();
3031

3132
RequestContext context = new RequestContext(

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByOnBehalfOfSupplier.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ AuthenticationResult execute() throws Exception {
2626
SilentParameters parameters = SilentParameters
2727
.builder(this.onBehalfOfRequest.parameters.scopes())
2828
.claims(this.onBehalfOfRequest.parameters.claims())
29+
.tenant(this.onBehalfOfRequest.parameters.tenant())
2930
.build();
3031

3132
RequestContext context = new RequestContext(

msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCredentialTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@
33

44
package com.microsoft.aad.msal4j;
55

6+
import java.util.Collections;
7+
import java.util.HashMap;
8+
69
import org.junit.jupiter.api.Test;
710
import org.junit.jupiter.api.TestInstance;
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
813
import static org.junit.jupiter.api.Assertions.assertThrows;
914
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
import static org.mockito.ArgumentMatchers.any;
16+
import static org.mockito.Mockito.mock;
17+
import static org.mockito.Mockito.times;
18+
import static org.mockito.Mockito.verify;
19+
import static org.mockito.Mockito.when;
1020

1121
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
1222
class ClientCredentialTest {
@@ -32,4 +42,69 @@ void testSecretNullAndEmpty() {
3242

3343
assertTrue(ex.getMessage().contains("clientSecret is null or empty"));
3444
}
45+
46+
@Test
47+
void OnBehalfOf_InternalCacheLookup_Success() throws Exception {
48+
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class);
49+
50+
when(httpClientMock.send(any(HttpRequest.class))).thenReturn(TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(new HashMap<>())));
51+
52+
ConfidentialClientApplication cca =
53+
ConfidentialClientApplication.builder("clientId", ClientCredentialFactory.createFromSecret("password"))
54+
.authority("https://login.microsoftonline.com/tenant/")
55+
.instanceDiscovery(false)
56+
.validateAuthority(false)
57+
.httpClient(httpClientMock)
58+
.build();
59+
60+
ClientCredentialParameters parameters = ClientCredentialParameters.builder(Collections.singleton("scopes")).build();
61+
62+
IAuthenticationResult result = cca.acquireToken(parameters).get();
63+
IAuthenticationResult result2 = cca.acquireToken(parameters).get();
64+
65+
//OBO flow should perform an internal cache lookup, so similar parameters should only cause one HTTP client call
66+
assertEquals(result.accessToken(), result2.accessToken());
67+
verify(httpClientMock, times(1)).send(any());
68+
}
69+
70+
@Test
71+
void OnBehalfOf_TenantOverride() throws Exception {
72+
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class);
73+
74+
ConfidentialClientApplication cca =
75+
ConfidentialClientApplication.builder("clientId", ClientCredentialFactory.createFromSecret("password"))
76+
.authority("https://login.microsoftonline.com/tenant")
77+
.instanceDiscovery(false)
78+
.validateAuthority(false)
79+
.httpClient(httpClientMock)
80+
.build();
81+
82+
HashMap<String, String> tokenResponseValues = new HashMap<>();
83+
tokenResponseValues.put("access_token", "accessTokenFirstCall");
84+
85+
when(httpClientMock.send(any(HttpRequest.class))).thenReturn(TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(tokenResponseValues)));
86+
ClientCredentialParameters parameters = ClientCredentialParameters.builder(Collections.singleton("scopes")).build();
87+
88+
//The two acquireToken calls have the same parameters...
89+
IAuthenticationResult resultAppLevelTenant = cca.acquireToken(parameters).get();
90+
IAuthenticationResult resultAppLevelTenantCached = cca.acquireToken(parameters).get();
91+
//...so only one token should be added to the cache, and the mocked HTTP client's "send" method should only have been called once
92+
assertEquals(1, cca.tokenCache.accessTokens.size());
93+
assertEquals(resultAppLevelTenant.accessToken(), resultAppLevelTenantCached.accessToken());
94+
verify(httpClientMock, times(1)).send(any());
95+
96+
tokenResponseValues.put("access_token", "accessTokenSecondCall");
97+
98+
when(httpClientMock.send(any(HttpRequest.class))).thenReturn(TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(tokenResponseValues)));
99+
parameters = ClientCredentialParameters.builder(Collections.singleton("scopes")).tenant("otherTenant").build();
100+
101+
//Overriding the tenant parameter in the request should lead to a new token call being made...
102+
IAuthenticationResult resultRequestLevelTenant = cca.acquireToken(parameters).get();
103+
IAuthenticationResult resultRequestLevelTenantCached = cca.acquireToken(parameters).get();
104+
//...which should be different from the original token, and thus the cache should have two tokens created from two HTTP calls
105+
assertEquals(2, cca.tokenCache.accessTokens.size());
106+
assertEquals(resultRequestLevelTenant.accessToken(), resultRequestLevelTenantCached.accessToken());
107+
assertNotEquals(resultAppLevelTenant.accessToken(), resultRequestLevelTenant.accessToken());
108+
verify(httpClientMock, times(2)).send(any());
109+
}
35110
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import java.util.Collections;
7+
import java.util.HashMap;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.ExtendWith;
11+
import org.mockito.junit.jupiter.MockitoExtension;
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
14+
import static org.mockito.ArgumentMatchers.any;
15+
import static org.mockito.Mockito.mock;
16+
import static org.mockito.Mockito.times;
17+
import static org.mockito.Mockito.verify;
18+
import static org.mockito.Mockito.when;
19+
20+
@ExtendWith(MockitoExtension.class)
21+
class OnBehalfOfTests {
22+
23+
@Test
24+
void OnBehalfOf_InternalCacheLookup_Success() throws Exception {
25+
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class);
26+
27+
when(httpClientMock.send(any(HttpRequest.class))).thenReturn(TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(new HashMap<>())));
28+
29+
ConfidentialClientApplication cca =
30+
ConfidentialClientApplication.builder("clientId", ClientCredentialFactory.createFromSecret("password"))
31+
.authority("https://login.microsoftonline.com/tenant/")
32+
.instanceDiscovery(false)
33+
.validateAuthority(false)
34+
.httpClient(httpClientMock)
35+
.build();
36+
37+
OnBehalfOfParameters parameters = OnBehalfOfParameters.builder(Collections.singleton("scopes"), new UserAssertion(TestHelper.signedAssertion)).build();
38+
39+
IAuthenticationResult result = cca.acquireToken(parameters).get();
40+
IAuthenticationResult result2 = cca.acquireToken(parameters).get();
41+
42+
//OBO flow should perform an internal cache lookup, so similar parameters should only cause one HTTP client call
43+
assertEquals(result.accessToken(), result2.accessToken());
44+
verify(httpClientMock, times(1)).send(any());
45+
}
46+
47+
@Test
48+
void OnBehalfOf_TenantOverride() throws Exception {
49+
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class);
50+
51+
ConfidentialClientApplication cca =
52+
ConfidentialClientApplication.builder("clientId", ClientCredentialFactory.createFromSecret("password"))
53+
.authority("https://login.microsoftonline.com/tenant")
54+
.instanceDiscovery(false)
55+
.validateAuthority(false)
56+
.httpClient(httpClientMock)
57+
.build();
58+
59+
HashMap<String, String> tokenResponseValues = new HashMap<>();
60+
tokenResponseValues.put("access_token", "accessTokenFirstCall");
61+
62+
when(httpClientMock.send(any(HttpRequest.class))).thenReturn(TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(tokenResponseValues)));
63+
OnBehalfOfParameters parameters = OnBehalfOfParameters.builder(Collections.singleton("scopes"), new UserAssertion(TestHelper.signedAssertion)).build();
64+
65+
//The two acquireToken calls have the same parameters...
66+
IAuthenticationResult resultAppLevelTenant = cca.acquireToken(parameters).get();
67+
IAuthenticationResult resultAppLevelTenantCached = cca.acquireToken(parameters).get();
68+
//...so only one token should be added to the cache, and the mocked HTTP client's "send" method should only have been called once
69+
assertEquals(1, cca.tokenCache.accessTokens.size());
70+
assertEquals(resultAppLevelTenant.accessToken(), resultAppLevelTenantCached.accessToken());
71+
verify(httpClientMock, times(1)).send(any());
72+
73+
tokenResponseValues.put("access_token", "accessTokenSecondCall");
74+
75+
when(httpClientMock.send(any(HttpRequest.class))).thenReturn(TestHelper.expectedResponse(200, TestHelper.getSuccessfulTokenResponse(tokenResponseValues)));
76+
parameters = OnBehalfOfParameters.builder(Collections.singleton("scopes"), new UserAssertion(TestHelper.signedAssertion)).tenant("otherTenant").build();
77+
78+
//Overriding the tenant parameter in the request should lead to a new token call being made...
79+
IAuthenticationResult resultRequestLevelTenant = cca.acquireToken(parameters).get();
80+
IAuthenticationResult resultRequestLevelTenantCached = cca.acquireToken(parameters).get();
81+
//...which should be different from the original token, and thus the cache should have two tokens created from two HTTP calls
82+
assertEquals(2, cca.tokenCache.accessTokens.size());
83+
assertEquals(resultRequestLevelTenant.accessToken(), resultRequestLevelTenantCached.accessToken());
84+
assertNotEquals(resultAppLevelTenant.accessToken(), resultRequestLevelTenant.accessToken());
85+
verify(httpClientMock, times(2)).send(any());
86+
}
87+
}

msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,38 @@
33

44
package com.microsoft.aad.msal4j;
55

6+
import com.nimbusds.jose.*;
7+
import com.nimbusds.jose.crypto.RSASSASigner;
8+
import com.nimbusds.jose.jwk.RSAKey;
9+
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
10+
611
import java.io.File;
712
import java.io.FileWriter;
813
import java.io.IOException;
914
import java.net.URISyntaxException;
1015
import java.nio.file.Files;
1116
import java.nio.file.Paths;
17+
import java.util.Collections;
18+
import java.util.HashMap;
19+
import java.util.List;
20+
import java.util.Map;
1221

1322
class TestHelper {
1423

15-
static String readResource(Class<?> classInstance, String resource) throws IOException, URISyntaxException {
16-
return new String(
17-
Files.readAllBytes(
18-
Paths.get(classInstance.getResource(resource).toURI())));
24+
//Signed JWT which should be enough to pass the parsing/validation in the library, useful if a unit test needs an
25+
// assertion but that is not the focus of the test
26+
static String signedAssertion = generateToken();
27+
private static final String successfulResponseFormat = "{\"access_token\":\"%s\",\"id_token\":\"%s\",\"refresh_token\":\"%s\"," +
28+
"\"client_id\":\"%s\",\"client_info\":\"%s\"," +
29+
"\"expires_on\": %d ,\"expires_in\": %d," +
30+
"\"token_type\":\"Bearer\"}";
31+
32+
static String readResource(Class<?> classInstance, String resource) {
33+
try {
34+
return new String(Files.readAllBytes(Paths.get(classInstance.getResource(resource).toURI())));
35+
} catch (IOException | URISyntaxException e) {
36+
throw new RuntimeException(e);
37+
}
1938
}
2039

2140
static void deleteFileContent(Class<?> classInstance, String resource)
@@ -27,4 +46,55 @@ static void deleteFileContent(Class<?> classInstance, String resource)
2746
fileWriter.write("");
2847
fileWriter.close();
2948
}
49+
50+
static String generateToken() {
51+
try {
52+
RSAKey rsaJWK = new RSAKeyGenerator(2048)
53+
.keyID("kid")
54+
.generate();
55+
JWSObject jwsObject = new JWSObject(
56+
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(),
57+
new Payload("payload"));
58+
59+
jwsObject.sign(new RSASSASigner(rsaJWK));
60+
61+
return jwsObject.serialize();
62+
} catch (JOSEException e) {
63+
throw new RuntimeException(e);
64+
}
65+
}
66+
67+
//Maps various values to the successfulResponseFormat string to create a valid token response
68+
static String getSuccessfulTokenResponse(HashMap<String, String> responseValues) {
69+
//Will default to expiring in one hour if expiry time values are not set
70+
long expiresIn = responseValues.containsKey("expires_in") ?
71+
Long.parseLong(responseValues.get("expires_in")) :
72+
3600;
73+
long expiresOn = responseValues.containsKey("expires_on")
74+
? Long.parseLong(responseValues.get("expires_0n")) :
75+
(System.currentTimeMillis() / 1000) + expiresIn;
76+
77+
return String.format(successfulResponseFormat,
78+
responseValues.getOrDefault("access_token", "access_token"),
79+
responseValues.getOrDefault("id_token", "id_token"),
80+
responseValues.getOrDefault("refresh_token", "refresh_token"),
81+
responseValues.getOrDefault("client_id", "client_id"),
82+
responseValues.getOrDefault("client_info", "client_info"),
83+
expiresOn,
84+
expiresIn
85+
);
86+
}
87+
88+
//Creates a valid HttpResponse that can be used when mocking HttpClient.send()
89+
static HttpResponse expectedResponse(int statusCode, String response) {
90+
Map<String, List<String>> headers = new HashMap<>();
91+
headers.put("Content-Type", Collections.singletonList("application/json"));
92+
93+
HttpResponse httpResponse = new HttpResponse();
94+
httpResponse.statusCode(statusCode);
95+
httpResponse.body(response);
96+
httpResponse.addHeaders(headers);
97+
98+
return httpResponse;
99+
}
30100
}

0 commit comments

Comments
 (0)