Skip to content

Commit 0b81ab6

Browse files
authored
Merge pull request #569 from AzureAD/SJAIN/instance-discovery-endpoint
expose instanceDiscovery flag
2 parents 1177c5b + dee401f commit 0b81ab6

File tree

4 files changed

+272
-9
lines changed

4 files changed

+272
-9
lines changed

msal4j-sdk/changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Version 1.13.4
22
=============
33
- regional endpoint updates
44
- fixed manifest
5+
- Expose instance discovery flag to perform instance discovery.
56

67
Version 1.13.3
78
=============
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package com.microsoft.aad.msal4j;
2+
3+
import org.easymock.Capture;
4+
import org.easymock.EasyMock;
5+
import org.powermock.api.easymock.PowerMock;
6+
import org.powermock.core.classloader.annotations.PrepareForTest;
7+
import org.testng.Assert;
8+
import org.testng.IObjectFactory;
9+
import org.testng.annotations.DataProvider;
10+
import org.testng.annotations.ObjectFactory;
11+
import org.testng.annotations.Test;
12+
13+
import java.net.URI;
14+
import java.util.Collections;
15+
import java.util.Date;
16+
import java.util.concurrent.CompletableFuture;
17+
18+
@PrepareForTest({HttpHelper.class, PublicClientApplication.class})
19+
public class InstanceDiscoveryTest {
20+
21+
private PublicClientApplication app;
22+
23+
@ObjectFactory
24+
public IObjectFactory getObjectFactory() {
25+
return new org.powermock.modules.testng.PowerMockObjectFactory();
26+
}
27+
28+
@DataProvider(name = "aadClouds")
29+
private static Object[][] getAadClouds(){
30+
return new Object[][] {{"https://login.microsoftonline.com/common"} , // #Known to Microsoft
31+
{"https://private.cloud/foo"}//Private Cloud
32+
};
33+
}
34+
35+
/**
36+
* when instance_discovery flag is set to true (by default), an instance_discovery is performed for authorityType = AAD
37+
*/
38+
@Test( dataProvider = "aadClouds")
39+
public void aadInstanceDiscoveryTrue(String authority) throws Exception{
40+
app = PowerMock.createPartialMock(PublicClientApplication.class,
41+
new String[]{"acquireTokenCommon"},
42+
PublicClientApplication.builder(TestConfiguration.AAD_CLIENT_ID)
43+
.authority(authority));
44+
45+
Capture<MsalRequest> capturedMsalRequest = Capture.newInstance();
46+
47+
PowerMock.expectPrivate(app, "acquireTokenCommon",
48+
EasyMock.capture(capturedMsalRequest), EasyMock.isA(AADAuthority.class)).andReturn(
49+
AuthenticationResult.builder().
50+
accessToken("accessToken").
51+
expiresOn(new Date().getTime() + 100).
52+
refreshToken("refreshToken").
53+
idToken("idToken").environment("environment").build());
54+
55+
PowerMock.mockStatic(HttpHelper.class);
56+
57+
HttpResponse instanceDiscoveryResponse = new HttpResponse();
58+
instanceDiscoveryResponse.statusCode(200);
59+
instanceDiscoveryResponse.body(TestConfiguration.INSTANCE_DISCOVERY_RESPONSE);
60+
61+
Capture<HttpRequest> capturedHttpRequest = Capture.newInstance();
62+
63+
EasyMock.expect(
64+
HttpHelper.executeHttpRequest(
65+
EasyMock.capture(capturedHttpRequest),
66+
EasyMock.isA(RequestContext.class),
67+
EasyMock.isA(ServiceBundle.class)))
68+
.andReturn(instanceDiscoveryResponse);
69+
70+
PowerMock.replay(HttpHelper.class, HttpResponse.class);
71+
72+
CompletableFuture<IAuthenticationResult> completableFuture = app.acquireToken(
73+
AuthorizationCodeParameters.builder
74+
("auth_code",
75+
new URI(TestConfiguration.AAD_DEFAULT_REDIRECT_URI))
76+
.scopes(Collections.singleton("default-scope"))
77+
.build());
78+
79+
completableFuture.get();
80+
Assert.assertEquals(capturedHttpRequest.getValues().size(),1);
81+
82+
}
83+
84+
/**
85+
* when instance_discovery flag is set to false, instance_discovery is not performed
86+
*/
87+
@Test (dataProvider = "aadClouds")
88+
public void aadInstanceDiscoveryFalse(String authority) throws Exception {
89+
90+
app = PowerMock.createPartialMock(PublicClientApplication.class,
91+
new String[]{"acquireTokenCommon"},
92+
PublicClientApplication.builder(TestConfiguration.AAD_CLIENT_ID)
93+
.authority(authority)
94+
.instanceDiscovery(false));
95+
96+
Capture<MsalRequest> capturedMsalRequest = Capture.newInstance();
97+
98+
PowerMock.expectPrivate(app, "acquireTokenCommon",
99+
EasyMock.capture(capturedMsalRequest), EasyMock.isA(AADAuthority.class)).andReturn(
100+
AuthenticationResult.builder().
101+
accessToken("accessToken").
102+
expiresOn(new Date().getTime() + 100).
103+
refreshToken("refreshToken").
104+
idToken("idToken").environment("environment").build());
105+
106+
PowerMock.mockStatic(HttpHelper.class);
107+
108+
HttpResponse instanceDiscoveryResponse = new HttpResponse();
109+
instanceDiscoveryResponse.statusCode(200);
110+
instanceDiscoveryResponse.body(TestConfiguration.INSTANCE_DISCOVERY_RESPONSE);
111+
112+
Capture<HttpRequest> capturedHttpRequest = Capture.newInstance();
113+
114+
EasyMock.expect(
115+
HttpHelper.executeHttpRequest(
116+
EasyMock.capture(capturedHttpRequest),
117+
EasyMock.isA(RequestContext.class),
118+
EasyMock.isA(ServiceBundle.class)))
119+
.andReturn(instanceDiscoveryResponse);
120+
121+
PowerMock.replay(HttpHelper.class, HttpResponse.class);
122+
123+
CompletableFuture<IAuthenticationResult> completableFuture = app.acquireToken(
124+
AuthorizationCodeParameters.builder
125+
("auth_code",
126+
new URI(TestConfiguration.AAD_DEFAULT_REDIRECT_URI))
127+
.scopes(Collections.singleton("default-scope"))
128+
.build());
129+
130+
completableFuture.get();
131+
Assert.assertEquals(capturedHttpRequest.getValues().size(),0);
132+
}
133+
134+
/**
135+
* when instance_discovery flag is set to true (by default), an instance_discovery is NOT performed for adfs.
136+
*/
137+
@Test
138+
public void adfsInstanceDiscoveryTrue() throws Exception{
139+
app = PowerMock.createPartialMock(PublicClientApplication.class,
140+
new String[]{"acquireTokenCommon"},
141+
PublicClientApplication.builder(TestConstants.ADFS_APP_ID)
142+
.authority("https://contoso.com/adfs")
143+
.instanceDiscovery(true));
144+
145+
Capture<MsalRequest> capturedMsalRequest = Capture.newInstance();
146+
147+
PowerMock.expectPrivate(app, "acquireTokenCommon",
148+
EasyMock.capture(capturedMsalRequest), EasyMock.isA(AADAuthority.class)).andReturn(
149+
AuthenticationResult.builder().
150+
accessToken("accessToken").
151+
expiresOn(new Date().getTime() + 100).
152+
refreshToken("refreshToken").
153+
idToken("idToken").environment("environment").build());
154+
155+
PowerMock.mockStatic(HttpHelper.class);
156+
157+
HttpResponse instanceDiscoveryResponse = new HttpResponse();
158+
instanceDiscoveryResponse.statusCode(200);
159+
instanceDiscoveryResponse.body(TestConfiguration.INSTANCE_DISCOVERY_RESPONSE);
160+
161+
Capture<HttpRequest> capturedHttpRequest = Capture.newInstance();
162+
163+
EasyMock.expect(
164+
HttpHelper.executeHttpRequest(
165+
EasyMock.capture(capturedHttpRequest),
166+
EasyMock.isA(RequestContext.class),
167+
EasyMock.isA(ServiceBundle.class)))
168+
.andReturn(instanceDiscoveryResponse);
169+
170+
PowerMock.replay(HttpHelper.class, HttpResponse.class);
171+
172+
CompletableFuture<IAuthenticationResult> completableFuture = app.acquireToken(
173+
AuthorizationCodeParameters.builder
174+
("auth_code",
175+
new URI(TestConfiguration.AAD_DEFAULT_REDIRECT_URI))
176+
.scopes(Collections.singleton("default-scope"))
177+
.build());
178+
179+
completableFuture.get();
180+
Assert.assertEquals(capturedHttpRequest.getValues().size(),0);
181+
182+
}
183+
184+
/**
185+
* when instance_discovery flag is set to true (by default), an instance_discovery is NOT performed for b2c.
186+
*/
187+
@Test
188+
public void b2cInstanceDiscoveryTrue() throws Exception{
189+
app = PowerMock.createPartialMock(PublicClientApplication.class,
190+
new String[]{"acquireTokenCommon"},
191+
PublicClientApplication.builder(TestConstants.ADFS_APP_ID)
192+
.b2cAuthority(TestConstants.B2C_MICROSOFTLOGIN_ROPC)
193+
.instanceDiscovery(true));
194+
195+
Capture<MsalRequest> capturedMsalRequest = Capture.newInstance();
196+
197+
PowerMock.expectPrivate(app, "acquireTokenCommon",
198+
EasyMock.capture(capturedMsalRequest), EasyMock.isA(AADAuthority.class)).andReturn(
199+
AuthenticationResult.builder().
200+
accessToken("accessToken").
201+
expiresOn(new Date().getTime() + 100).
202+
refreshToken("refreshToken").
203+
idToken("idToken").environment("environment").build());
204+
205+
PowerMock.mockStatic(HttpHelper.class);
206+
207+
HttpResponse instanceDiscoveryResponse = new HttpResponse();
208+
instanceDiscoveryResponse.statusCode(200);
209+
instanceDiscoveryResponse.body(TestConfiguration.INSTANCE_DISCOVERY_RESPONSE);
210+
211+
Capture<HttpRequest> capturedHttpRequest = Capture.newInstance();
212+
213+
EasyMock.expect(
214+
HttpHelper.executeHttpRequest(
215+
EasyMock.capture(capturedHttpRequest),
216+
EasyMock.isA(RequestContext.class),
217+
EasyMock.isA(ServiceBundle.class)))
218+
.andReturn(instanceDiscoveryResponse);
219+
220+
PowerMock.replay(HttpHelper.class, HttpResponse.class);
221+
222+
CompletableFuture<IAuthenticationResult> completableFuture = app.acquireToken(
223+
AuthorizationCodeParameters.builder
224+
("auth_code",
225+
new URI(TestConfiguration.AAD_DEFAULT_REDIRECT_URI))
226+
.scopes(Collections.singleton("default-scope"))
227+
.build());
228+
229+
completableFuture.get();
230+
Assert.assertEquals(capturedHttpRequest.getValues().size(),0);
231+
232+
}
233+
234+
235+
}

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ class AadInstanceDiscoveryProvider {
2626
private final static String SOVEREIGN_HOST_TEMPLATE_WITH_REGION = "{region}.{host}";
2727
private final static String REGION_NAME = "REGION_NAME";
2828
private final static int PORT_NOT_SET = -1;
29+
2930
// For information of the current api-version refer: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service#versioning
30-
private final static String DEFAULT_API_VERSION = "2020-06-01";
31-
private final static String IMDS_ENDPOINT = "https://169.254.169.254/metadata/instance/compute/location?" + DEFAULT_API_VERSION + "&format=text";
31+
private static final String DEFAULT_API_VERSION = "2020-06-01";
32+
private static final String IMDS_ENDPOINT = "https://169.254.169.254/metadata/instance/compute/location?" + DEFAULT_API_VERSION + "&format=text";
3233

33-
final static TreeSet<String> TRUSTED_HOSTS_SET = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
34-
final static TreeSet<String> TRUSTED_SOVEREIGN_HOSTS_SET = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
34+
static final TreeSet<String> TRUSTED_HOSTS_SET = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
35+
static final TreeSet<String> TRUSTED_SOVEREIGN_HOSTS_SET = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
3536

36-
private final static Logger log = LoggerFactory.getLogger(HttpHelper.class);
37+
private static final Logger log = LoggerFactory.getLogger(AadInstanceDiscoveryProvider.class);
3738

3839
static ConcurrentHashMap<String, InstanceDiscoveryMetadataEntry> cache = new ConcurrentHashMap<>();
3940

@@ -67,10 +68,9 @@ static InstanceDiscoveryMetadataEntry getMetadataEntry(URL authorityUrl,
6768

6869
//If region autodetection is enabled and a specific region not already set,
6970
// set the application's region to the discovered region so that future requests can skip the IMDS endpoint call
70-
if (msalRequest.application().azureRegion() == null && msalRequest.application().autoDetectRegion()) {
71-
if (detectedRegion != null) {
71+
if (null == msalRequest.application().azureRegion() && msalRequest.application().autoDetectRegion()
72+
&& null != detectedRegion) {
7273
msalRequest.application().azureRegion = detectedRegion;
73-
}
7474
}
7575
cacheRegionInstanceMetadata(authorityUrl.getHost(), msalRequest.application().azureRegion());
7676
serviceBundle.getServerSideTelemetry().getCurrentRequest().regionOutcome(
@@ -80,7 +80,16 @@ static InstanceDiscoveryMetadataEntry getMetadataEntry(URL authorityUrl,
8080
InstanceDiscoveryMetadataEntry result = cache.get(host);
8181

8282
if (result == null) {
83-
doInstanceDiscoveryAndCache(authorityUrl, validateAuthority, msalRequest, serviceBundle);
83+
if(msalRequest.application().instanceDiscovery()){
84+
doInstanceDiscoveryAndCache(authorityUrl, validateAuthority, msalRequest, serviceBundle);
85+
} else {
86+
// instanceDiscovery flag is set to False. Do not perform instanceDiscovery.
87+
return InstanceDiscoveryMetadataEntry.builder().
88+
preferredCache(host).
89+
preferredNetwork(host).
90+
aliases(Collections.singleton(host)).
91+
build();
92+
}
8493
}
8594

8695
return cache.get(host);

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public abstract class AbstractClientApplicationBase implements IClientApplicatio
104104
@Getter
105105
protected String azureRegion;
106106

107+
@Accessors(fluent = true)
108+
@Getter
109+
private boolean instanceDiscovery;
110+
107111
@Override
108112
public CompletableFuture<IAuthenticationResult> acquireToken(AuthorizationCodeParameters parameters) {
109113

@@ -325,6 +329,7 @@ public abstract static class Builder<T extends Builder<T>> {
325329
private String azureRegion;
326330
private Integer connectTimeoutForDefaultHttpClient;
327331
private Integer readTimeoutForDefaultHttpClient;
332+
private boolean instanceDiscovery = true;
328333

329334
/**
330335
* Constructor to create instance of Builder of client application
@@ -643,6 +648,18 @@ public T azureRegion(String val) {
643648
return self();
644649
}
645650

651+
/** Historically, MSAL would connect to a central endpoint located at
652+
``https://login.microsoftonline.com`` to acquire some metadata, especially when using an unfamiliar authority.
653+
This behavior is known as Instance Discovery.
654+
This parameter defaults to true, which enables the Instance Discovery.
655+
If you do not know some authorities beforehand,
656+
yet still want MSAL to accept any authority that you will provide,
657+
you can use a ``False`` to unconditionally disable Instance Discovery. */
658+
public T instanceDiscovery(boolean val) {
659+
instanceDiscovery = val;
660+
return self();
661+
}
662+
646663
abstract AbstractClientApplicationBase build();
647664
}
648665

@@ -671,6 +688,7 @@ public T azureRegion(String val) {
671688
clientCapabilities = builder.clientCapabilities;
672689
autoDetectRegion = builder.autoDetectRegion;
673690
azureRegion = builder.azureRegion;
691+
instanceDiscovery = builder.instanceDiscovery;
674692

675693
if (aadAadInstanceDiscoveryResponse != null) {
676694
AadInstanceDiscoveryProvider.cacheInstanceDiscoveryMetadata(

0 commit comments

Comments
 (0)