Azure / azure-sdk

This is the Azure SDK parent repository and mostly contains documentation around guidelines and policies as well as the releases for the various languages supported by the Azure SDK.
http://azure.github.io/azure-sdk
MIT License
472 stars 291 forks source link

Board Review: Azure Communication Services (SPOOL) - Authentication (Identity and Common packages) #1896

Closed tophpalmer closed 2 years ago

tophpalmer commented 3 years ago

The Basics

About this client library

Access key HMAC auth

Finally, the Common library includes a CommunicationAccessKeyCredentialPolicy that uses a custom documented HMAC scheme for signing the request and providing the signature on the Authorization header.

HMAC auth is used for the Communication SMS and Administration libraries, user auth is used for the Communication Chat and Calling libraries where all requests get sent on behalf of a single user.

Managed identities

We have recently added support for Managed Identities as well.

Communication user directory

Azure Communication Services supports a Bring-Your-Own-Identity scheme that isn't tied to identity providers such as Active Directory. The Communication Administration library and service provide a simple API to

The developer has to store a mapping between Communication user ids and their own identity models. It is up to the developer to manage the lifetime of a communication user, whether for long-lived scenarios (employee or customer directory) or for short-lived scenarios (a customer support chat, contact your delivery person).

User access token auth

The Common library further includes a CommunicationTokenCredential which is a wrapper around a Communication Service access token and provides auto-refresh mechanics. The developer can provide a callback that the credential then uses to request a new token. If a callback is provided, the credential will use the callback on-demand if the token has expired. Additionally, the developer can opt-in to refresh proactively. With proactive refresh, the credential uses a timer mechanism to request a new token before expiry. The user scenario for proactive refresh is connection stability where waiting for a token roundtrip could cause dropping calls.

Developer flow to issue user access tokens

A typical flow involves the developer to implement their own server route to issue tokens. This route is guarded by whatever auth and identity mechanism the developer already uses, for example Google accounts or their own identity implementation. Then once authenticated, the route controller uses the Communication Administration library to issue a new Communication user access token and pass it down to the client. The client uses the token to instantiate or refresh the CommunicationTokenCredential.

Docs for building a token issuer service with Azure Functions

Azure Function token service

Identifier types

The Communication Common library includes common types for identifiers that are used with the Communication libraries, for example phone numbers, calling applications or communication users. We expect the list of identifiers to grow over time, Microsoft Teams user types will be added soon.

Identifiers are a developer friendly model that hides the IC3 internal concept of a generic MRI (message resource identifier) in favor of a pre-defined set of different concrete types.

Artifacts required (per language)

Language Common Package Identity Package
.NET 1.0.0.beta-4: ApiView + ReadMe 1.0.0.beta-4: ApiView + ReadMe
JS 1.0.0.beta-5: ApiView + ReadMe 1.0.0.beta-4: ApiView + ReadMe
Java 1.0.0.beta-4: ApiView + ReadMe 1.0.0.beta-4: ApiView + ReadMe
Python N/A (Common code is part of the _shared folder of other packages) 1.0.0b4: ApiView + ReadMe
Android 1.0.0.beta-5: ApiView + ReadMe N/A (Identity is not intended to be used on clients)
iOS 1.0.0.beta-8: ApiView + ReadMe N/A (Identity is not intended to be used on clients)

Champion scenarios

A champion scenario is a use case that the consumer of the client library is commonly expected to perform. Champion scenarios are used to ensure the developer experience is exemplary for the common cases. You need to show the entire code sample (including error handling, as an example) for the champion scenarios.

Champion Scenario 1: Use CommunicationIdentityClient to create CommunicationUser and issue token

C

  string connectionString = "CONNECTION_STRING";
  CommunicationIdentityClient identityClient = new CommunicationIdentityClient(connectionString);

  Response<CommunicationUser> communicationUser = await identityClient.CreateUserAsync();
  CommunicationUserToken communicationUserToken = await identityClient.GetTokenAsync(
       communicationUser: communicationUser.Value,
       scopes: new[] { CommunicationTokenScope.Chat });

  Console.WriteLine($"Communication User Id: {communicationUser.Value.Id}");
  Console.WriteLine($"Communication User Issued Token Value: {communicationUserToken.Token}");

TypeScript

  const identityClient = new CommunicationIdentityClient("CONNECTION_STRING");

  const user = await identityClient.createUser();
  const token = await identityClient.getToken(user, ["chat"]);

  console.log("Communication user id: ", user.communicationUserId);
  console.log("Communication user token: ", token.token);

Java

   String endpoint = "https://<RESOURCE_NAME>.communication.azure.com";
   String accessKey = "SECRET";

   HttpClient httpClient = new NettyAsyncHttpClientBuilder().build();
   CommunicationIdentityClient identityClient = new CommunicationIdentityClientBuilder()
        .endpoint("ENDPOINT")
        .accessKey("ACCESS_KEY")
        .httpClient(httpClient)
        .buildClient();

  CommunicationUser user = identityClient.createUser();
  List<String> scopes = new ArrayList<>(Arrays.asList("chat"));
  CommunicationUserToken userToken = identityClient.getToken(user, scopes);

  System.out.println("Token: " + userToken.getToken());
  System.out.println("Expires On: " + userToken.getExpiresOn());

Champion Scenario 2: Create CommunicationTokenCredential to provide user tokens in other clients like ChatClient

