Use FluidFramework in a Microsoft Teams app

Use FluidFramework in a Microsoft Teams app

While producing my last sample on Microsoft teams meeting app I discovered the challenge to synchronize real time data. This led to my exploration of Microsoft’s FluidFramework. While in previous parts I explained how to simply deal with it in the frontend (part 1) and how to have a reliable storage in the backend (part 2) now it’s finally time to put the pieces together and implement everything in a (my former) Teams app. So grab your popcorn 🍿 and let’s vote for favorite movies again.

Series

Content

Setup

To set up the app we need to scaffold a Teams tab application. This time it needs to include SSO. Of course the manifest needs to consider Teams meeting app settings like described in my previous sample.

Teams Meeting tab setup in yo Teams with SSO

Additionally for Microsoft FluidFramework and Azure Fluid Relay service the following npm packages need to be installed:

npm i fluid-framework @fluidframework/azure-client @fluidframework/test-client-utils

Configuration

AzureConnectionConfig

Since FluidFramework version 1 there is a distinction between a local and a remote configuration. While the local one is quite straightforward the remote one needs parameters from the Azure Fluid Relay service.

const useAzure = true; // | false
const AzureLocalConnection: AzureLocalConnectionConfig = {
  type: "local",
  tokenProvider: new InsecureTokenProvider("c51b27e2881cfc8d8101d0e1dfaea768", { id: userID }), // Problematic to have secret here in client-side code
  endpoint: process.env.REACT_APP_REDIRECTURI!,
};
const AzureRemoteConnection: AzureRemoteConnectionConfig = {
  type: "remote",
  tenantId: process.env.REACT_APP_TENANT_ID!,
  tokenProvider: new AzureFunctionTokenProvider(process.env.REACT_APP_AZURETOKENURL + "/api/FluidTokenProvider", { userId: userID, userName: "Test User" }),
  endpoint: process.env.REACT_APP_ORDERER!
};
export const connectionConfig: AzureClientProps = useAzure ? { connection: AzureRemoteConnection} : { connection: AzureLocalConnection } ;
Azure Fluid Relay – Access information

App installation

While in the previous posts the containerId was stored in the app url once one was established (and we did not really care how and where to persist it btw…) which looked liked this:

http://localhost:3000/#a31ffdc3-f230-48fb-83de-38632a30c46c

In Teams this needs to be handled differently. The Microsoft tutorial suggests to establish a container during app setup (installation) and persist the containerId into app’s contentUrl.
The disadvantage here is that in case of an app reconfiguration any existing containerId might get lost. But here is a more flexible way. As this app needs an app configuration (for the movie urls) anyway let’s put the containerId there. Furthermore I optionally allow to “reset” it. Maybe someone wants to change movies AND clear existing votes.

Teams app configuration with urls and option to reset container (of votes)
const saveConfig = async (idToken: string, saveEvent: pages.config.SaveEvent) => {
    if (reset) {
      setContainerId("");
    }
    const currentContainerId = await getFluidContainerId(context?.user?.userPrincipalName!, idToken, containerId);
    const host = "https://" + window.location.host;
    pages.config.setConfig({
      contentUrl: host + "/voteMovieFluidTab/?" + 
          containerIdQueryParamKey + "=" + currentContainerId +
          "&name={loginHint}&tenant={tid}&group={groupId}&theme={theme}",
      websiteUrl: host + "/voteMovieFluidTab/?" + 
          containerIdQueryParamKey + "=" + currentContainerId +
          "&name={loginHint}&tenant={tid}&group={groupId}&theme={theme}",
      suggestedDisplayName: "Vote Movie Fluid",
      removeUrl: host + "/voteMovieFluidTab/remove.html?theme={theme}",
      entityId: entityId.current
    }).then(() => {
      Axios.post(`https://${process.env.PUBLIC_HOSTNAME}/api/config/${meetingID.current}`,
                { config: { movie1url: movieRef1.current, movie2url: movieRef2.current, movie3url: movieRef3.current, containerId: currentContainerId }});
      saveEvent.notifySuccess();
    });
  };

In code at first any existing containerId is reset to blank if the user clicked the corresponding Toggle. Next a container is either created or a connection to a given one is established. Then the containerId is written to contentUrl and websiteUrl as the Microsoft tutorial suggests. But finally the containerId is also written to the app configuration store together with the movie urls. From here it can also be easily retrieved while next time the settings page is opened for re-configuration and in case the user does not want a reset any given containerId can be kept.

const loadConfig = async (meeting: string) => {
    Axios.get(`https://${process.env.PUBLIC_HOSTNAME}/api/config/${meeting}`).then((response) => {
        const config = response.data;
        setMovie1(config.movie1url);
        setMovie2(config.movie2url);
        setMovie3(config.movie3url);
        setContainerId(config.containerId);
    });
  };
