@@ -199,74 +199,126 @@ public void createAadTokenCredential() throws InterruptedException {
199199 }
200200
201201 @ Test (groups = { "long-emulator" }, timeOut = 10 * TIMEOUT )
202- public void testAadScopeOverride () throws Exception {
203- CosmosAsyncClient setupClient = null ;
204- CosmosAsyncClient aadClient = null ;
205- String containerName = UUID .randomUUID ().toString ();
206- String overrideScope = "https://cosmos.azure.com/.default" ;
202+ public void overrideScope_only_noFallback_onSuccess () throws Exception {
203+ CosmosAsyncClient client = null ;
204+ ScopeRecorder .clear ();
205+
206+ java .net .URI ep = new java .net .URI (TestConfigurations .HOST );
207+ final String overrideScope = ep .getScheme () + "://" + ep .getHost () + "/.default" ;
208+ setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , overrideScope );
207209
208210 try {
209- setupClient = new CosmosClientBuilder ()
211+ TokenCredential cred = new AadSimpleEmulatorTokenCredential (TestConfigurations .MASTER_KEY );
212+
213+ client = new CosmosClientBuilder ()
210214 .endpoint (TestConfigurations .HOST )
211- .key ( TestConfigurations . MASTER_KEY )
215+ .credential ( cred )
212216 .buildAsyncClient ();
213217
214- setupClient .createDatabase (databaseId ).block ();
215- setupClient .getDatabase (databaseId ).createContainer (containerName , PARTITION_KEY_PATH ).block ();
216- } finally {
217- if (setupClient != null ) {
218- safeClose (setupClient );
218+ client .readAllDatabases ().byPage ().blockFirst ();
219+
220+ java .util .List <String > scopes = ScopeRecorder .all ();
221+ assert scopes .size () >= 1 : "Expected at least one AAD call" ;
222+ for (String s : scopes ) {
223+ assert overrideScope .equals (s ) : "Expected only override scope; saw: " + scopes ;
219224 }
225+ } finally {
226+ if (client != null ) safeClose (client );
227+ setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , Configs .DEFAULT_AAD_SCOPE_OVERRIDE );
220228 }
229+ }
221230
222- Thread .sleep (TIMEOUT );
231+ @ Test (groups = { "long-emulator" }, timeOut = 10 * TIMEOUT )
232+ public void overrideScope_authError_noFallback () throws Exception {
233+ CosmosAsyncClient client = null ;
234+ ScopeRecorder .clear ();
223235
236+ final String overrideScope = "https://my.custom.scope/.default" ;
224237 setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , overrideScope );
225238
226- TokenCredential emulatorCredential =
227- new AadSimpleEmulatorTokenCredential (TestConfigurations .MASTER_KEY );
228-
229- aadClient = new CosmosClientBuilder ()
230- .endpoint (TestConfigurations .HOST )
231- .credential (emulatorCredential )
232- .buildAsyncClient ();
233-
234239 try {
235- CosmosAsyncContainer container = aadClient
236- .getDatabase (databaseId )
237- .getContainer (containerName );
238-
239- String itemId = UUID .randomUUID ().toString ();
240- String pk = UUID .randomUUID ().toString ();
241- ItemSample item = getDocumentDefinition (itemId , pk );
240+ TokenCredential cred = new AlwaysFail500011Credential ();
242241
243- container .createItem (item ).block ();
244-
245- List <String > scopes = AadSimpleEmulatorTokenCredential .getLastScopes ();
246- assert scopes != null && scopes .size () == 1 ;
247- assert overrideScope .equals (scopes .get (0 ));
248-
249- container .deleteItem (item .id , new PartitionKey (item .mypk )).block ();
250- } finally {
251242 try {
252- CosmosAsyncClient cleanupClient = new CosmosClientBuilder ()
243+ client = new CosmosClientBuilder ()
253244 .endpoint (TestConfigurations .HOST )
254- .key ( TestConfigurations . MASTER_KEY )
245+ .credential ( cred )
255246 .buildAsyncClient ();
256- try {
257- cleanupClient .getDatabase (databaseId ).delete ().block ();
258- } finally {
259- safeClose (cleanupClient );
260- }
261- } finally {
262- if (aadClient != null ) {
263- safeClose (aadClient );
247+
248+ client .readAllDatabases ().byPage ().blockFirst ();
249+ assert false : "Expected an auth failure with override scope" ;
250+ } catch (Exception ex ) {
251+ // Only the override scope should have been attempted; no fallback allowed
252+ java .util .List <String > scopes = ScopeRecorder .all ();
253+ assert scopes .size () >= 1 : "Expected at least one scope attempt" ;
254+ for (String s : scopes ) {
255+ assert overrideScope .equals (s ) : "No fallback allowed in override mode; saw: " + scopes ;
264256 }
265- setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , Configs .DEFAULT_AAD_SCOPE_OVERRIDE );
266257 }
258+ } finally {
259+ if (client != null ) safeClose (client );
260+ setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , Configs .DEFAULT_AAD_SCOPE_OVERRIDE );
267261 }
262+ }
268263
269- Thread .sleep (SHUTDOWN_TIMEOUT );
264+ @ Test (groups = { "long-emulator" }, timeOut = 10 * TIMEOUT )
265+ public void accountScope_only_whenNoOverride_andNoAuthFailure () throws Exception {
266+ CosmosAsyncClient client = null ;
267+ ScopeRecorder .clear ();
268+ setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , Configs .DEFAULT_AAD_SCOPE_OVERRIDE );
269+
270+ java .net .URI ep = new java .net .URI (TestConfigurations .HOST );
271+ String accountScope = ep .getScheme () + "://" + ep .getHost () + "/.default" ;
272+
273+ try {
274+ TokenCredential cred = new AadSimpleEmulatorTokenCredential (TestConfigurations .MASTER_KEY );
275+
276+ client = new CosmosClientBuilder ()
277+ .endpoint (TestConfigurations .HOST )
278+ .credential (cred )
279+ .buildAsyncClient ();
280+
281+ client .readAllDatabases ().byPage ().blockFirst ();
282+
283+ java .util .List <String > scopes = ScopeRecorder .all ();
284+ assert scopes .size () >= 1 : "Expected at least one AAD call" ;
285+ for (String s : scopes ) {
286+ assert accountScope .equals (s ) : "Expected only account scope; saw: " + scopes ;
287+ }
288+ } finally {
289+ if (client != null ) safeClose (client );
290+ setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , Configs .DEFAULT_AAD_SCOPE_OVERRIDE );
291+ }
292+ }
293+
294+ @ Test (groups = { "long-emulator" }, timeOut = 10 * TIMEOUT )
295+ public void accountScope_fallbackToCosmosScope_onAadSts500011 () throws Exception {
296+ CosmosAsyncClient client = null ;
297+ ScopeRecorder .clear ();
298+ setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , Configs .DEFAULT_AAD_SCOPE_OVERRIDE );
299+
300+ java .net .URI ep = new java .net .URI (TestConfigurations .HOST );
301+ String accountScope = ep .getScheme () + "://" + ep .getHost () + "/.default" ;
302+ String fallbackScope = "https://cosmos.azure.com/.default" ;
303+
304+ try {
305+ // Fail on account scope with AADSTS500011
306+ TokenCredential cred = new AccountThenFallbackCredential (TestConfigurations .MASTER_KEY , accountScope );
307+
308+ client = new CosmosClientBuilder ()
309+ .endpoint (TestConfigurations .HOST )
310+ .credential (cred )
311+ .buildAsyncClient ();
312+
313+ client .readAllDatabases ().byPage ().blockFirst ();
314+
315+ java .util .List <String > scopes = ScopeRecorder .all ();
316+ assert scopes .contains (accountScope ) : "Expected primary account scope attempt; saw: " + scopes ;
317+ assert scopes .contains (fallbackScope ) : "Expected fallback to cosmos public scope; saw: " + scopes ;
318+ } finally {
319+ if (client != null ) safeClose (client );
320+ setEnv (Configs .AAD_SCOPE_OVERRIDE_VARIABLE , Configs .DEFAULT_AAD_SCOPE_OVERRIDE );
321+ }
270322 }
271323
272324 @ SuppressWarnings ({"unchecked" , "rawtypes" })
@@ -298,6 +350,65 @@ private static void setEnv(String key, String value) throws Exception {
298350 }
299351 }
300352
353+ // Records all scopes used during the test run (append-only).
354+ static final class ScopeRecorder {
355+ private static final java .util .concurrent .CopyOnWriteArrayList <String > SEEN = new java .util .concurrent .CopyOnWriteArrayList <>();
356+ static void clear () { SEEN .clear (); }
357+ static void record (TokenRequestContext ctx ) {
358+ java .util .List <String > s = ctx .getScopes ();
359+ if (s != null ) SEEN .addAll (s );
360+ }
361+ static java .util .List <String > all () { return new java .util .ArrayList <>(SEEN ); }
362+ }
363+
364+ /**
365+ * Always fails with an AADSTS500011 message for any scope.
366+ * Used to prove that the "override scope" path does NOT fallback.
367+ */
368+ static class AlwaysFail500011Credential implements TokenCredential {
369+ @ Override
370+ public Mono <AccessToken > getToken (TokenRequestContext tokenRequestContext ) {
371+ ScopeRecorder .record (tokenRequestContext );
372+ return Mono .error (new RuntimeException ("AADSTS500011: Application <id> was not found in the directory" ));
373+ }
374+ }
375+
376+ static class AccountThenFallbackCredential implements TokenCredential {
377+ private final AadSimpleEmulatorTokenCredential emulatorIssuer ;
378+ private final String accountScope ;
379+ private final String cosmosPublicScope = "https://cosmos.azure.com/.default" ;
380+ private final java .util .concurrent .atomic .AtomicInteger calls = new java .util .concurrent .atomic .AtomicInteger (0 );
381+
382+ AccountThenFallbackCredential (String emulatorKey , String accountScope ) {
383+ this .emulatorIssuer = new AadSimpleEmulatorTokenCredential (emulatorKey );
384+ this .accountScope = accountScope ;
385+ }
386+
387+ @ Override
388+ public Mono <AccessToken > getToken (TokenRequestContext tokenRequestContext ) {
389+ ScopeRecorder .record (tokenRequestContext );
390+
391+ String scope = tokenRequestContext .getScopes () != null && !tokenRequestContext .getScopes ().isEmpty ()
392+ ? tokenRequestContext .getScopes ().get (0 )
393+ : "" ;
394+
395+ int n = calls .incrementAndGet ();
396+
397+ // Fail on the first attempt if it is the account scope, to trigger fallback
398+ if (n == 1 && scope .equals (accountScope )) {
399+ return Mono .error (new RuntimeException ("AADSTS500011: Application <id> was not found in the directory" ));
400+ }
401+
402+ // When SDK retries with the cosmos public scope, succeed with a valid emulator token
403+ if (scope .equals (cosmosPublicScope )) {
404+ return emulatorIssuer .getToken (tokenRequestContext );
405+ }
406+
407+ // If anything unexpected happens, fail loudly so the test points to the issue
408+ return Mono .error (new IllegalStateException ("Unexpected scope or call ordering. Scope=" + scope + " call=" + n ));
409+ }
410+ }
411+
301412 private ItemSample getDocumentDefinition (String itemId , String partitionKeyValue ) {
302413 ItemSample itemSample = new ItemSample ();
303414 itemSample .id = itemId ;
@@ -329,11 +440,6 @@ static class AadSimpleEmulatorTokenCredential implements TokenCredential {
329440 private final String AAD_HEADER_COSMOS_EMULATOR = "{\" typ\" :\" JWT\" ,\" alg\" :\" RS256\" ,\" x5t\" :\" CosmosEmulatorPrimaryMaster\" ,\" kid\" :\" CosmosEmulatorPrimaryMaster\" }" ;
330441 private final String AAD_CLAIM_COSMOS_EMULATOR_FORMAT = "{\" aud\" :\" https://localhost.localhost\" ,\" iss\" :\" https://sts.fake-issuer.net/7b1999a1-dfd7-440e-8204-00170979b984\" ,\" iat\" :%d,\" nbf\" :%d,\" exp\" :%d,\" aio\" :\" \" ,\" appid\" :\" localhost\" ,\" appidacr\" :\" 1\" ,\" idp\" :\" https://localhost:8081/\" ,\" oid\" :\" 96313034-4739-43cb-93cd-74193adbe5b6\" ,\" rh\" :\" \" ,\" sub\" :\" localhost\" ,\" tid\" :\" EmulatorFederation\" ,\" uti\" :\" \" ,\" ver\" :\" 1.0\" ,\" scp\" :\" user_impersonation\" ,\" groups\" :[\" 7ce1d003-4cb3-4879-b7c5-74062a35c66e\" ,\" e99ff30c-c229-4c67-ab29-30a6aebc3e58\" ,\" 5549bb62-c77b-4305-bda9-9ec66b85d9e4\" ,\" c44fd685-5c58-452c-aaf7-13ce75184f65\" ,\" be895215-eab5-43b7-9536-9ef8fe130330\" ]}" ;
331442
332- private static volatile List <String > lastScopes = Collections .emptyList ();
333-
334- public static List <String > getLastScopes () {
335- return lastScopes ;
336- }
337443 public AadSimpleEmulatorTokenCredential (String emulatorKey ) {
338444 if (emulatorKey == null || emulatorKey .isEmpty ()) {
339445 throw new IllegalArgumentException ("emulatorKey" );
@@ -344,10 +450,8 @@ public AadSimpleEmulatorTokenCredential(String emulatorKey) {
344450
345451 @ Override
346452 public Mono <AccessToken > getToken (TokenRequestContext tokenRequestContext ) {
347- List <String > scopes = tokenRequestContext .getScopes (); // List<String>, not String[]
348- lastScopes = (scopes != null && !scopes .isEmpty ())
349- ? new ArrayList <>(scopes )
350- : Collections .emptyList ();
453+ // Record scopes for verification in tests
454+ AadAuthorizationTests .ScopeRecorder .record (tokenRequestContext );
351455
352456 String aadToken = emulatorKey_based_AAD_String ();
353457 return Mono .just (new AccessToken (aadToken , OffsetDateTime .now ().plusHours (2 )));
0 commit comments