Skip to content

Commit

Permalink
[ACS][Common] OPS - Added Routing Logic Based on Scopes (#47609)
Browse files Browse the repository at this point in the history
* added routing logic

* fixed comments

* reverted back optional scope param

* fixed tests
  • Loading branch information
AikoBB committed Jan 28, 2025
1 parent 6998b06 commit c5990f2
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 45 deletions.
1 change: 1 addition & 0 deletions sdk/communication/Azure.Communication.Common/CHANGELOG.md
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

0 comments on commit c5990f2

Please sign in to comment.