useEffect(() => {
    if (context) {
      let meeting = "";
      meeting = context.meeting?.id!;
      meetingID.current = meeting;
      loadConfig(meeting);
      pages.config.registerOnSaveHandler(onSaveHandler);
      pages.config.setValidityState(true);
      app.notifySuccess();
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [context]);

SSO

For a secure connection to the Azure Fluid Relay service a Token service provider represented by an Azure function is needed. I already explained this Azure function in the previous part and you can also refer to it in my GitHub repository.

If this Azure function requires user authentication the teams app needs an identity token to access this Azure function. To get this identity token an application ID is needed. This application ID comes from the identity provider of the Azure function. In the previous part of this series the configuration of the Azure function identity provider was already introduced. If the identity provider application is configured as needed for Teams tab SSO additionally two steps are needed. But first in short the regular SSO steps here. This needs to be applied to the app registration of the Azure Function’s identity provider:

  • Expose an API and give the Api URI a name like api://xxxx.ngrok.io/<Your App ID> (xxxx depends on your current ngrok Url)
  • Set scope name to access_as_user and provide Admin & User messages
  • Add Teams Client 1fec8e78-bce4-4aaf-ab1b-5451cc387264 and Teams Web Client 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 IDs under “Add a client application”

Additionally the teams app URL needs to be added to the CORS settings of the Azure function. And the app ID URI needs to be added as allowed token audience to the identity provider.

Teams app url added to CORS of TokenServiceProvider Azure Function
AppIdURI as allowed token audience for Azure Function’s identity provider

Having everything configured the implementation of SSO looks like the given example from the configuration page:

const secureAccess = true; // false
…
if (secureAccess) {
      if (inTeams === true) {
        authentication.getAuthToken({
            resources: [process.env.TAB_APP_URI as string],
            silent: false
        } as authentication.AuthTokenRequestParameters).then(token => {
          saveConfig(token, saveEvent)
            
        }).catch(message => {
          app.notifyFailure({
              reason: app.FailedReason.AuthFailed,
              message
          });
        });
      }
    }
    else {
      saveConfig("", saveEvent);
    }  
  };
  const saveConfig = async (idToken: string, saveEvent: pages.config.SaveEvent) => {
    if (reset) {
      setContainerId("");
    }
    const currentContainerId = await getFluidContainerId(context?.user?.userPrincipalName!, idToken, containerId);
…
}

If the app is configured to use secure access an ID token is generated and used for establishing the fluid container connection. If not the token is handed in as blank. As known from the previous part in case the fluid connection function receives a token a custom AzureFunctionTokenProviderSec is used.

if (authToken !== "") {
    connectionConfig.connection.tokenProvider = new AzureFunctionTokenProviderSec(process.env.REACT_APP_AZURETOKENURL + "/api/FluidTokenProvider", authToken, { userId: userID, userName: "Test User" });
  }

Client implementation

Establish Fluid Relay connection

In the parent client-side JSX component the connection to the Fluid backend container represented by a SharedMap is established.

const setFluidAccess = (token: string, containerId: string) => {
    getFluidContainer(context?.user?.userPrincipalName!, token, containerId)
      .then((fluidContainer) => {
        if (fluidContainer !== undefined) {
          const sharedVotes = fluidContainer.initialObjects.sharedVotes as SharedMap;
          setFluidContainerMap(sharedVotes);
        }
      });
 };

Although FluidFramework can deal with different objects a SharedMap is ideal here. As in this case there’s a need to store the following four values:

// Initialize votes
const sharedVotes = container.initialObjects.sharedVotes as SharedMap;
sharedVotes.set("votes1", 0);
sharedVotes.set("votes2", 0);
sharedVotes.set("votes3", 0);
sharedVotes.set("votedUsers", "");

Voting

Having a connection to the SharedMap coming from the parent component the voting is quite easy. Once the Vote button is clicked the corresponding vote() action is called. This writes an increased value to the SharedMap. An event receiver synchronizes this with the state variable responsible for the UI text value. While the write operation to the SharedMap automatically takes care for correspondence/sync with other clients.

const vote = async () => {    
    let votedUsers = props.votingMap.get("votedUsers");
    votedUsers += `;${props.userID}`;
    props.votingMap.set("votedUsers", votedUsers);
  };
  useEffect(() => {
    evalVotable();
  }, []);

  React.useEffect(() => {
    const updateVotes = () => {
      setVotes1(props.votingMap.get("votes1")!);
      setVotes2(props.votingMap.get("votes2")!);
      setVotes3(props.votingMap.get("votes3")!);
      evalVotable();
    };

    props.votingMap.on("valueChanged", updateVotes);

    return () => {
      props.votingMap.off("valueChanged", updateVotes);
    };
  });
....
{votable &&
              <div>
                <Button className="voteBtn" onClick={() => { props.votingMap.set("votes1", votes1! + 1); vote(); }}>Vote Movie 1</Button>
              </div>}

Result and summary

With Microsoft FluidFramework and Azure Fluid Relay service the given Teams application still looks the same as introduced in my previous post.

SidePanel movie voting
StageView watch most voted movie

But with some configuration effort and, depending on your security requirements, a reduced amount of lines of code it is achievable having a reliable and scalable enterprise-ready real time data synchronization.

Nevertheless establishing a secure access to Azure Fluid Relay service could still be facilitated by Microsoft with more advanced serverless components. For further reference check my GitHub repositories with the current solution, the Secure Token Service provider and for comparison my former solution.

Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 Development. He loves SharePoint Framework but also has a passion for Microsoft Graph and Teams Development.
He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich.
In 2021 he received his first Microsoft MVP award in M365 Development for his continuous community contributions.
Although if partially inspired by his daily work opinions are always personal.

2 thoughts on “Use FluidFramework in a Microsoft Teams app

Leave a comment