Skip to content

Commit 55e7ac9

Browse files
authored
[Identity] Add token provider (#28645)
In our work to add support for AAD authentication in external clients, we need a public API to get access tokens. This PR adds one similar to Python's one added in Azure/azure-sdk-for-python#32655. ~TODO: add tests and samples~.
1 parent e713574 commit 55e7ac9

7 files changed

Lines changed: 167 additions & 1 deletion

File tree

sdk/identity/identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- Adds support for `getBearerTokenProvider` that returns a callback function to get a token for a given scope. This is useful for scenarios where an explicit Entra token is needed without having to worry about the token refreshing details.
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/identity/identity/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "js",
44
"TagPrefix": "js/identity/identity",
5-
"Tag": "js/identity/identity_9b15d10264"
5+
"Tag": "js/identity/identity_58e656fd32"
66
}

sdk/identity/identity/review/identity.api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CommonClientOptions } from '@azure/core-client';
1010
import { GetTokenOptions } from '@azure/core-auth';
1111
import { LogPolicyOptions } from '@azure/core-rest-pipeline';
1212
import { TokenCredential } from '@azure/core-auth';
13+
import type { TracingContext } from '@azure/core-auth';
1314

1415
export { AccessToken }
1516

@@ -288,6 +289,17 @@ export interface ErrorResponse {
288289
traceId?: string;
289290
}
290291

292+
// @public
293+
export function getBearerTokenProvider(credential: TokenCredential, scopes: string | string[], options?: GetBearerTokenProviderOptions): () => Promise<string>;
294+
295+
// @public
296+
export interface GetBearerTokenProviderOptions {
297+
abortSignal?: AbortSignal;
298+
tracingOptions?: {
299+
tracingContext?: TracingContext;
300+
};
301+
}
302+
291303
// @public
292304
export function getDefaultAzureCredential(): TokenCredential;
293305

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
/**
5+
* @summary demonstrates how to get a bearer token.
6+
*/
7+
8+
import { PipelineRequest, createPipelineRequest } from "@azure/core-rest-pipeline";
9+
import { getBearerTokenProvider, DefaultAzureCredential } from "@azure/identity";
10+
import { config } from "dotenv";
11+
12+
// Load the .env file if it exists
13+
config();
14+
15+
export async function main(): Promise<void> {
16+
const credential = new DefaultAzureCredential();
17+
const scope = "https://cognitiveservices.azure.com/.default";
18+
const getAccessToken = getBearerTokenProvider(credential, scope);
19+
const token = await getAccessToken();
20+
21+
// create a request
22+
const request: PipelineRequest = createPipelineRequest({ url: "https://example.com" });
23+
// add the access token to the request
24+
request.headers.set("Authorization", `Bearer ${token}`);
25+
console.log("Authorization header has been added to the request");
26+
}
27+
28+
main().catch((err) => {
29+
console.log("error code: ", err.code);
30+
console.log("error message: ", err.message);
31+
console.log("error stack: ", err.stack);
32+
});

sdk/identity/identity/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,5 @@ export { AzureAuthorityHosts } from "./constants";
113113
export function getDefaultAzureCredential(): TokenCredential {
114114
return new DefaultAzureCredential();
115115
}
116+
117+
export { getBearerTokenProvider, GetBearerTokenProviderOptions } from "./tokenProvider";
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import type { TokenCredential, TracingContext } from "@azure/core-auth";
5+
import {
6+
bearerTokenAuthenticationPolicy,
7+
createEmptyPipeline,
8+
createPipelineRequest,
9+
} from "@azure/core-rest-pipeline";
10+
11+
/**
12+
* The options to configure the token provider.
13+
*/
14+
export interface GetBearerTokenProviderOptions {
15+
/** The abort signal to abort requests to get tokens */
16+
abortSignal?: AbortSignal;
17+
/** The tracing options for the requests to get tokens */
18+
tracingOptions?: {
19+
/**
20+
* Tracing Context for the current request to get a token.
21+
*/
22+
tracingContext?: TracingContext;
23+
};
24+
}
25+
26+
/**
27+
* Returns a callback that provides a bearer token.
28+
* For example, the bearer token can be used to authenticate a request as follows:
29+
* ```js
30+
* import { DefaultAzureCredential } from "@azure/identity";
31+
*
32+
* const credential = new DefaultAzureCredential();
33+
* const scope = "https://cognitiveservices.azure.com/.default";
34+
* const getAccessToken = getBearerTokenProvider(credential, scope);
35+
* const token = await getAccessToken();
36+
*
37+
* // usage
38+
* const request = createPipelineRequest({ url: "https://example.com" });
39+
* request.headers.set("Authorization", `Bearer ${token}`);
40+
* ```
41+
*
42+
* @param credential - The credential used to authenticate the request.
43+
* @param scopes - The scopes required for the bearer token.
44+
* @param options - Options to configure the token provider.
45+
* @returns a callback that provides a bearer token.
46+
*/
47+
export function getBearerTokenProvider(
48+
credential: TokenCredential,
49+
scopes: string | string[],
50+
options?: GetBearerTokenProviderOptions,
51+
): () => Promise<string> {
52+
const { abortSignal, tracingOptions } = options || {};
53+
const pipeline = createEmptyPipeline();
54+
pipeline.addPolicy(bearerTokenAuthenticationPolicy({ credential, scopes }));
55+
async function getRefreshedToken(): Promise<string> {
56+
// Create a pipeline with just the bearer token policy
57+
// and run a dummy request through it to get the token
58+
const res = await pipeline.sendRequest(
59+
{
60+
sendRequest: (request) =>
61+
Promise.resolve({
62+
request,
63+
status: 200,
64+
headers: request.headers,
65+
}),
66+
},
67+
createPipelineRequest({
68+
url: "https://example.com",
69+
abortSignal,
70+
tracingOptions,
71+
}),
72+
);
73+
const accessToken = res.headers.get("authorization")?.split(" ")[1];
74+
if (!accessToken) {
75+
throw new Error("Failed to get access token");
76+
}
77+
return accessToken;
78+
}
79+
return getRefreshedToken;
80+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { EnvironmentCredential, getBearerTokenProvider } from "../../../src";
5+
import { MsalTestCleanup, msalNodeTestSetup } from "../../node/msalNodeTestSetup";
6+
import { Recorder, delay, isPlaybackMode } from "@azure-tools/test-recorder";
7+
import { Context } from "mocha";
8+
import { assert } from "@azure/test-utils";
9+
10+
describe("getBearerTokenProvider", function () {
11+
let cleanup: MsalTestCleanup;
12+
let recorder: Recorder;
13+
14+
beforeEach(async function (this: Context) {
15+
const setup = await msalNodeTestSetup(this.currentTest);
16+
recorder = setup.recorder;
17+
cleanup = setup.cleanup;
18+
});
19+
afterEach(async function () {
20+
await cleanup();
21+
});
22+
23+
const scope = "https://vault.azure.net/.default";
24+
25+
it("returns a callback that returns string tokens", async function () {
26+
const credential = new EnvironmentCredential(recorder.configureClientOptions({}));
27+
28+
const getAccessToken = getBearerTokenProvider(credential, scope);
29+
30+
for (let i = 0; i < 5; i++) {
31+
if (!isPlaybackMode()) {
32+
await delay(500);
33+
}
34+
const token = await getAccessToken();
35+
assert.isString(token);
36+
}
37+
});
38+
});

0 commit comments

Comments
 (0)