Skip to content

Commit db79bca

Browse files
authored
[@azure/cosmos] Add AAD Scope Override and fallback (#36024)
### Packages impacted by this PR @azure/cosmos ### Issues associated with this PR #36015 ### Describe the problem that is addressed by this PR Added support for overriding AAD authentication scope via the new `aadScope` option in `CosmosClientOptions`. When no custom scope is provided, the system uses the account-specific scope for authentication and implements a fallback mechanism to `https://cosmos.azure.com/.default` in case of `AADSTS500011` errors. When a custom scope is explicitly provided via the `aadScope` option, no fallback occurs. ### What are the possible designs available to address the problem? If there are more than one possible design, why was the one in this PR chosen? ### Are there test cases added in this PR? _(If not, why?)_ Yes ### Provide a list of related PRs _(if any)_ ### Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ ### Checklists - [ ] Added impacted package name to the issue description - [ ] Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ - [ ] Added a changelog (if necessary)
1 parent 8534bb9 commit db79bca

File tree

11 files changed

+661
-8
lines changed

11 files changed

+661
-8
lines changed

sdk/cosmosdb/cosmos/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Release History
2-
## 4.6.0 (2025-10-01)
2+
3+
## 4.6.0 (2025-10-08)
34

45
### Features Added
56

@@ -13,6 +14,7 @@ await container.items.upsert(city, requestOptions);
1314

1415
await container.item("1").delete(requestOptions);
1516
```
17+
- [#36015](https://github.com/Azure/azure-sdk-for-js/issues/36015) AAD Authentication Scope Override: Added support for overriding AAD authentication scope via the new `aadScope` option in `CosmosClientOptions`. When no custom scope is provided, the system uses the account-specific scope for authentication and implements a fallback mechanism to `https://cosmos.azure.com/.default` in case of `AADSTS500011` errors. When a custom scope is explicitly provided via the `aadScope` option, no fallback occurs.
1618

1719
### Bugs Fixed
1820
- [#35875](https://github.com/Azure/azure-sdk-for-js/issues/35875) Fixed the per-operation partition key format in the batch API to match the API-level partition key,

sdk/cosmosdb/cosmos/review/cosmos-node.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export type ClientConfigDiagnostic = {
180180
diagnosticLevel?: CosmosDbDiagnosticLevel;
181181
pluginsConfigured: boolean;
182182
sDKVersion: string;
183+
aadScopeOverride?: boolean;
183184
};
184185

185186
// @public (undocumented)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* @summary Demonstrates how to authenticate and use your database account using AAD credentials with Fabric.
6+
*
7+
* Prerequisites:
8+
* 1. An Azure Cosmos account in fabric environment and database and container created.
9+
* https://learn.microsoft.com/en-us/fabric/database/cosmos-db/overview
10+
* 2. Node.js packages (@azure/cosmos + @azure/identity) and login:
11+
* npm install @azure/cosmos @azure/identity
12+
* az login
13+
*
14+
* Sample - demonstrates how to authenticate and use your database account using AAD credentials with Fabric.
15+
* Read more about operations allowed for this authorization method: https://aka.ms/cosmos-native-rbac
16+
*
17+
* Note:
18+
* This sample assumes the database and container already exist.
19+
* It writes one item (PK path assumed to be "/pk") and reads it back.
20+
*/
21+
22+
require("dotenv/config");
23+
const { DefaultAzureCredential } = require("@azure/identity");
24+
const { CosmosClient } = require("@azure/cosmos");
25+
const { handleError, finish, logStep } = require("./Shared/handleError.js");
26+
27+
// Configuration - replace with your values
28+
const endpoint = process.env.COSMOS_ENDPOINT || "<cosmos endpoint>";
29+
const databaseId = process.env.COSMOS_DATABASE || "<cosmos database>";
30+
const containerId = process.env.COSMOS_CONTAINER || "<cosmos container>";
31+
32+
function getTestItem(num) {
33+
return {
34+
id: `Item_${num}`,
35+
pk: "partition1",
36+
name: `Item ${num}`,
37+
description: `This is item ${num}`,
38+
runId: crypto.randomUUID(),
39+
};
40+
}
41+
42+
async function run() {
43+
44+
logStep("Setting up AAD credentials");
45+
46+
// AAD auth works with az login
47+
const credentials = new DefaultAzureCredential();
48+
49+
logStep("Creating Cosmos client with AAD credentials");
50+
const client = new CosmosClient({
51+
endpoint,
52+
aadCredentials: credentials,
53+
aadScope: "https://cosmos.azure.com/.default"
54+
});
55+
56+
57+
// Do R/W data operations with your authorized AAD client
58+
logStep("Getting database and container references");
59+
const database = client.database(databaseId);
60+
const container = database.container(containerId);
61+
62+
logStep("Creating a test item");
63+
// Create item
64+
const testItem = getTestItem(0);
65+
const { resource: createdItem } = await container.items.create(testItem);
66+
console.log(`Created item: ${createdItem?.id}`);
67+
68+
logStep("Reading the item back");
69+
// Read item
70+
const { resource: readItem } = await container.item(testItem.id, testItem.pk).read();
71+
console.log("Point read:");
72+
console.log(JSON.stringify(readItem, null, 2));
73+
74+
logStep("Querying for items in the partition");
75+
// Query items
76+
const querySpec = {
77+
query: "SELECT * FROM c WHERE c.pk = @partitionKey",
78+
parameters: [
79+
{
80+
name: "@partitionKey",
81+
value: testItem.pk,
82+
},
83+
],
84+
};
85+
86+
const { resources: items } = await container.items.query(querySpec).fetchAll();
87+
console.log(`Found ${items.length} items in partition '${testItem.pk}':`);
88+
items.forEach((item) => {
89+
console.log(`- ${item.id}: ${item.name}`);
90+
});
91+
92+
logStep("Sample completed successfully");
93+
await finish();
94+
}
95+
96+
run().catch(handleError);
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* @summary Demonstrates how to authenticate and use your database account using AAD credentials with Fabric.
6+
*
7+
* Prerequisites:
8+
* 1. An Azure Cosmos account in fabric environment and database and container created.
9+
* https://learn.microsoft.com/en-us/fabric/database/cosmos-db/overview
10+
* 2. Node.js packages (@azure/cosmos + @azure/identity) and login:
11+
* npm install @azure/cosmos @azure/identity
12+
* az login
13+
*
14+
* Sample - demonstrates how to authenticate and use your database account using AAD credentials with Fabric.
15+
* Read more about operations allowed for this authorization method: https://aka.ms/cosmos-native-rbac
16+
*
17+
* Note:
18+
* This sample assumes the database and container already exist.
19+
* It writes one item (PK path assumed to be "/pk") and reads it back.
20+
*/
21+
22+
import "dotenv/config";
23+
import { DefaultAzureCredential } from "@azure/identity";
24+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
25+
// @ts-ignore
26+
import { CosmosClient } from "@azure/cosmos";
27+
import { handleError, finish, logStep } from "./Shared/handleError.js";
28+
29+
// Configuration - replace with your values
30+
const endpoint = process.env.COSMOS_ENDPOINT || "<cosmos endpoint>";
31+
const databaseId = process.env.COSMOS_DATABASE || "<cosmos database>";
32+
const containerId = process.env.COSMOS_CONTAINER || "<cosmos container>";
33+
34+
// Test item structure
35+
interface TestItem {
36+
id: string;
37+
pk: string;
38+
name: string;
39+
description: string;
40+
runId: string;
41+
}
42+
43+
function getTestItem(num: number): TestItem {
44+
return {
45+
id: `Item_${num}`,
46+
pk: "partition1",
47+
name: `Item ${num}`,
48+
description: `This is item ${num}`,
49+
runId: crypto.randomUUID(),
50+
};
51+
}
52+
53+
async function run(): Promise<void> {
54+
55+
logStep("Setting up AAD credentials");
56+
57+
// AAD auth works with az login
58+
const credentials = new DefaultAzureCredential();
59+
60+
logStep("Creating Cosmos client with AAD credentials");
61+
62+
const client = new CosmosClient({
63+
endpoint,
64+
aadCredentials: credentials,
65+
aadScope: "https://cosmos.azure.com/.default"
66+
});
67+
68+
logStep("Getting database and container references");
69+
const database = client.database(databaseId);
70+
const container = database.container(containerId);
71+
72+
logStep("Creating a test item");
73+
// Create item
74+
const testItem = getTestItem(0);
75+
const { resource: createdItem } = await container.items.create(testItem);
76+
console.log(`Created item: ${createdItem?.id}`);
77+
78+
logStep("Reading the item back");
79+
// Read item
80+
const { resource: readItem } = await container.item(testItem.id, testItem.pk).read<TestItem>();
81+
console.log("Point read:");
82+
console.log(JSON.stringify(readItem, null, 2));
83+
84+
logStep("Querying for items in the partition");
85+
// Query items
86+
const querySpec = {
87+
query: "SELECT * FROM c WHERE c.pk = @partitionKey",
88+
parameters: [
89+
{
90+
name: "@partitionKey",
91+
value: testItem.pk,
92+
},
93+
],
94+
};
95+
96+
const { resources: items } = await container.items.query<TestItem>(querySpec).fetchAll();
97+
console.log(`Found ${items.length} items in partition '${testItem.pk}':`);
98+
items.forEach((item) => {
99+
console.log(`- ${item.id}: ${item.name}`);
100+
});
101+
102+
logStep("Sample completed successfully");
103+
await finish();
104+
}
105+
106+
run().catch(handleError);

sdk/cosmosdb/cosmos/src/ClientContext.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ import { getUserAgent } from "./common/platform.js";
4646
import type { GlobalPartitionEndpointManager } from "./globalPartitionEndpointManager.js";
4747
import type { RetryOptions } from "./retry/retryOptions.js";
4848
import { PartitionKeyRangeCache } from "./routing/partitionKeyRangeCache.js";
49+
import {
50+
AAD_DEFAULT_SCOPE,
51+
AAD_AUTH_PREFIX,
52+
AAD_RESOURCE_NOT_FOUND_ERROR,
53+
} from "./common/constants.js";
4954

5055
const logger: AzureLogger = createClientLogger("ClientContext");
5156

@@ -83,17 +88,43 @@ export class ClientContext {
8388
if (cosmosClientOptions.aadCredentials) {
8489
this.pipeline = createEmptyPipeline();
8590
const hrefEndpoint = sanitizeEndpoint(cosmosClientOptions.endpoint);
86-
const scope = `${hrefEndpoint}/.default`;
91+
92+
// Use custom AAD scope if provided, otherwise use account-based scope
93+
const accountScope = `${hrefEndpoint}/.default`;
94+
const primaryScope = cosmosClientOptions.aadScope || accountScope;
95+
const fallbackScope = AAD_DEFAULT_SCOPE;
96+
8797
this.pipeline.addPolicy(
8898
bearerTokenAuthenticationPolicy({
8999
credential: cosmosClientOptions.aadCredentials,
90-
scopes: scope,
100+
scopes: primaryScope,
91101
challengeCallbacks: {
92102
async authorizeRequest({ request, getAccessToken }) {
93-
const tokenResponse = await getAccessToken([scope], {});
94-
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
95-
const authorizationToken = `${AUTH_PREFIX}${tokenResponse.token}`;
96-
request.headers.set("Authorization", authorizationToken);
103+
try {
104+
const tokenResponse = await getAccessToken([primaryScope], {});
105+
106+
const authorizationToken = `${AAD_AUTH_PREFIX}${tokenResponse.token}`;
107+
request.headers.set(Constants.HttpHeaders.Authorization, authorizationToken);
108+
} catch (error: any) {
109+
// If no custom scope is provided and we get AADSTS500011 error,
110+
// fallback to the default Cosmos scope
111+
if (
112+
!cosmosClientOptions.aadScope &&
113+
error?.message?.includes(AAD_RESOURCE_NOT_FOUND_ERROR)
114+
) {
115+
try {
116+
const fallbackTokenResponse = await getAccessToken([fallbackScope], {});
117+
const authorizationToken = `${AAD_AUTH_PREFIX}${fallbackTokenResponse.token}`;
118+
request.headers.set(Constants.HttpHeaders.Authorization, authorizationToken);
119+
} catch (fallbackError) {
120+
// If fallback also fails, throw the original error
121+
throw error;
122+
}
123+
} else {
124+
// If custom scope is provided or error is not AADSTS500011, throw the original error
125+
throw error;
126+
}
127+
}
97128
},
98129
},
99130
}),

sdk/cosmosdb/cosmos/src/CosmosClient.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ import { GlobalPartitionEndpointManager } from "./globalPartitionEndpointManager
4949
* },
5050
* });
5151
* ```
52+
* @example Instantiate a client with AAD authentication and custom scope
53+
* ```ts snippet:CosmosClientWithAADScope
54+
* import { DefaultAzureCredential } from "@azure/identity";
55+
* import { CosmosClient } from "@azure/cosmos";
56+
*
57+
* const endpoint = "https://your-account.documents.azure.com";
58+
* const aadCredentials = new DefaultAzureCredential();
59+
* const client = new CosmosClient({
60+
* endpoint,
61+
* aadCredentials,
62+
* aadScope: "https://cosmos.azure.com/.default", // Optional custom scope
63+
* });
64+
* ```
5265
*/
5366
export class CosmosClient {
5467
/**
@@ -214,6 +227,7 @@ export class CosmosClient {
214227
diagnosticLevel: optionsOrConnectionString.diagnosticLevel,
215228
pluginsConfigured: optionsOrConnectionString.plugins !== undefined,
216229
sDKVersion: Constants.SDKVersion,
230+
aadScopeOverride: optionsOrConnectionString.aadScope !== undefined,
217231
};
218232
}
219233

sdk/cosmosdb/cosmos/src/CosmosClientOptions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ export interface CosmosClientOptions {
3838
* to authenticate requests to Cosmos
3939
*/
4040
aadCredentials?: TokenCredential;
41+
/**
42+
* @internal
43+
* Optional custom AAD scope to override the default account-based scope for authentication.
44+
* If not provided, the default scope will be constructed from the endpoint URL.
45+
* When provided, no fallback mechanism will be applied if authentication fails.
46+
*/
47+
aadScope?: string;
4148
/** An array of {@link Permission} objects. */
4249
permissionFeed?: PermissionDefinition[];
4350
/** An instance of {@link ConnectionPolicy} class.

sdk/cosmosdb/cosmos/src/CosmosDiagnostics.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export type ClientConfigDiagnostic = {
9797
* SDK version
9898
*/
9999
sDKVersion: string;
100+
/**
101+
* True if `aadScope` were supplied during client initialization.
102+
*/
103+
aadScopeOverride?: boolean;
100104
};
101105

102106
/**

sdk/cosmosdb/cosmos/src/common/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ export const Constants = {
306306
RequestTimeoutForReadsInMs: 2000, // 2 seconds
307307
};
308308

309+
export const AAD_DEFAULT_SCOPE = "https://cosmos.azure.com/.default";
310+
export const AAD_AUTH_PREFIX = "type=aad&ver=1.0&sig=";
311+
export const AAD_RESOURCE_NOT_FOUND_ERROR = "AADSTS500011";
312+
309313
/**
310314
* @hidden
311315
*/

0 commit comments

Comments
 (0)