|
39 | 39 | import com.microsoft.identity.common.adal.internal.AuthenticationConstants; |
40 | 40 | import com.microsoft.identity.common.java.exception.ClientException; |
41 | 41 | import com.microsoft.identity.common.internal.mocks.MockCommonFlightsManager; |
| 42 | +import com.microsoft.identity.common.internal.ui.webview.challengehandlers.ReAttachPrtHeaderHandler; |
42 | 43 | import com.microsoft.identity.common.internal.ui.webview.challengehandlers.SwitchBrowserRequestHandler; |
43 | 44 | import com.microsoft.identity.common.java.flighting.CommonFlight; |
44 | 45 | import com.microsoft.identity.common.java.flighting.CommonFlightsManager; |
|
57 | 58 |
|
58 | 59 | import java.util.HashMap; |
59 | 60 |
|
| 61 | +import io.opentelemetry.api.trace.Span; |
| 62 | + |
60 | 63 | /** |
61 | 64 | * Tests for eSTS cloud host detection and PRT header re-attachment in |
62 | 65 | * {@link AzureActiveDirectoryWebViewClient}. |
@@ -113,125 +116,219 @@ public void cleanUp() { |
113 | 116 | CommonFlightsManager.INSTANCE.resetFlightsManager(); |
114 | 117 | } |
115 | 118 |
|
116 | | - // ------- isEstsCloudHost() tests ------- |
| 119 | + // ------- Flight DISABLED regression tests ------- |
| 120 | + // When the ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT flight is OFF, all eSTS host URLs |
| 121 | + // must fall through to the default path (return false) — exactly the same behavior as |
| 122 | + // before this feature existed. reAttachPrtHeader must never be called. |
117 | 123 |
|
118 | | - @Test |
119 | | - public void isEstsCloudHost_returnsTrue_forKnownEstsHost() { |
120 | | - assertTrue(mWebViewClient.isEstsCloudHost(TEST_ESTS_URL)); |
| 124 | + /** |
| 125 | + * Helper: initializes the flights manager with the eSTS host PRT flight set to the given value. |
| 126 | + */ |
| 127 | + private void initFlightsWithEstsPrtFlight(boolean enabled) { |
| 128 | + final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class); |
| 129 | + when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT)) |
| 130 | + .thenReturn(enabled); |
| 131 | + final MockCommonFlightsManager mockCommonFlightsManager = new MockCommonFlightsManager(); |
| 132 | + mockCommonFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); |
| 133 | + CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockCommonFlightsManager); |
121 | 134 | } |
122 | 135 |
|
| 136 | + /** |
| 137 | + * When the flight is disabled, every URL variant must fall through (return false) |
| 138 | + * and reAttachPrtHeader must never be called — same behavior as before this feature existed. |
| 139 | + */ |
123 | 140 | @Test |
124 | | - public void isEstsCloudHost_returnsFalse_forNonEstsHost() { |
125 | | - assertFalse(mWebViewClient.isEstsCloudHost(TEST_NON_ESTS_URL)); |
| 141 | + public void flightDisabled_allUrlVariants_fallThrough_returnsFalse() { |
| 142 | + initFlightsWithEstsPrtFlight(false); |
| 143 | + |
| 144 | + final String[][] urlsWithLabels = { |
| 145 | + {"eSTS host /authorize", TEST_ESTS_URL}, |
| 146 | + {"eSTS /authorize with client_id", "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=test-client-id&response_type=code"}, |
| 147 | + {"China cloud", "https://login.chinacloudapi.cn/organizations/oauth2/v2.0/authorize?x=10"}, |
| 148 | + {"US Gov cloud", "https://login.microsoftonline.us/organizations/oauth2/v2.0/authorize?x=10"}, |
| 149 | + {"eSTS /token path", "https://login.microsoftonline.com/common/oauth2/v2.0/token?code=abc"}, |
| 150 | + {"eSTS KMSI path", "https://login.microsoftonline.com/common/oauth2/v2.0/authorize/kmsi"}, |
| 151 | + {"non-eSTS HTTPS", TEST_NON_ESTS_URL}, |
| 152 | + }; |
| 153 | + |
| 154 | + final AzureActiveDirectoryWebViewClient spyClient = spy(mWebViewClient); |
| 155 | + for (final String[] entry : urlsWithLabels) { |
| 156 | + final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, entry[1]); |
| 157 | + assertFalse(entry[0] + " must fall through when flight is disabled", result); |
| 158 | + } |
| 159 | + |
| 160 | + // Across all URLs, reAttachPrtHeader must never have been invoked. |
| 161 | + verify(spyClient, never()).reAttachPrtHeader( |
| 162 | + Mockito.anyString(), |
| 163 | + Mockito.any(ReAttachPrtHeaderHandler.class), |
| 164 | + Mockito.any(WebView.class), |
| 165 | + Mockito.anyString(), |
| 166 | + Mockito.any(Span.class) |
| 167 | + ); |
126 | 168 | } |
127 | 169 |
|
| 170 | + // ------- Flight ENABLED tests ------- |
| 171 | + // When the flight is ON, shouldReAttachPrtForEstsHost returns true iff: |
| 172 | + // isEstsCloudHost(url) AND hasPrtHeaderAttached() |
| 173 | + // AND (!url.contains("/authorize") || hasKnownClientId(url)) |
| 174 | + // |
| 175 | + // hasKnownClientId(url): |
| 176 | + // - redirectClientId == null → false |
| 177 | + // - redirectClientId matches originalClientId (from mRequestUrl) → true |
| 178 | + // - redirectClientId matches BROKER_CLIENT_ID → true |
| 179 | + // - exception during extraction → true |
| 180 | + |
| 181 | + private static final String TEST_ORIGINAL_CLIENT_ID = "test-original-client-id"; |
| 182 | + private static final String BROKER_CLIENT_ID = "29d9ed98-a469-4536-ade2-f981bc1d605e"; |
| 183 | + |
| 184 | + // -- Non-/authorize paths: should always be handled (no client_id check) -- |
| 185 | + |
128 | 186 | @Test |
129 | | - public void isEstsCloudHost_returnsFalse_forMalformedUrl() { |
130 | | - assertFalse(mWebViewClient.isEstsCloudHost(TEST_MALFORMED_URL)); |
| 187 | + public void flightEnabled_nonAuthorizePaths_handled() { |
| 188 | + initFlightsWithEstsPrtFlight(true); |
| 189 | + |
| 190 | + final String[][] handledUrls = { |
| 191 | + {"eSTS /token endpoint", "https://login.microsoftonline.com/common/oauth2/v2.0/token?code=abc"}, |
| 192 | + {"Sovereign cloud /token", "https://login.chinacloudapi.cn/common/oauth2/v2.0/token?code=abc"}, |
| 193 | + }; |
| 194 | + |
| 195 | + final AzureActiveDirectoryWebViewClient spyClient = spy(mWebViewClient); |
| 196 | + for (final String[] entry : handledUrls) { |
| 197 | + final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, entry[1]); |
| 198 | + assertTrue(entry[0] + " should be handled", result); |
| 199 | + } |
| 200 | + |
| 201 | + // reAttachPrtHeader must have been called for each handled URL. |
| 202 | + verify(spyClient, Mockito.times(handledUrls.length)).reAttachPrtHeader( |
| 203 | + Mockito.anyString(), |
| 204 | + Mockito.any(ReAttachPrtHeaderHandler.class), |
| 205 | + Mockito.any(WebView.class), |
| 206 | + Mockito.anyString(), |
| 207 | + Mockito.any(Span.class) |
| 208 | + ); |
131 | 209 | } |
132 | 210 |
|
133 | | - // ------- handleUrl() flight-gating tests ------- |
| 211 | + // -- /authorize path with known client_id (original or broker) → handled -- |
134 | 212 |
|
135 | 213 | @Test |
136 | | - public void handleUrl_whenFlightEnabled_andEstsHost_returnsTrue() { |
137 | | - final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class); |
138 | | - when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT)) |
139 | | - .thenReturn(true); |
140 | | - // Keep other flights at default behaviour — not enabled |
141 | | - final MockCommonFlightsManager mockCommonFlightsManager = new MockCommonFlightsManager(); |
142 | | - mockCommonFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); |
143 | | - CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockCommonFlightsManager); |
| 214 | + public void flightEnabled_authorizePath_knownClientId_handled() { |
| 215 | + initFlightsWithEstsPrtFlight(true); |
| 216 | + |
| 217 | + // Set request URL with a client_id so getClientIdFromRequestUrl() returns it. |
| 218 | + mWebViewClient.setRequestUrl( |
| 219 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + TEST_ORIGINAL_CLIENT_ID); |
| 220 | + |
| 221 | + final String[][] handledUrls = { |
| 222 | + {"matching original client_id", |
| 223 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + TEST_ORIGINAL_CLIENT_ID + "&response_type=code"}, |
| 224 | + {"broker client_id", |
| 225 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + BROKER_CLIENT_ID}, |
| 226 | + }; |
144 | 227 |
|
145 | 228 | final AzureActiveDirectoryWebViewClient spyClient = spy(mWebViewClient); |
146 | | - final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, TEST_ESTS_URL); |
147 | | - assertTrue(result); |
| 229 | + for (final String[] entry : handledUrls) { |
| 230 | + final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, entry[1]); |
| 231 | + assertTrue("/authorize with " + entry[0] + " should be handled", result); |
| 232 | + } |
| 233 | + |
| 234 | + verify(spyClient, Mockito.times(handledUrls.length)).reAttachPrtHeader( |
| 235 | + Mockito.anyString(), |
| 236 | + Mockito.any(ReAttachPrtHeaderHandler.class), |
| 237 | + Mockito.any(WebView.class), |
| 238 | + Mockito.anyString(), |
| 239 | + Mockito.any(Span.class) |
| 240 | + ); |
148 | 241 | } |
149 | 242 |
|
| 243 | + // -- /authorize path with unrecognized/missing client_id → falls through -- |
| 244 | + |
150 | 245 | @Test |
151 | | - public void handleUrl_whenFlightDisabled_andEstsHost_doesNotCallProcessEstsHostRedirect() { |
152 | | - final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class); |
153 | | - when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT)) |
154 | | - .thenReturn(false); |
155 | | - final MockCommonFlightsManager mockCommonFlightsManager = new MockCommonFlightsManager(); |
156 | | - mockCommonFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); |
157 | | - CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockCommonFlightsManager); |
| 246 | + public void flightEnabled_authorizePath_unrecognizedClientId_fallsThrough() { |
| 247 | + initFlightsWithEstsPrtFlight(true); |
| 248 | + |
| 249 | + mWebViewClient.setRequestUrl( |
| 250 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + TEST_ORIGINAL_CLIENT_ID); |
| 251 | + |
| 252 | + // All these /authorize URLs should fail the hasKnownClientId check: |
| 253 | + // - unknown client_id doesn't match original or broker |
| 254 | + // - missing client_id returns null → false |
| 255 | + // - KMSI path contains "/authorize" substring, and has no client_id |
| 256 | + final String[][] fallThroughUrls = { |
| 257 | + {"unknown client_id", |
| 258 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=unknown-app-id"}, |
| 259 | + {"no client_id param", |
| 260 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code"}, |
| 261 | + {"KMSI path (contains /authorize, no client_id)", |
| 262 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize/kmsi"}, |
| 263 | + }; |
158 | 264 |
|
159 | 265 | final AzureActiveDirectoryWebViewClient spyClient = spy(mWebViewClient); |
160 | | - // When the flight is disabled, the eSTS host check is bypassed entirely; |
161 | | - // the URL falls through to the unrecognized-URL path and returns false. |
162 | | - final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, TEST_ESTS_URL); |
163 | | - assertFalse(result); |
| 266 | + for (final String[] entry : fallThroughUrls) { |
| 267 | + final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, entry[1]); |
| 268 | + assertFalse(entry[0] + " should fall through", result); |
| 269 | + } |
| 270 | + |
| 271 | + verify(spyClient, never()).reAttachPrtHeader( |
| 272 | + Mockito.anyString(), |
| 273 | + Mockito.any(ReAttachPrtHeaderHandler.class), |
| 274 | + Mockito.any(WebView.class), |
| 275 | + Mockito.anyString(), |
| 276 | + Mockito.any(Span.class) |
| 277 | + ); |
164 | 278 | } |
165 | 279 |
|
| 280 | + // -- Non-eSTS host → falls through -- |
| 281 | + |
166 | 282 | @Test |
167 | | - public void handleUrl_whenFlightEnabled_andNonEstsHost_doesNotHandleAsEstsHostRedirect() { |
168 | | - final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class); |
169 | | - when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT)) |
170 | | - .thenReturn(true); |
171 | | - final MockCommonFlightsManager mockCommonFlightsManager = new MockCommonFlightsManager(); |
172 | | - mockCommonFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); |
173 | | - CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockCommonFlightsManager); |
| 283 | + public void flightEnabled_nonEstsHost_returnsFalse() { |
| 284 | + initFlightsWithEstsPrtFlight(true); |
174 | 285 |
|
175 | 286 | final AzureActiveDirectoryWebViewClient spyClient = spy(mWebViewClient); |
176 | | - // Non-eSTS HTTPS URL should not be caught by the new branch — it falls through |
177 | | - // to the unrecognized-URL path and returns false. |
178 | 287 | final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, TEST_NON_ESTS_URL); |
179 | | - assertFalse(result); |
| 288 | + assertFalse("Non-eSTS host should fall through even with flight enabled", result); |
180 | 289 | } |
181 | 290 |
|
| 291 | + // -- No PRT header → falls through -- |
| 292 | + |
182 | 293 | @Test |
183 | | - public void handleUrl_whenFlightEnabled_andEstsHost_butNoPrtHeader_doesNotHandleAsEstsHostRedirect() { |
184 | | - final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class); |
185 | | - when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT)) |
186 | | - .thenReturn(true); |
187 | | - final MockCommonFlightsManager mockCommonFlightsManager = new MockCommonFlightsManager(); |
188 | | - mockCommonFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); |
189 | | - CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockCommonFlightsManager); |
| 294 | + public void flightEnabled_noPrtHeader_returnsFalse() { |
| 295 | + initFlightsWithEstsPrtFlight(true); |
190 | 296 |
|
191 | | - // Remove PRT header from request headers to simulate a flow that was not PRT-authenticated. |
| 297 | + // Remove PRT header from request headers. |
192 | 298 | final HashMap<String, String> headersWithoutPrt = new HashMap<>(); |
193 | 299 | headersWithoutPrt.put("key", "value"); |
194 | 300 | mWebViewClient.setRequestHeaders(headersWithoutPrt); |
195 | 301 |
|
196 | 302 | final AzureActiveDirectoryWebViewClient spyClient = spy(mWebViewClient); |
197 | | - // Without a PRT header in the initial request, the eSTS host redirect should fall through. |
198 | 303 | final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, TEST_ESTS_URL); |
199 | | - assertFalse(result); |
| 304 | + assertFalse("Without PRT header, eSTS host redirect should fall through", result); |
200 | 305 | } |
201 | 306 |
|
202 | 307 | // ------- setRequestUrl() / mLoginHint extraction tests ------- |
203 | 308 |
|
204 | 309 | @Test |
205 | 310 | public void setRequestUrl_extractsLoginHint_whenPresent() { |
206 | | - // Verifies that setRequestUrl does not throw and populates mLoginHint by |
207 | | - // checking indirectly: when the flight is enabled and the redirect goes to |
208 | | - // an eSTS host, the handler is called (i.e., processEstsHostRedirect |
209 | | - // was triggered), meaning the URL was handled. |
210 | | - final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class); |
211 | | - when(mockFlightsProvider.isFlightEnabled(CommonFlight.ENABLE_PRT_HEADER_FOR_ESTS_HOST_REDIRECT)) |
212 | | - .thenReturn(true); |
213 | | - final MockCommonFlightsManager mockCommonFlightsManager = new MockCommonFlightsManager(); |
214 | | - mockCommonFlightsManager.setMockCommonFlightsProvider(mockFlightsProvider); |
215 | | - CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(mockCommonFlightsManager); |
216 | | - |
217 | | - // Set request URL with login_hint — this also populates mLoginHint |
218 | | - mWebViewClient.setRequestUrl(TEST_ESTS_URL_WITH_LOGIN_HINT); |
219 | | - |
220 | | - // The eSTS host redirect should be handled by the new code path |
| 311 | + // Verifies that setRequestUrl populates mLoginHint. We check this indirectly: |
| 312 | + // set a request URL with login_hint AND client_id, then verify a redirect with |
| 313 | + // matching client_id is handled (which triggers processEstsHostRedirect with mLoginHint). |
| 314 | + initFlightsWithEstsPrtFlight(true); |
| 315 | + |
| 316 | + mWebViewClient.setRequestUrl( |
| 317 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" |
| 318 | + + TEST_ORIGINAL_CLIENT_ID + "&login_hint=user%40contoso.com"); |
| 319 | + |
| 320 | + // Redirect with matching client_id so shouldReAttachPrtForEstsHost → true. |
| 321 | + final String redirectUrl = |
| 322 | + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + TEST_ORIGINAL_CLIENT_ID; |
221 | 323 | final AzureActiveDirectoryWebViewClient spyClient = spy(mWebViewClient); |
222 | | - final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, TEST_ESTS_URL); |
| 324 | + final boolean result = spyClient.shouldOverrideUrlLoading(mMockWebView, redirectUrl); |
223 | 325 | assertTrue("Expected eSTS host redirect to be handled after login_hint extraction", result); |
224 | 326 | } |
225 | 327 |
|
226 | 328 | @Test |
227 | | - public void setRequestUrl_doesNotThrow_whenLoginHintAbsent() { |
228 | | - // No exception expected when login_hint is not present in the URL. |
| 329 | + public void setRequestUrl_doesNotThrow_forEdgeCases() { |
| 330 | + // Neither missing login_hint nor malformed URL should throw. |
229 | 331 | mWebViewClient.setRequestUrl(TEST_ESTS_URL); |
230 | | - } |
231 | | - |
232 | | - @Test |
233 | | - public void setRequestUrl_doesNotThrow_forMalformedUrl() { |
234 | | - // Should log a warning and not propagate the exception. |
235 | 332 | mWebViewClient.setRequestUrl(TEST_MALFORMED_URL); |
236 | 333 | } |
237 | 334 | } |
0 commit comments