Champion Scenario 2A: Get Token String

C

  string userToken = communicationUserToken.Token;
  string endpoint = "ENDPOINT";

  ChatClient chatClient = new ChatClient(
      new Uri(endpoint),
      new CommunicationTokenCredential(userToken));

  Console.WriteLine("Chat Client successfully instantiated");

TypeScript

  const userToken = communicationUserToken.token;
  const chatClient = new ChatClient("ENDPOINT", new AzureCommunicationTokenCredential(userToken));

Java

  NettyAsyncHttpClientBuilder httpClientBuilder = new NettyAsyncHttpClientBuilder();
  HttpClient httpClient = httpClientBuilder.build();

  CommunicationTokenCredential credential = new CommunicationTokenCredential("TOKEN");

  final ChatClientBuilder builder = new ChatClientBuilder();
  builder.endpoint(endpoint)
      .credential(credential)
      .httpClient(httpClient);
  ChatClient chatClient = builder.buildClient();

Swift

let credential = try CommunicationTokenCredential(<"user_access_token>")

let options = AzureCommunicationChatClientOptions(
    logger: ClientLoggers.default,
    dispatchQueue: self.queue
)

let chatClient = ChatClient(endpoint: endpoint, credential: credential, withOptions: options)

Champion Scenario 2B: Token from Callback With Proactive Refresh

C

  using var userCredential = new CommunicationUserCredential(
      refreshProactively: true, // Indicates if the token should be proactively refreshed in the background or only on-demand
      tokenRefresher: cancellationToken => FetchTokenForUserFromMyServer("bob@contoso.com", cancellationToken),
      asyncTokenRefresher: cancellationToken => FetchTokenForUserFromMyServerAsync("bob@contoso.com", 
      cancellationToken));
  await userCredential.GetTokenAsync();

  ChatClient chatClient = new ChatClient(
      new Uri(endpoint),
      userCredential);

TypeScript

  const chatClient = new ChatClient(
    "ENDPOINT",
    new AzureCommunicationUserCredential({
      tokenRefresher: (abortSignal) =>
        fetchTokenForUserFromMyServer("bob@contoso.com", abortSignal),
      refreshProactively: true
    })
  );

Champion Scenario 3: Communication Identifiers appearing in other Clients like Chat

C

await chatThreadClient.AddParticipantsAsync(new []
{
    new ChatThreadParticipant(user1) { DisplayName ="Bob"},
    new ChatThreadParticipant(user2) { DisplayName ="Mary"},
    new ChatThreadParticipant(new PhoneNumberIdentifier("+12223334444")) { DisplayName ="Sarah"}
});

AsyncPageable<ChatThreadParticipant> allParticipants = chatThreadClient.GetParticipantsAsync();
await foreach (ChatThreadParticipant participant in allParticipants)
{
     Console.WriteLine($"Chat participant id: {participant.User.Id}");
}

TypeScript

  chatThreadClient.addParticipants({
    members: [
      { user: user1, displayName: "Alice" },
      { user: user2, displayName: "Bob" },
      { user: user3, displayName: "Mallory" }
    ]
  });

  for await (const participant of chatThreadClient.listParticipants()) {
    console.log("Chat participant id", participant.user.communicationUserId);
  }

Java

  PagedIterable<ChatThreadMember> chatThreadMembersResponse = chatThreadClient.listMembers();

  chatThreadMembersResponse.iterableByPage().forEach(resp -> {
      resp.getItems().forEach(chatMember -> {
          System.out.printf("Member id is %s.", chatMember.getUser().getId());
      });
  });

Swift

  chatClient.listChatThreadParticipants(chatThreadId: "SomeChatThreadId") { (result, httpResponse) in
      switch result {
      case let .success(pages):
          guard let items = pages.items else {
              print("Failed to get members back for this thread id")
              return
          }
          if items.count > 0 {
              pages.forEachItem { (member) -> Bool in
                  guard let name = participant.displayName else {
                      print("Chat participanthas has no display name")
                      return false
                  }
                  print("Chat participant's display name is: \(name)")
                  return true
              }
          }
      case .failure:
          print("Failed to get members back for this thread id")
      }
  }
lilyjma commented 3 years ago

Recording[MS INTERNAL ONLY]

RezaJooyandeh commented 3 years ago

👋🏻 @KrzysztofCwalina, @bterlson, @johanste, @JonathanGiles and @tjprescott

The identity service has released their 1.0 version and we are ready to release 1.0.0 of the SDK.

Here are the updated ApiViews based on all the comments from the review sessions:

Language Common Package Identity Package
.NET 1.0.0.beta-4: ApiView + ReadMe 1.0.0.beta-4: ApiView + ReadMe
JS 1.0.0.beta-5: ApiView + ReadMe 1.0.0.beta-4: ApiView + ReadMe
Java 1.0.0.beta-4: ApiView + ReadMe 1.0.0.beta-4: ApiView + ReadMe
Python N/A (Common code is part of the _shared folder of other packages) 1.0.0b4: ApiView + ReadMe
Android 1.0.0.beta-5: ApiView + ReadMe N/A (Identity is not intended to be used on clients)
iOS 1.0.0.beta-8: ApiView + ReadMe N/A (Identity is not intended to be used on clients)
RezaJooyandeh commented 2 years ago

Already approved and release completed last year