Skip to content

Commit f0d3418

Browse files
authored
AAD Auth: Adds an options to override AAD audience scope through environment variable. (#46237)
* Adds AAD scope override feature. * Update changelog and code cleanup. * Update changelog and code cleanup. * Update static method usage. * fix build errors with static class. * Update test name. * Update imports * Update environment variable property. * Update environment variable property.
1 parent 06bcd6a commit f0d3418

File tree

4 files changed

+134
-6
lines changed

4 files changed

+134
-6
lines changed

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/AadAuthorizationTests.java

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@
3636
import reactor.core.publisher.Flux;
3737
import reactor.core.publisher.Mono;
3838

39+
import java.lang.reflect.Field;
3940
import java.time.OffsetDateTime;
4041
import java.time.ZonedDateTime;
4142
import java.util.List;
4243
import java.util.Properties;
4344
import java.util.UUID;
45+
import java.util.ArrayList;
46+
import java.util.Collections;
47+
import java.util.Map;
4448

4549
public class AadAuthorizationTests extends TestSuiteBase {
4650
private final static Logger log = LoggerFactory.getLogger(AadAuthorizationTests.class);
@@ -193,6 +197,106 @@ public void createAadTokenCredential() throws InterruptedException {
193197
Thread.sleep(SHUTDOWN_TIMEOUT);
194198
}
195199

200+
@Test(groups = { "emulator" }, timeOut = 10 * TIMEOUT)
201+
public void testAadScopeOverride() throws Exception {
202+
CosmosAsyncClient setupClient = null;
203+
CosmosAsyncClient aadClient = null;
204+
String containerName = UUID.randomUUID().toString();
205+
String overrideScope = "https://cosmos.azure.com/.default";
206+
207+
try {
208+
setupClient = new CosmosClientBuilder()
209+
.endpoint(TestConfigurations.HOST)
210+
.key(TestConfigurations.MASTER_KEY)
211+
.buildAsyncClient();
212+
213+
setupClient.createDatabase(databaseId).block();
214+
setupClient.getDatabase(databaseId).createContainer(containerName, PARTITION_KEY_PATH).block();
215+
} finally {
216+
if (setupClient != null) {
217+
safeClose(setupClient);
218+
}
219+
}
220+
221+
Thread.sleep(TIMEOUT);
222+
223+
setEnv("AZURE_COSMOS_AAD_SCOPE_OVERRIDE", overrideScope);
224+
225+
TokenCredential emulatorCredential =
226+
new AadSimpleEmulatorTokenCredential(TestConfigurations.MASTER_KEY);
227+
228+
aadClient = new CosmosClientBuilder()
229+
.endpoint(TestConfigurations.HOST)
230+
.credential(emulatorCredential)
231+
.buildAsyncClient();
232+
233+
try {
234+
CosmosAsyncContainer container = aadClient
235+
.getDatabase(databaseId)
236+
.getContainer(containerName);
237+
238+
String itemId = UUID.randomUUID().toString();
239+
String pk = UUID.randomUUID().toString();
240+
ItemSample item = getDocumentDefinition(itemId, pk);
241+
242+
container.createItem(item).block();
243+
244+
List<String> scopes = AadSimpleEmulatorTokenCredential.getLastScopes();
245+
assert scopes != null && scopes.size() == 1;
246+
assert overrideScope.equals(scopes.get(0));
247+
248+
container.deleteItem(item.id, new PartitionKey(item.mypk)).block();
249+
} finally {
250+
try {
251+
CosmosAsyncClient cleanupClient = new CosmosClientBuilder()
252+
.endpoint(TestConfigurations.HOST)
253+
.key(TestConfigurations.MASTER_KEY)
254+
.buildAsyncClient();
255+
try {
256+
cleanupClient.getDatabase(databaseId).delete().block();
257+
} finally {
258+
safeClose(cleanupClient);
259+
}
260+
} finally {
261+
if (aadClient != null) {
262+
safeClose(aadClient);
263+
}
264+
setEnv("AZURE_COSMOS_AAD_SCOPE_OVERRIDE", "");
265+
}
266+
}
267+
268+
Thread.sleep(SHUTDOWN_TIMEOUT);
269+
}
270+
271+
@SuppressWarnings({"unchecked", "rawtypes"})
272+
private static void setEnv(String key, String value) throws Exception {
273+
Map<String, String> env = System.getenv();
274+
Class<?> cl = env.getClass();
275+
try {
276+
Field field = cl.getDeclaredField("m");
277+
field.setAccessible(true);
278+
Map<String, String> writableEnv = (Map<String, String>) field.get(env);
279+
if (value == null) {
280+
writableEnv.remove(key);
281+
} else {
282+
writableEnv.put(key, value);
283+
}
284+
} catch (NoSuchFieldException nsfe) {
285+
Field[] fields = cl.getDeclaredFields();
286+
for (Field f : fields) {
287+
if (f.getType().getName().equals("java.util.Map")) {
288+
f.setAccessible(true);
289+
Map<String, String> map = (Map<String, String>) f.get(env);
290+
if (value == null) {
291+
map.remove(key);
292+
} else {
293+
map.put(key, value);
294+
}
295+
}
296+
}
297+
}
298+
}
299+
196300
private ItemSample getDocumentDefinition(String itemId, String partitionKeyValue) {
197301
ItemSample itemSample = new ItemSample();
198302
itemSample.id = itemId;
@@ -219,11 +323,16 @@ public void afterMethod() {
219323
public void afterClass() {
220324
}
221325

222-
class AadSimpleEmulatorTokenCredential implements TokenCredential {
326+
static class AadSimpleEmulatorTokenCredential implements TokenCredential {
223327
private final String emulatorKeyEncoded;
224328
private final String AAD_HEADER_COSMOS_EMULATOR = "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"x5t\":\"CosmosEmulatorPrimaryMaster\",\"kid\":\"CosmosEmulatorPrimaryMaster\"}";
225329
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\"]}";
226330

331+
private static volatile List<String> lastScopes = Collections.emptyList();
332+
333+
public static List<String> getLastScopes() {
334+
return lastScopes;
335+
}
227336
public AadSimpleEmulatorTokenCredential(String emulatorKey) {
228337
if (emulatorKey == null || emulatorKey.isEmpty()) {
229338
throw new IllegalArgumentException("emulatorKey");
@@ -234,6 +343,11 @@ public AadSimpleEmulatorTokenCredential(String emulatorKey) {
234343

235344
@Override
236345
public Mono<AccessToken> getToken(TokenRequestContext tokenRequestContext) {
346+
List<String> scopes = tokenRequestContext.getScopes(); // List<String>, not String[]
347+
lastScopes = (scopes != null && !scopes.isEmpty())
348+
? new ArrayList<>(scopes)
349+
: Collections.emptyList();
350+
237351
String aadToken = emulatorKey_based_AAD_String();
238352
return Mono.just(new AccessToken(aadToken, OffsetDateTime.now().plusHours(2)));
239353
}

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#### Other Changes
2222
* Added quicker cross region retry capability when a 410 `Lease Not Found` is returned by a partition in a Strong Consistency account. - See [PR 46071](https://github.com/Azure/azure-sdk-for-java/pull/46071)
23+
* Added an option to override AAD audience scope through environment variable. See [PR 46237](https://github.com/Azure/azure-sdk-for-java/pull/46237).
2324

2425
### 4.73.0 (2025-07-18)
2526

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ public class Configs {
8484
private static final String QUERY_PLAN_RESPONSE_TIMEOUT_IN_SECONDS = "COSMOS.QUERY_PLAN_RESPONSE_TIMEOUT_IN_SECONDS";
8585
private static final String ADDRESS_REFRESH_RESPONSE_TIMEOUT_IN_SECONDS = "COSMOS.ADDRESS_REFRESH_RESPONSE_TIMEOUT_IN_SECONDS";
8686

87+
private static final String AAD_SCOPE_OVERRIDE = "COSMOS.AAD_SCOPE_OVERRIDE";
88+
private static final String AAD_SCOPE_OVERRIDE_VARIABLE = "COSMOS_AAD_SCOPE_OVERRIDE";
89+
private static final String DEFAULT_AAD_SCOPE_OVERRIDE = "";
90+
8791
public static final String NON_IDEMPOTENT_WRITE_RETRY_POLICY = "COSMOS.WRITE_RETRY_POLICY";
8892
public static final String NON_IDEMPOTENT_WRITE_RETRY_POLICY_VARIABLE = "COSMOS_WRITE_RETRY_POLICY";
8993

@@ -1198,6 +1202,14 @@ public static boolean isReadAvailabilityStrategyEnabledWithPpaf() {
11981202
return Boolean.parseBoolean(isReadAvailabilityStrategyEnabledWithPpaf);
11991203
}
12001204

1205+
public static String getAadScopeOverride() {
1206+
return System.getProperty(
1207+
AAD_SCOPE_OVERRIDE,
1208+
firstNonNull(
1209+
emptyToNull(System.getenv().get(AAD_SCOPE_OVERRIDE_VARIABLE)),
1210+
DEFAULT_AAD_SCOPE_OVERRIDE));
1211+
}
1212+
12011213
public static int getWarnLevelLoggingThresholdForPpaf() {
12021214
String warnLevelLoggingThresholdForPpaf = System.getProperty(
12031215
WARN_LEVEL_LOGGING_THRESHOLD_FOR_PPAF,
@@ -1214,7 +1226,7 @@ public static String getAzureMonitorConnectionString() {
12141226
System.getenv(APPLICATIONINSIGHTS_CONNECTION_STRING_VARIABLE)
12151227
);
12161228
}
1217-
1229+
12181230
public static EnumSet<AttributeNamingScheme> getDefaultOtelSpanAttributeNamingScheme() {
12191231
String valueFromSystemProperty = System.getProperty(OTEL_SPAN_ATTRIBUTE_NAMING_SCHEME);
12201232
if (valueFromSystemProperty != null && !valueFromSystemProperty.isEmpty()) {

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,11 @@ private RxDocumentClientImpl(URI serviceEndpoint,
535535
hasAuthKeyResourceToken = false;
536536
this.authorizationTokenProvider = null;
537537
if (tokenCredential != null) {
538-
this.tokenCredentialScopes = new String[] {
539-
// AadTokenAuthorizationHelper.AAD_AUTH_TOKEN_COSMOS_WINDOWS_SCOPE,
540-
serviceEndpoint.getScheme() + "://" + serviceEndpoint.getHost() + "/.default"
541-
};
538+
String scopeOverride = Configs.getAadScopeOverride();
539+
String defaultScope = serviceEndpoint.getScheme() + "://" + serviceEndpoint.getHost() + "/.default";
540+
String scopeToUse = (scopeOverride != null && !scopeOverride.isEmpty()) ? scopeOverride : defaultScope;
541+
542+
this.tokenCredentialScopes = new String[] { scopeToUse };
542543
this.tokenCredentialCache = new SimpleTokenCache(() -> this.tokenCredential
543544
.getToken(new TokenRequestContext().addScopes(this.tokenCredentialScopes)));
544545
this.authorizationTokenType = AuthorizationTokenType.AadToken;

0 commit comments

Comments
 (0)