|
| 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) |
0 commit comments