Skip to content

Commit 9f2fc51

Browse files
authored
Merge pull request #186290 from petrsvihlik/creating-credential
Managing credentials in Azure Communication Services SDKs
2 parents 2623608 + c94ba86 commit 9f2fc51

File tree

6 files changed

+295
-7
lines changed

6 files changed

+295
-7
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
---
2+
title: Azure Communication Services - Credentials best practices
3+
description: Learn more about the best practices for managing User Access Tokens in SDKs
4+
author: petrsvihlik
5+
manager: soricos
6+
services: azure-communication-services
7+
8+
ms.author: petrsvihlik
9+
ms.date: 01/30/2022
10+
ms.topic: conceptual
11+
ms.service: azure-communication-services
12+
#Customer intent: As a developer, I want learn how to correctly handle Credential objects so that I can build applications that run efficiently.
13+
---
14+
15+
# Credentials in Communication SDKs
16+
17+
This article provides best practices for managing [User Access Tokens](./authentication.md#user-access-tokens) in Azure Communication Services SDKs. Following this guidance will help you optimize the resources used by your application and reduce the number of roundtrips to the Azure Communication Identity API.
18+
19+
## Communication Token Credential
20+
21+
Communication Token Credential (Credential) is an authentication primitive that wraps User Access Tokens. It's used to authenticate users in Communication Services, such as Chat or Calling. Additionally, it provides built-in token refreshing functionality for the convenience of the developer.
22+
23+
## Initialization
24+
25+
Depending on your scenario, you may want to initialize the Credential with a [static token](#static-token) or a [callback function](#callback-function) returning tokens.
26+
No matter which method you choose, you can supply the tokens to the Credential via the Azure Communication Identity API.
27+
28+
### Static token
29+
30+
For short-lived clients, initialize the Credential with a static token. This approach is suitable for scenarios such as sending one-off Chat messages or time-limited Calling sessions.
31+
32+
```javascript
33+
const tokenCredential = new AzureCommunicationTokenCredential("<user_access_token>");
34+
```
35+
36+
### Callback function
37+
38+
For long-lived clients, initialize the Credential with a callback function that ensures a continuous authentication state during communications. This approach is suitable, for example, for long Calling sessions.
39+
40+
```javascript
41+
const tokenCredential = new AzureCommunicationTokenCredential({
42+
tokenRefresher: async (abortSignal) => fetchTokenFromMyServerForUser(abortSignal, "<user_name>")
43+
});
44+
```
45+
46+
## Token refreshing
47+
48+
To correctly implement the token refresher callback, the code must return a string with a valid JSON Web Token (JWT). It's necessary that the returned token is valid (its expiration date is set in the future) at all times. Some platforms, such as JavaScript and .NET, offer a way to abort the refresh operation, and pass `AbortSignal` or `CancellationToken` to your function. It's recommended to accept these objects, utilize them or pass them further.
49+
50+
### Example 1: Refresh token for a Communication User
51+
52+
Let's assume we have a Node.js application built on Express with the `/getToken` endpoint allowing to fetch a new valid token for a user specified by name.
53+
54+
```javascript
55+
app.post('/getToken', async (req, res) => {
56+
// Custom logic to determine the communication user id
57+
let userId = await getCommunicationUserIdFromDb(req.body.username);
58+
// Get a fresh token
59+
const identityClient = new CommunicationIdentityClient("<COMMUNICATION_SERVICES_CONNECTION_STRING>");
60+
let communicationIdentityToken = await identityClient.getToken({ communicationUserId: userId }, ["chat", "voip"]);
61+
res.json({ communicationIdentityToken: communicationIdentityToken.token });
62+
});
63+
```
64+
65+
Next, we need to implement a token refresher callback in the client application, properly utilizing the `AbortSignal` and returning an unwrapped JWT string.
66+
67+
```javascript
68+
const fetchTokenFromMyServerForUser = async function (abortSignal, username) {
69+
const response = await fetch(`${HOST_URI}/getToken`,
70+
{
71+
method: "POST",
72+
body: JSON.stringify({ username: username }),
73+
signal: abortSignal,
74+
headers: { 'Content-Type': 'application/json' }
75+
});
76+
77+
if (response.ok) {
78+
const data = await response.json();
79+
return data.communicationIdentityToken;
80+
}
81+
};
82+
```
83+
84+
### Example 2: Refresh token for a Teams User
85+
86+
Let's assume we have a Node.js application built on Express with the `/getTokenForTeamsUser` endpoint allowing to exchange an Azure Active Directory (Azure AD) access token of a Teams user for a new Communication Identity access token with a matching expiration time.
87+
88+
```javascript
89+
app.post('/getTokenForTeamsUser', async (req, res) => {
90+
const identityClient = new CommunicationIdentityClient("<COMMUNICATION_SERVICES_CONNECTION_STRING>");
91+
let communicationIdentityToken = await identityClient.getTokenForTeamsUser(req.body.teamsToken);
92+
res.json({ communicationIdentityToken: communicationIdentityToken.token });
93+
});
94+
```
95+
96+
Next, we need to implement a token refresher callback in the client application, whose responsibility will be to:
97+
98+
1. Refresh the Azure AD access token of the Teams User
99+
1. Exchange the Azure AD access token of the Teams User for a Communication Identity access token
100+
101+
```javascript
102+
const fetchTokenFromMyServerForUser = async function (abortSignal, username) {
103+
// 1. Refresh the Azure AD access token of the Teams User
104+
let teamsTokenResponse = await refreshAadToken(abortSignal, username);
105+
106+
// 2. Exchange the Azure AD access token of the Teams User for a Communication Identity access token
107+
const response = await fetch(`${HOST_URI}/getTokenForTeamsUser`,
108+
{
109+
method: "POST",
110+
body: JSON.stringify({ teamsToken: teamsTokenResponse.accessToken }),
111+
signal: abortSignal,
112+
headers: { 'Content-Type': 'application/json' }
113+
});
114+
115+
if (response.ok) {
116+
const data = await response.json();
117+
return data.communicationIdentityToken;
118+
}
119+
}
120+
```
121+
122+
In this example, we use the Microsoft Authentication Library (MSAL) to refresh the Azure AD access token. Following the guide to [acquire an Azure AD token to call an API](../../active-directory/develop/scenario-spa-acquire-token.md), we first try to obtain the token without the user's interaction. If that's not possible, we trigger one of the interactive flows.
123+
124+
```javascript
125+
const refreshAadToken = async function (abortSignal, username) {
126+
if (abortSignal.aborted === true) throw new Error("Operation canceled");
127+
128+
// MSAL.js v2 exposes several account APIs; the logic to determine which account to use is the responsibility of the developer.
129+
// In this case, we'll use an account from the cache.
130+
let account = (await publicClientApplication.getTokenCache().getAllAccounts()).find(u => u.username === username);
131+
132+
const renewRequest = {
133+
scopes: ["https://auth.msft.communication.azure.com/Teams.ManageCalls"],
134+
account: account,
135+
forceRefresh: forceRefresh
136+
};
137+
let tokenResponse = null;
138+
// Try to get the token silently without the user's interaction
139+
await publicClientApplication.acquireTokenSilent(renewRequest).then(renewResponse => {
140+
tokenResponse = renewResponse;
141+
}).catch(async (error) => {
142+
// In case of an InteractionRequired error, send the same request in an interactive call
143+
if (error instanceof InteractionRequiredAuthError) {
144+
// You can choose the popup or redirect experience (`acquireTokenPopup` or `acquireTokenRedirect` respectively)
145+
publicClientApplication.acquireTokenPopup(renewRequest).then(function (renewInteractiveResponse) {
146+
tokenResponse = renewInteractiveResponse;
147+
}).catch(function (interactiveError) {
148+
console.log(interactiveError);
149+
});
150+
}
151+
});
152+
return tokenResponse;
153+
}
154+
```
155+
156+
## Initial token
157+
158+
To further optimize your code, you can fetch the token at the application's startup and pass it to the Credential directly. Providing an initial token will skip the first call to the refresher callback function while preserving all subsequent calls to it.
159+
160+
```javascript
161+
const tokenCredential = new AzureCommunicationTokenCredential({
162+
tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_id>"),
163+
token: "<initial_token>"
164+
});
165+
```
166+
167+
## Proactive token refreshing
168+
169+
Use proactive refreshing to eliminate any possible delay during the on-demand fetching of the token. The proactive refreshing will refresh the token in the background at the end of its lifetime. When the token is about to expire, 10 minutes before the end of its validity, the Credential will start attempting to retrieve the token. It will trigger the refresher callback with increasing frequency until it succeeds and retrieves a token with long enough validity.
170+
171+
```javascript
172+
const tokenCredential = new AzureCommunicationTokenCredential({
173+
tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_id>"),
174+
refreshProactively: true
175+
});
176+
```
177+
178+
If you want to cancel scheduled refresh tasks, [dispose](#clean-up-resources) of the Credential object.
179+
180+
### Proactively refresh token for a Teams User
181+
182+
To minimize the number of roundtrips to the Azure Communication Identity API, make sure the Azure AD token you're passing for an [exchange](../quickstarts/manage-teams-identity.md#step-3-exchange-the-azure-ad-access-token-of-the-teams-user-for-a-communication-identity-access-token) has long enough validity (> 10 minutes). In case that MSAL returns a cached token with a shorter validity, you have the following options to bypass the cache:
183+
184+
1. Refresh the token forcibly
185+
1. Increase the MSAL's token renewal window to more than 10 minutes
186+
187+
# [JavaScript](#tab/javascript)
188+
189+
Option 1: Trigger the token acquisition flow with [`AuthenticationParameters.forceRefresh`](../../active-directory/develop/msal-js-pass-custom-state-authentication-request.md) set to `true`.
190+
191+
```javascript
192+
// Extend the `refreshAadToken` function
193+
const refreshAadToken = async function (abortSignal, username) {
194+
195+
// ... existing refresh logic
196+
197+
// Make sure the token has at least 10-minute lifetime and if not, force-renew it
198+
if (tokenResponse.expiresOn < (Date.now() + (10 * 60 * 1000))) {
199+
const renewRequest = {
200+
scopes: ["https://auth.msft.communication.azure.com/Teams.ManageCalls"],
201+
account: account,
202+
forceRefresh: true // Force-refresh the token
203+
};
204+
205+
await publicClientApplication.acquireTokenSilent(renewRequest).then(renewResponse => {
206+
tokenResponse = renewResponse;
207+
});
208+
}
209+
}
210+
```
211+
212+
Option 2: Initialize the MSAL authentication context by instantiating a `PublicClientApplication` with a custom [`SystemOptions.tokenRenewalOffsetSeconds`](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#systemoptions-1).
213+
214+
```javascript
215+
const publicClientApplication = new PublicClientApplication({
216+
system: {
217+
tokenRenewalOffsetSeconds: 900 // 15 minutes (by default 5 minutes)
218+
});
219+
```
220+
221+
---
222+
223+
## Cancel refreshing
224+
225+
For the Communication clients to be able to cancel ongoing refresh tasks, it's necessary to pass a cancellation object to the refresher callback.
226+
*Note that this pattern applies only to JavaScript and .NET.*
227+
228+
```javascript
229+
var controller = new AbortController();
230+
var signal = controller.signal;
231+
232+
var joinChatBtn = document.querySelector('.joinChat');
233+
var leaveChatBtn = document.querySelector('.leaveChat');
234+
235+
joinChatBtn.addEventListener('click', function() {
236+
// Wrong:
237+
const tokenCredentialWrong = new AzureCommunicationTokenCredential({
238+
tokenRefresher: async () => fetchTokenFromMyServerForUser("<user_name>")
239+
});
240+
241+
// Correct: Pass abortSignal through the arrow function
242+
const tokenCredential = new AzureCommunicationTokenCredential({
243+
tokenRefresher: async (abortSignal) => fetchTokenFromMyServerForUser(abortSignal, "<user_name>")
244+
});
245+
246+
// ChatClient is now able to abort token refresh tasks
247+
const chatClient = new ChatClient("<endpoint-url>", tokenCredential);
248+
249+
// ...
250+
});
251+
252+
leaveChatBtn.addEventListener('click', function() {
253+
controller.abort();
254+
console.log('Leaving chat...');
255+
});
256+
```
257+
258+
### Clean up resources
259+
260+
Communication Services applications should dispose the Credential instance when it's no longer needed. Disposing the credential is also the recommended way of canceling scheduled refresh actions when the proactive refreshing is enabled.
261+
262+
Call the `.dispose()` function.
263+
264+
```javascript
265+
const tokenCredential = new AzureCommunicationTokenCredential("<token>");
266+
// Use the credential for Calling or Chat
267+
const chatClient = new ChatClient("<endpoint-url>", tokenCredential);
268+
// ...
269+
tokenCredential.dispose()
270+
```
271+
272+
---
273+
274+
## Next steps
275+
276+
In this article, you learned how to:
277+
278+
> [!div class="checklist"]
279+
> * Correctly initialize and dispose of a Credential object
280+
> * Implement a token refresher callback
281+
> * Optimize your token refreshing logic
282+
283+
To learn more, you may want to explore the following quickstart guides:
284+
285+
* [Create and manage access tokens](../quickstarts/access-tokens.md)
286+
* [Manage access tokens for Teams users](../quickstarts/manage-teams-identity.md)

articles/communication-services/quickstarts/includes/manage-teams-identity-java.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ CommunicationIdentityClient communicationIdentityClient = new CommunicationIdent
123123
.buildClient();
124124
```
125125

126-
### Step 3: Exchange the Azure AD user token for the Teams access token
126+
### Step 3: Exchange the Azure AD access token of the Teams User for a Communication Identity access token
127127

128128
Use the `getTokenForTeamsUser` method to issue an access token for the Teams user that can be used with the Azure Communication Services SDKs.
129129

articles/communication-services/quickstarts/includes/manage-teams-identity-js.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ app.get('/redirect', async (req, res) => {
115115
};
116116
117117
pca.acquireTokenByCode(tokenRequest).then((response) => {
118-
console.log("Response: ", response);
118+
console.log("Response:", response);
119119
//TODO: the following code snippets go here
120120
res.sendStatus(200);
121121
}).catch((error) => {
@@ -140,14 +140,14 @@ const connectionString = process.env['COMMUNICATION_SERVICES_CONNECTION_STRING']
140140
const identityClient = new CommunicationIdentityClient(connectionString);
141141
```
142142
143-
### Step 3: Exchange the Azure AD user token for the Teams access token
143+
### Step 3: Exchange the Azure AD access token of the Teams User for a Communication Identity access token
144144
145145
Use the `getTokenForTeamsUser` method to issue an access token for the Teams user that can be used with the Azure Communication Services SDKs.
146146
147147
```javascript
148148
let teamsToken = response.accessToken;
149149
let accessToken = await identityClient.getTokenForTeamsUser(teamsToken);
150-
console.log(`Token: ${accessToken}`);
150+
console.log("Token:", accessToken);
151151
```
152152
153153
## Run the code

articles/communication-services/quickstarts/includes/manage-teams-identity-net.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ string connectionString = Environment.GetEnvironmentVariable("COMMUNICATION_SERV
110110
var client = new CommunicationIdentityClient(connectionString);
111111
```
112112

113-
### Step 3: Exchange the Azure AD user token for the Teams access token
113+
### Step 3: Exchange the Azure AD access token of the Teams User for a Communication Identity access token
114114

115115
Use the `GetTokenForTeamsUser` method to issue an access token for the Teams user that can be used with the Azure Communication Services SDKs.
116116

articles/communication-services/quickstarts/includes/manage-teams-identity-python.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ connection_string = os.environ["COMMUNICATION_SERVICES_CONNECTION_STRING"]
8282
client = CommunicationIdentityClient.from_connection_string(connection_string)
8383
```
8484

85-
### Step 3: Exchange the Azure AD user token for the Teams access token
85+
### Step 3: Exchange the Azure AD access token of the Teams User for a Communication Identity access token
8686

8787
Use the `get_token_for_teams_user` method to issue an access token for the Teams user that can be used with the Azure Communication Services SDKs.
8888

articles/communication-services/toc.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ items:
101101
href: concepts/identity-model.md
102102
- name: Authenticating services
103103
href: concepts/authentication.md
104+
- name: Credentials best practices
105+
href: concepts/credentials-best-practices.md
104106
- name: Chat
105107
items:
106108
- name: Chat SDK overview
@@ -179,7 +181,7 @@ items:
179181
href: concepts/logging-and-diagnostics.md
180182
- name: Metrics
181183
href: concepts/metrics.md
182-
- name: Service Events (EventGrid)
184+
- name: Service Events (Event Grid)
183185
items:
184186
- name: Overview
185187
href: ../event-grid/event-schema-communication-services.md

0 commit comments

Comments
 (0)