Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ACS][Common] OPS - Added Routing Logic Based on Scopes #47609

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 1.4.0-beta.1 (Unreleased)

### Features Added
- Introduced support for `Azure.Core.TokenCredential` with `EntraCommunicationTokenCredentialOptions`, enabling Entra users to authorize Communication Services and allowing an Entra user with a Teams license to use Teams Phone Extensibility features through the Azure Communication Services resource.

### Breaking Changes

Expand Down
36 changes: 29 additions & 7 deletions sdk/communication/Azure.Communication.Common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,46 @@ using var tokenCredential = new CommunicationTokenCredential(
### Create a credential with a token credential capable of obtaining an Entra user token

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``.
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:

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.
If the scopes are not provided, by default, it sets the scopes to `https://communication.azure.com/clients/.default`.
```C#
var options = new InteractiveBrowserCredentialOptions
{
TenantId = "<your-tenant-id>",
ClientId = "<your-client-id>",
RedirectUri = new Uri("<your-redirect-uri>"),
AuthorityHost = new Uri("https://login.microsoftonline.com/<your-tenant-id>")
RedirectUri = new Uri("<your-redirect-uri>")
};
var entraTokenCredential = new InteractiveBrowserCredential(options);

var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions(
resourceEndpoint: "https://<your-resource>.communication.azure.com",
entraTokenCredential: entraTokenCredential
){
entraTokenCredential: entraTokenCredential)
{
Scopes = new[] { "https://communication.azure.com/clients/VoIP" }
};
};

var credential = new CommunicationTokenCredential(entraTokenCredentialOptions);

```

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.
This requires providing the `https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls` scope
```C#
var options = new InteractiveBrowserCredentialOptions
{
TenantId = "<your-tenant-id>",
ClientId = "<your-client-id>",
RedirectUri = new Uri("<your-redirect-uri>")
};
var entraTokenCredential = new InteractiveBrowserCredential(options);

var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions(
resourceEndpoint: "https://<your-resource>.communication.azure.com",
entraTokenCredential: entraTokenCredential)
)
{
Scopes = new[] { "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls" }
};

var credential = new CommunicationTokenCredential(entraTokenCredentialOptions);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -15,8 +16,16 @@ namespace Azure.Communication
/// </summary>
internal sealed class EntraTokenCredential : ICommunicationTokenCredential
{
private const string TeamsExtensionScopePrefix = "https://auth.msft.communication.azure.com/";
private const string ComunicationClientsScopePrefix = "https://communication.azure.com/clients/";
private const string TeamsExtensionEndpoint = "/access/teamsPhone/:exchangeAccessToken";
private const string TeamsExtensionApiVersion = "2025-03-02-preview";
private const string ComunicationClientsEndpoint = "/access/entra/:exchangeAccessToken";
private const string ComunicationClientsApiVersion = "2024-04-01-preview";

private HttpPipeline _pipeline;
private string _resourceEndpoint;
private string[] _scopes { get; set; }
private readonly ThreadSafeRefreshableAccessTokenCache _accessTokenCache;

/// <summary>
Expand All @@ -27,6 +36,7 @@ internal sealed class EntraTokenCredential : ICommunicationTokenCredential
public EntraTokenCredential(EntraCommunicationTokenCredentialOptions options, HttpPipelineTransport pipelineTransport = null)
{
this._resourceEndpoint = options.ResourceEndpoint;
this._scopes = options.Scopes;
_pipeline = CreatePipelineFromOptions(options, pipelineTransport);
_accessTokenCache = new ThreadSafeRefreshableAccessTokenCache(
ExchangeEntraToken,
Expand Down Expand Up @@ -99,14 +109,9 @@ private async ValueTask<AccessToken> ExchangeEntraTokenAsync(bool async, Cancell

private HttpMessage CreateRequestMessage()
{
var uri = new RequestUriBuilder();
uri.Reset(new Uri(_resourceEndpoint));
uri.AppendPath("/access/entra/:exchangeAccessToken", false);
uri.AppendQuery("api-version", "2024-04-01-preview", true);

var message = _pipeline.CreateMessage();
var request = message.Request;
request.Uri = uri;
request.Uri = CreateRequestUri();
request.Method = RequestMethod.Post;
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Content-Type", "application/json");
Expand All @@ -115,6 +120,37 @@ private HttpMessage CreateRequestMessage()
return message;
}

private RequestUriBuilder CreateRequestUri()
{
var uri = new RequestUriBuilder();
uri.Reset(new Uri(_resourceEndpoint));

var (endpoint, apiVersion) = DetermineEndpointAndApiVersion();
uri.AppendPath(endpoint, false);
uri.AppendQuery("api-version", apiVersion, true);
return uri;
}

private (string Endpoint, string ApiVersion) DetermineEndpointAndApiVersion()
{
if (_scopes == null || !_scopes.Any())
{
throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtensionScopePrefix} or {ComunicationClientsScopePrefix}.", nameof(_scopes));
}
else if (_scopes.All(item => item.StartsWith(TeamsExtensionScopePrefix)))
{
return (TeamsExtensionEndpoint, TeamsExtensionApiVersion);
}
else if (_scopes.All(item => item.StartsWith(ComunicationClientsScopePrefix)))
{
return (ComunicationClientsEndpoint, ComunicationClientsApiVersion);
}
else
{
throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtensionScopePrefix} or {ComunicationClientsScopePrefix}.", nameof(_scopes));
}
}

private AccessToken ParseAccessTokenFromResponse(Response response)
{
switch (response.Status)
Expand Down
Loading
Loading