Skip to content

Commit 36015f9

Browse files
[OPS Common SDK] Updating Communication Common SDK to version supporting Teams Phone Extensibility GA version (Azure#50140)
* Update API version Update API version for TeamsExtension API called from CommonSDK * Ensure that null/empty scopes are handled when setting, Validate Scopes in options Never allow empty Scopes in EntraCommunicationTokenCredentialOptions * New version changelog and readme
1 parent d18cd24 commit 36015f9

File tree

7 files changed

+110
-30
lines changed

7 files changed

+110
-30
lines changed

sdk/communication/Azure.Communication.Common/CHANGELOG.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
# Release History
22

3-
## 1.4.0-beta.2 (Unreleased)
3+
## 1.4.0 (Unreleased)
44

55
### Features Added
6-
7-
### Breaking Changes
8-
9-
### Bugs Fixed
10-
11-
### Other Changes
6+
- Introduced support for `Azure.Core.TokenCredential` with `EntraCommunicationTokenCredentialOptions`, allowing an Entra user with a Teams license to use Teams Phone Extensibility features through the Azure Communication Services resource.
7+
- Added support for a new communication identifier `TeamsExtensionUserIdentifier` which maps rawIds with format `8:acs:{resourceId}_{tenantId}_{userId}`.
8+
- Added `IsAnonymous` and `AssertedId` properties to `PhoneNumberIdentifier`.
129

1310
## 1.4.0-beta.1 (2025-04-01)
1411

sdk/communication/Azure.Communication.Common/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ using var tokenCredential = new CommunicationTokenCredential(
106106

107107
For scenarios where an Entra user can be used with Communication Services, you need to initialize any implementation of [Azure.Core.TokenCredential](https://docs.microsoft.com/dotnet/api/azure.core.tokencredential?view=azure-dotnet) and provide it to the ``EntraCommunicationTokenCredentialOptions``.
108108
Along with this, you must provide the URI of the Azure Communication Services resource and the scopes required for the Entra user token. These scopes determine the permissions granted to the token.
109-
If the scopes are not provided, by default, it sets the scopes to `https://communication.azure.com/clients/.default`.
109+
110+
This approach needs to be used for authorizing an Entra user with a Teams license to use Teams Phone Extensibility features through your Azure Communication Services resource.
111+
This requires providing the `https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls` scope.
110112
```C#
111113
var options = new InteractiveBrowserCredentialOptions
112114
{
@@ -119,16 +121,18 @@ var entraTokenCredential = new InteractiveBrowserCredential(options);
119121
var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions(
120122
resourceEndpoint: "https://<your-resource>.communication.azure.com",
121123
entraTokenCredential: entraTokenCredential)
124+
)
122125
{
123-
Scopes = new[] { "https://communication.azure.com/clients/VoIP" }
126+
Scopes = new[] { "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls" }
124127
};
125128

126129
var credential = new CommunicationTokenCredential(entraTokenCredentialOptions);
127130

128131
```
129132

130-
The same approach can be used for authorizing an Entra user with a Teams license to use Teams Phone Extensibility features through your Azure Communication Services resource.
131-
This requires providing the `https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls` scope
133+
Other scenarios for Entra users to utilize Azure Communication Services are currently in the **preview stage only and should not be used in production**.
134+
The scopes for these scenarios follow the format `https://communication.azure.com/clients/<Azure Communication Services Clients API permission>`.
135+
If specific scopes are not provided, the default scopes will be set to `https://communication.azure.com/clients/.default`.
132136
```C#
133137
var options = new InteractiveBrowserCredentialOptions
134138
{
@@ -141,9 +145,8 @@ var entraTokenCredential = new InteractiveBrowserCredential(options);
141145
var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions(
142146
resourceEndpoint: "https://<your-resource>.communication.azure.com",
143147
entraTokenCredential: entraTokenCredential)
144-
)
145148
{
146-
Scopes = new[] { "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls" }
149+
Scopes = new[] { "https://communication.azure.com/clients/VoIP" }
147150
};
148151

149152
var credential = new CommunicationTokenCredential(entraTokenCredentialOptions);

sdk/communication/Azure.Communication.Common/src/Azure.Communication.Common.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
For this release, see notes - https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/communication/Azure.Communication.Common/README.md and https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/communication/Azure.Communication.Common/CHANGELOG.md.
66
</Description>
77
<AssemblyTitle>Azure Communication Services Common</AssemblyTitle>
8-
<Version>1.4.0-beta.2</Version>
8+
<Version>1.4.0</Version>
99
<!--The ApiCompatVersion is managed automatically and should not generally be modified manually.-->
1010
<ApiCompatVersion>1.3.0</ApiCompatVersion>
1111
<PackageTags>Microsoft Azure Communication Service;Microsoft;Azure;Azure Communication Service;Communication;$(PackageCommonTags)</PackageTags>

sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Linq;
56
using Azure.Core;
67

78
namespace Azure.Communication
@@ -11,7 +12,9 @@ namespace Azure.Communication
1112
/// </summary>
1213
public class EntraCommunicationTokenCredentialOptions
1314
{
14-
private static string[] DefaultScopes = { "https://communication.azure.com/clients/.default" };
15+
private static string[] DefaultScopes = { EntraCommunicationTokenScopes.DefaultScopes };
16+
private string[] _scopes;
17+
1518
/// <summary>
1619
/// The URI of the Azure Communication Services resource.
1720
/// </summary>
@@ -25,7 +28,13 @@ public class EntraCommunicationTokenCredentialOptions
2528
/// <summary>
2629
/// The scopes required for the Entra user token. These scopes determine the permissions granted to the token. For example, ["https://communication.azure.com/clients/VoIP"].
2730
/// </summary>
28-
public string[] Scopes { get; set; }
31+
public string[] Scopes {
32+
get => _scopes;
33+
set
34+
{
35+
_scopes = ValidateScopes(value);
36+
}
37+
}
2938

3039
/// <summary>
3140
/// Initializes a new instance of <see cref="EntraCommunicationTokenCredentialOptions"/>.
@@ -43,5 +52,22 @@ public EntraCommunicationTokenCredentialOptions(
4352
this.TokenCredential = entraTokenCredential;
4453
this.Scopes = DefaultScopes;
4554
}
55+
56+
private static string[] ValidateScopes(string[] scopes)
57+
{
58+
if (scopes == null || scopes.Length == 0)
59+
{
60+
throw new ArgumentException(
61+
$"Scopes must not be null or empty. Ensure all scopes start with either {EntraCommunicationTokenScopes.TeamsExtensionScopePrefix} or {EntraCommunicationTokenScopes.CommunicationClientsScopePrefix}.", nameof(scopes));
62+
}
63+
64+
if (scopes.All(item => item.StartsWith(EntraCommunicationTokenScopes.TeamsExtensionScopePrefix))
65+
|| scopes.All(item => item.StartsWith(EntraCommunicationTokenScopes.CommunicationClientsScopePrefix)))
66+
{
67+
return scopes;
68+
}
69+
70+
throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {EntraCommunicationTokenScopes.TeamsExtensionScopePrefix} or {EntraCommunicationTokenScopes.CommunicationClientsScopePrefix}.", nameof(_scopes));
71+
}
4672
}
4773
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Communication
5+
{
6+
/// <summary>
7+
/// The Entra Communication Token Options.
8+
/// </summary>
9+
internal static class EntraCommunicationTokenScopes
10+
{
11+
public const string TeamsExtensionScopePrefix = "https://auth.msft.communication.azure.com/";
12+
public const string CommunicationClientsScopePrefix = "https://communication.azure.com/clients/";
13+
public const string DefaultScopes = CommunicationClientsScopePrefix + ".default";
14+
}
15+
}

sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@ namespace Azure.Communication
1717
/// </summary>
1818
internal sealed class EntraTokenCredential : ICommunicationTokenCredential
1919
{
20-
private const string TeamsExtensionScopePrefix = "https://auth.msft.communication.azure.com/";
21-
private const string CommunicationClientsScopePrefix = "https://communication.azure.com/clients/";
2220
private const string TeamsExtensionEndpoint = "/access/teamsExtension/:exchangeAccessToken";
23-
private const string TeamsExtensionApiVersion = "2025-03-02-preview";
21+
private const string TeamsExtensionApiVersion = "2025-06-30";
2422
private const string CommunicationClientsEndpoint = "/access/entra/:exchangeAccessToken";
2523
private const string CommunicationClientsApiVersion = "2025-03-02-preview";
2624

@@ -36,6 +34,10 @@ internal sealed class EntraTokenCredential : ICommunicationTokenCredential
3634
/// <param name="pipelineTransport">Only for testing.</param>
3735
public EntraTokenCredential(EntraCommunicationTokenCredentialOptions options, HttpPipelineTransport pipelineTransport = null)
3836
{
37+
Argument.AssertNotNull(options, nameof(options));
38+
// Should not ever happen, validated in EntraCommunicationTokenCredentialOptions
39+
Argument.AssertNotNullOrEmpty(options.Scopes, nameof(options.Scopes));
40+
3941
this._resourceEndpoint = options.ResourceEndpoint;
4042
this._scopes = (ICollection<string>)options.Scopes.Clone();
4143
_pipeline = CreatePipelineFromOptions(options, pipelineTransport);
@@ -134,21 +136,17 @@ private RequestUriBuilder CreateRequestUri()
134136

135137
private (string Endpoint, string ApiVersion) DetermineEndpointAndApiVersion()
136138
{
137-
if (_scopes == null || !_scopes.Any())
138-
{
139-
throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtensionScopePrefix} or {CommunicationClientsScopePrefix}.", nameof(_scopes));
140-
}
141-
else if (_scopes.All(item => item.StartsWith(TeamsExtensionScopePrefix)))
139+
if (_scopes.All(item => item.StartsWith(EntraCommunicationTokenScopes.TeamsExtensionScopePrefix)))
142140
{
143141
return (TeamsExtensionEndpoint, TeamsExtensionApiVersion);
144142
}
145-
else if (_scopes.All(item => item.StartsWith(CommunicationClientsScopePrefix)))
143+
else if (_scopes.All(item => item.StartsWith(EntraCommunicationTokenScopes.CommunicationClientsScopePrefix)))
146144
{
147145
return (CommunicationClientsEndpoint, CommunicationClientsApiVersion);
148146
}
149147
else
150148
{
151-
throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtensionScopePrefix} or {CommunicationClientsScopePrefix}.", nameof(_scopes));
149+
throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {EntraCommunicationTokenScopes.TeamsExtensionScopePrefix} or {EntraCommunicationTokenScopes.CommunicationClientsScopePrefix}.", nameof(_scopes));
152150
}
153151
}
154152

sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ public class EntraTokenCredentialTest
4343
new object[] { new string[] { teamsExtensionScope, communicationClientsScope } },
4444
new object[] { new string[] { "invalidScope" } },
4545
new object[] { new string[] { "" } },
46-
new object[] { new string[] { } }
4746
};
4847

4948
[SetUp]
@@ -57,7 +56,7 @@ public void Setup()
5756
}
5857

5958
[Test, TestCaseSource(nameof(validScopes))]
60-
public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes)
59+
public void EntraCommunicationTokenCredentialOptions_Init_ThrowsErrorWithNulls(string[] scopes)
6160
{
6261
Assert.Throws<ArgumentNullException>(() => new EntraCommunicationTokenCredentialOptions(
6362
null,
@@ -82,7 +81,35 @@ public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes)
8281
}
8382

8483
[Test]
85-
public void EntraTokenCredential_InitWithoutScopes_InitsWithDefaultScope()
84+
public void EntraCommunicationTokenCredentialOptions_NullOrEmptyScopes_ThrowsError()
85+
{
86+
Assert.Throws<ArgumentException>(() => new EntraCommunicationTokenCredentialOptions(
87+
_resourceEndpoint,
88+
_mockTokenCredential.Object)
89+
{
90+
Scopes = null
91+
});
92+
Assert.Throws<ArgumentException>(() => new EntraCommunicationTokenCredentialOptions(
93+
_resourceEndpoint,
94+
_mockTokenCredential.Object)
95+
{
96+
Scopes = Array.Empty<string>()
97+
});
98+
}
99+
100+
[Test, TestCaseSource(nameof(invalidScopes))]
101+
public void EntraCommunicationTokenCredentialOptions_InvalidScopes_ThrowsForInvalidScopes(string[] scopes)
102+
{
103+
Assert.Throws<ArgumentException>(() => new EntraCommunicationTokenCredentialOptions(
104+
_resourceEndpoint,
105+
_mockTokenCredential.Object)
106+
{
107+
Scopes = scopes
108+
});
109+
}
110+
111+
[Test]
112+
public void EntraCommunicationTokenCredentialOptions_InitWithoutScopes_InitsWithDefaultScope()
86113
{
87114
var credential = new EntraCommunicationTokenCredentialOptions(
88115
_resourceEndpoint,
@@ -91,6 +118,15 @@ public void EntraTokenCredential_InitWithoutScopes_InitsWithDefaultScope()
91118
Assert.AreEqual(credential.Scopes, scopes);
92119
}
93120

121+
[Test]
122+
public void EntraTokenCredential_Ctor_NullOptionsThrows()
123+
{
124+
// Arrange
125+
var mockTransport = CreateMockTransport(new[] { CreateMockResponse(200, TokenResponse) });
126+
// Assert
127+
Assert.Throws<ArgumentNullException>(() => new EntraTokenCredential(null, mockTransport));
128+
}
129+
94130
[Test, TestCaseSource(nameof(validScopes))]
95131
public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes)
96132
{
@@ -261,7 +297,12 @@ public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError(stri
261297
public void EntraTokenCredential_GetToken_ThrowsForInvalidScopes(string[] scopes)
262298
{
263299
// Arrange
264-
var options = CreateEntraTokenCredentialOptions(scopes);
300+
var options = new EntraCommunicationTokenCredentialOptions(_resourceEndpoint, _mockTokenCredential.Object)
301+
{
302+
Scopes = Enumerable.Repeat(communicationClientsScope, scopes.Length).ToArray()
303+
};
304+
scopes.CopyTo(options.Scopes, 0);
305+
265306
var mockResponses = new MockResponse[]
266307
{
267308
CreateMockResponse(200, TokenResponse)

0 commit comments

Comments
 (0)