Tag: Azure Function

Calling Microsoft Graph in SPFx the secure way

Calling Microsoft Graph in SPFx the secure way

As mentioned in many, many posts Microsoft Graph but also others 3rd party APIs become more and more essential to be called from SharePoint Framework. In this post I want to highlight the potential most secure way this should be done and I will also argue why after a step-by-step description how to set it up client and server side.

As most of the parts and especially the Magic happens server-side the post will start with that part. The consuming client part will take place at the end of this post.

Content

Setup Consuming App registration

A basic app registration needs to be created and the typical values like clientId, tenantId and clientSecret are stored. No need to mention that in production scenarios or even in shared developer scenarios they should not be shared in the application settings like done in this simple sample.

To access Microsoft Graph from inside the AzureFunction 2 more essential things are needed. These are given delegated Graph permissions and a valid “Application ID URI” used as Audience to be verified.

At first authentication needs to be done. Therefore, a bearer token should be in place at the HTTP request’s header. This must be validated. How to do that is nearly taken from this Microsoft Graph sample.

The token itself is generated client-side from SPFx (see below).

Basically, it just needs to be a bearer token. But in the sample it is validated against a lot of attributes such as clientID, a tenantID SigningKey, ValidIssuer, ValidAudience e.g. Also a check against extended token attributes such as Entra ID group membership might make sense.

public async Task<string>ValidateAuthorizationHeaderAsync(Microsoft.Azure.Functions.Worker.Http.HttpRequestData request)
{
  if (request.Headers.TryGetValues("authorization", out IEnumerable<string>? authValues))
  {
    var authHeader = AuthenticationHeaderValue.Parse(authValues.ToArray().First());
    if (authHeader != null && string.Compare(authHeader.Scheme, "bearer", true, CultureInfo.InvariantCulture) == 0 && !string.IsNullOrEmpty(authHeader.Parameter))
    {
      var validationParameters = await GetTokenValidationParametersAsync();
      ...
private async Task<TokenValidationParameters?> GetTokenValidationParametersAsync()
    {
      if (_validationParameters == null)
      {
        var tenantId = _config["tenantId"];
        var clientId = _config["clientId"];
        var domain = _config["domain"];
        var configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"https://login.microsoftonline.com/{tenantId}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
        var config = await configManager.GetConfigurationAsync();
        _validationParameters = new TokenValidationParameters
        {
          IssuerSigningKeys = config.SigningKeys,
          ValidateAudience = true,
          ValidAudience = $"api://{domain}/{clientId}",
          ValidateIssuer = true,
          ValidIssuer = config.Issuer,
          ValidateLifetime = true
        };
      }
      return _validationParameters;
    }

Next step is the authentication against Microsoft Graph. Theoretically, this can be done in two ways.

We need some values such as a clientID, a clientSecret or a tenantID. While those should be stored in a secure content storage. At least secrets should be stored in an Azure Key Vault or similar. Other IDs could be stored in an Azure App Configuration Service.

On-behalf-flow

The on-behalf-flow takes the bearer token from the last step and creates a new access token based on a client ID and secret to access Microsoft Graph on behalf of the current user.

public GraphServiceClient? GetUserGraphClient(string userAssertion)
{
  var tenantId = _config["tenantId"];
  var clientId = _config["clientId"];
  var clientSecret = _config["clientSecret"];
  var scopes = new[] { "https://graph.microsoft.com/.default" };
  var onBehalfOfCredential = new OnBehalfOfCredential(tenantId, clientId, clientSecret, userAssertion);
  return new GraphServiceClient(onBehalfOfCredential, scopes);
}

The bearer token from the last step here is also called the userAssertion. It is the User Representation compared to the app and tenant representation.

Client-credentials-flow

This is the elevated app permissions variant. Theoretically, there’s no need to authenticate before at the Function level of course. But as this is the more risky one, any focus should be taken on this.

Nevertheless, as this blog post is called “the secure way” it will concentrate on the user scope which should always be preferred.

Graph Operation

As simple user operation with overwrite the description of the given site. At first the the right GraphServiceClient needs to be established (see above). The rest is pretty straightforward.

public async Task<bool> UpdateSiteDescreption(string userAssertion, string siteUrl, string newSiteDescreption)
{
  _appGraphClient = GetUserGraphClient(userAssertion);
  Uri uri = new Uri(siteUrl);
  string domain = uri.Host;
  var path = uri.LocalPath;
  var site = await _appGraphClient.Sites[$"{domain}:{path}"].GetAsync();
  var newSite = new Site
  {
    Description = newSiteDescreption
  };
  try
  {
    await _appGraphClient.Sites[site.Id].PatchAsync(newSite);
  }
  catch (Microsoft.Graph.Models.ODataErrors.ODataError ex)
  {
    _logger.LogError(ex.Message);

First the given site is requested. Then a new SiteBuilder is created with the fields to be updated and finally it’s patched against the given site.

Although this seems a less sensitive operation it’s a good example for „security“ as even for such a harmless operation Sites.FullControl.All permission is required. And it’s much better to limit it to one app / function instead of providing it to all (including future) SPFx web parts.

User permissions vs delegated Sites.Selected scope

In my last post, I explained Sites.Selected dedicated scope. But the simpler way might be to use user permissions combined with sites access. So what’s the difference here?

It’s that simple: One additional step!

In the simpler scenario the Function can do what the user is allowed to and the app is allowed to. With the delegated scope the user can do what he is allowed to and the app together with permissions given on any dedicated site.

Azure Function considerations

Set authentication

Authentication for Azure Functions should be set up on the resource base as shown in the following picture:

Authentication for Azure Function, choose provider
Authentication for Azure Function

As shown we can use the already established application registration for this.

CORS

To support CORS (Cross-origin resource sharing) two levels need to be considered. At first a local debug scenario and second the official client-server connection. Locally this needs to be configured in the app settings while client server it will be configured in the resource settings.

{
  "Values": {
     ...
  },
  "Host": {
    "CORS": "*"
  }
}

This is how it looks like locally while on the Azure resource it looks like this

CORS setting in the Azure resource

Consuming web part

The consuming web part is a simple one providing a given url and the new description. It transports the values server-side while authenticating only user_impersonation together with AadHttpClient and inside the AzureFunction it’s decided who is able to do what/more. This is what I call “the secure way“.

So only a quick look on the web part. Of course there is a need for a ui providing inputs for URL and new description. Additionally, there must be a button to execute the function and that’s it. Behind the scenes the function looks quite like this.

The web part to update the site description
The web part

The most interesting part potentially takes place after the button is pressed. Because the authentication against the backend Azure Function needs to take place and the content needs to be transported.

export default class FunctionService {
  private aadHttpClientFactory: AadHttpClientFactory;
  private client: AadHttpClient;
  public static readonly serviceKey: ServiceKey<FunctionService> =
    ServiceKey.create<FunctionService>('react-site-secure-function-call-smpl', FunctionService);
  constructor(serviceScope: ServiceScope) {  
    serviceScope.whenFinished(async () => {
      this.aadHttpClientFactory = serviceScope.consume(AadHttpClientFactory.serviceKey);      
    });
  }
  public async setNewSiteDescreption(siteUrl: string, siteDescreption: string): Promise<any[]> {
    this.client = await this.aadHttpClientFactory.getClient('api://xxx.azurewebsites.net/0a8dfbc9-0423-495b-a1e6-1055f0ca69c2');
    const requestUrl = `http://localhost:7241/api/SiteFunction?URL=${siteUrl}&Descreption=${siteDescreption}`;
    return this.client
      .get(requestUrl, AadHttpClient.configurations.v1)   
      .then((response: HttpClientResponse) => {
        return response.json();
      });
  }
}

Based on the aadHttpClientFactory and the ServiceKey pattern in the update method a client is established using the audience (see above) and finally a get request (in this case against a local host debug) is executed.

Summary

So what are the big advantages of this way? The first thing of course is, this way does not allow any uncontrolled things from any client/web part to serve side. No Microsoft Graph access is directly given to a public enterprise application. Only an access to an Azure function is provided. And this Azure function (incl. access to it!) is controlled by the developer. An exclusive application registration for the Azure Function controls and restricts this independently from other solutions.

Diagram: Usage of MsGraphClient vs AadHttpClient
MsGraphClient vs AadHttpClient

For further reference on the whole solution, please refer to the corresponding GitHub repository.

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.
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.
FluidFramework and Azure Fluid Relay service

FluidFramework and Azure Fluid Relay service

Back on using Microsoft’s FluidFramework for using synchronized real time data in collaborative applications. Additionally to what I covered in the first post here we will consider more enterprise-ready storage options for handling our backend data.

In fact we are going to replace one single line of code from part I which establishes the client.

const client = new TinyliciousClient();

And yes it’s about replacing one single line with a bunch of code but of course we have to talk about reliability, enterprise-readiness and last not least security. The answer will be the Azure Fluid Relay service which is in public preview right now at the time of writing this post.

Series

Content

Azure Fluid Relay

While in part one our server side component tinylicious was working out of the box we now have to deploy an Azure Fluid Relay service. As for typical Azure resources there’s nothing more than selecting a subscription, resource group and a name.

Create an Azure Fluid Relay

Client access

To establish a connection at first an azure-client package has to be installed.

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

For access to the Azure Fluid Relay service a shared key is needed. This can be retrieved from the access keys section.

Insecure

The easiest but not very secure way is to use the InsecureTokenProvider. As needed this will create a Json Web Token (JWT) for access to Azure Fluid Relay service.

export const connectionConfig: AzureClientProps = { connection: {
    tenantId: "34d381d*-****-****-****-*******60f90",
    tokenProvider: new InsecureTokenProvider("c51b27e2881cfc8d8101d0e1dfaea768", { id: userID }),
    orderer: "https://alfred.westeurope.fluidrelay.azure.com",
    storage: "https://historian.westeurope.fluidrelay.azure.com",
}} ;
const client = new AzureClient(connectionConfig);

While in line 2 the Tenant Id from above’s screenshot is used in line 3 it’s the Primary Key as shared secret. A userID is used as a variable as later a real login is established for a different reason. But here also a dummy user could be used.

Azure Token serverless

The disadvantage of the previous alternative is that the shared secret is published within client-side code. The better and more enterprise-ready solution is to retrieve the Json Web Token (JWT) from an Azure Function. So the shared secret is held in the Azure Function server-side.

Except the shared secret the Microsoft tutorial “How to implement the service token provider” handles the other variables as parameters. In this implementation I put some of them to the configuration or even hardcoded such as the tenant ID and the scope. This helps for simplicity reasons but of courses does not enable a more flexible or multi-tenant scenario. The main function code looks like this:

import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import { ScopeType } from "@fluidframework/azure-client";
import { generateToken } from "@fluidframework/azure-service-utils";
import { getSecret } from "./keyVault";
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
  const tenantId = process.env.TENANTID;
  const documentId = (req.query.documentId || (req.body && req.body.documentId)) as string | undefined;
  const userId = (req.query.userId || (req.body && req.body.userId)) as string;
  const userName = (req.query.userName || (req.body && req.body.userName)) as string;
  const scopes = [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite]; // "doc:read", "doc:write", "summary:write"
  if (!tenantId) {
    context.res = {
      status: 400,
      body: "No tenantId provided in query params",
    };
    return;
  }
  const key = await getSecret("AzureFluidRelay"); // Name of KeyVault secret
  if (!key) {
    context.res = {
      status: 404,
      body: `No key found for the provided tenantId: ${tenantId}`,
    };
    return;
  }
  let user = { name: userName, id: userId };
  // Will generate the token and returned by an ITokenProvider implementation to use with the AzureClient.
  const token = generateToken(
    tenantId,
    key,
    scopes ?? [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
    documentId,
    user
  );
  context.res = {
    status: 200,
    body: token
  };
};
export default httpTrigger;

For a full solution running at the point of writing, refer to my GitHub repo for this Azure Function. Also as the referred Microsoft tutorial was not up to date and having several errors / inconsistencies at the same point of time.

Testing

As the Azure Function runs anonymously it can simply be tested:
Run your function Url with following query parameters
https://<YourFunctionUrl>.azurewebsites.net/api/FluidTokenProvider?userName=markus&userId=markus@mmsharepoint.onmicrosoft.com&documentId=a0004aeb-272a-4dc2-b28b-51830cef61d7

This might produce a response like the following:

{
  "alg": "HS256",
  "typ": "JWT"
}.{
  "documentId": "a0004aeb-272a-4dc2-b28b-51830cef61d7",
  "scopes": [
    "doc:read",
    "doc:write",
    "summary:write"
  ],
  "tenantId": "34d381d*-****-****-****-*******60f90",
  "user": {
    "name": "markus",
    "id": "markus@mmsharepoint.onmicrosoft.com"
  },
  "iat": 1654865714,
  "exp": 1654869314,
  "ver": "1.0",
  "jti": "b21cd59f-3efa-4cc0-a8fd-6cd61f5429df"
}.[Signature]

Consume it

As now the Azure function is running anonymously it simply can be consumed by our existing client-side code. This looks like the following:

export const connectionConfig: AzureClientProps = { connection: {
    tenantId: process.env.REACT_APP_TENANT_ID!,
    tokenProvider: new AzureFunctionTokenProvider(process.env.REACT_APP_AZURETOKENURL + "/api/FluidTokenProvider", { userId: userID, userName: "Test User" }),
    orderer: process.env.REACT_APP_ORDERER!,
    storage: process.env.REACT_APP_STORAGE!,
}}
const client = new AzureClient(connectionConfig);

In line 3 there is the Azure Function url from above coming from environment variables now. documentId is missing here, this will automatically be added by the corresponding AzureClient methods (getContainer(…) or createContainer() ) as the documentId is the same like the id of the container. userID (and also the optional userName could) is handled like above coming from a user login.

Azure Function and user authentication

Authentication against Azure Fluid Relay service always works with a shared secret. To implement user authentication you can use the workaround that the user authentication works against the Azure function which then will provide shared secret only to authenticated and authorized users. Two things need to be considered for that. At first the Azure Function needs to be configured to only accept calls from authenticated users. Next the client-side call needs to be executed in an authenticated manner. To configure the Azure Function switch to the authentication tab and require authentication while adding Microsoft as an identity provider.

Configure Azure Function authentication
Add Microsoft Azure AD as authentication provider

Next the created corresponding app registration needs to be configured. From the authentication tab click the app registration under Microsoft identity provider:

Microsoft identity provider of Azure Function authentication

What’s needed is a single page application with a corresponding redirect URI. In the given example only with a local test URL:

Single page application platform configuration

Now we can login to our application and use the received identity token to authenticate against our AzureTokenProvider function. Microsoft’s sample AzureTokenProvider does not implement authentication but we can take the open source code and further adjust it. The sample code can be found in Microsoft’s tutorial which simply can be enhanced by additionally accepting an identity token and use that to authenticate against the Azure Function:

export class AzureFunctionTokenProviderSec implements ITokenProvider {
    constructor(
        private readonly azFunctionUrl: string,
        private readonly authToken: string,
        private readonly user?: Pick,        
    ) { }
    public async fetchOrdererToken(tenantId: string, documentId?: string): Promise {
        return {
            jwt: await this.getToken(tenantId, documentId),
        };
    }
    public async fetchStorageToken(tenantId: string, documentId: string): Promise {
        return {
            jwt: await this.getToken(tenantId, documentId),
        };
    }
    private async getToken(tenantId: string, documentId: string | undefined): Promise {
        const response = await axios.get(this.azFunctionUrl, {
            headers: {
                Authorization: `Bearer ${this.authToken}`
            },
            params: {
                tenantId,
                documentId,
                userId: this.user?.userId,
                userName: this.user?.userName,
                additionalDetails: this.user?.additionalDetails,
            },
        });
        return response.data as string;
    }
}

So the difference can be found in line 4 and line 19-21. The constructor accepts an identity token and the getToken function uses it for authentication. Another difference now can be found in the utils class. If the getFluidContainer function receives an identity token It will simply exchange the AzureFunctionTokenProvider.

export async function getFluidContainer(userId: string, authToken?: string): Promise {
  userID = userId;
  if (authToken !== undefined) {
    connectionConfig.connection.tokenProvider = new AzureFunctionTokenProviderSec(process.env.REACT_APP_AZURETOKENURL + "/api/FluidTokenProvider", authToken, { userId: userID, userName: "Test User" });
  }  
  const client = new AzureClient(connectionConfig);
  let containerId: string = window.location.hash.substring(1);
  if (!containerId) {
    containerId = await createContainer(client);
    window.location.hash = containerId;
  }
  const container = await getContainer(client, containerId);
  return container;
};

So how do we get this identity token? In our app there is a specific login component. For this login component I am using MSAL.js 2.0 (npm install @azure/msal-browser) which I already introduced here. Once the user logs in a callback function is called:

const loginUser = async () => {
    const loginResponse = await msalInstance.loginPopup({ scopes:[] });
    props.login(loginResponse.account?.name!, loginResponse.account?.username!, loginResponse.idToken);
};

The callback function now retrieves the userID, userName and the idToken. Having that the fluidContainer can be instantiated:

const login = async (name: string, userName: string, idToken: string) => {
    setUserDisplayName(name);    
    setUserLoggedin(true);
    setUserID(userName);
    const container: IFluidContainer = await getFluidContainer(userName, idToken);
    setFluidContainer(container);
};

Here only a valid user authentication was considered. Of course you can furthermore use Azure AD security group based authorization which I showed client-side here or server-side here.

Azure Fluid Relay management

Of course many containers might get created over time And there might be a need for a proper lifecycle management. Here are some methods with Microsoft Azure CLI for that. The first one is for listing all available containers:

az rest --method get --uri https://management.azure.com/subscriptions/<subscriptionId>/resourcegroups/<resourceGroupName>/providers/Microsoft.FluidRelay/FluidRelayServers/<frsResourceName>/FluidRelayContainers?api-version=<apiVersion>

This might result in an output like this:

[
...
{
      "id": "/subscriptions/*9ba8c0*-****-****-****-**c081cd91**/resourceGroups/DefaultMMsharepoint/providers/Microsoft.FluidRelay/fluidRelayServers/mmdemorelay/fluidRelayContainers/72bb13b9-55e6-4007-b11c-0607c3f94f38",
      "name": "72bb13b9-55e6-4007-b11c-0607c3f94f38",
      "properties": {
        "frsContainerId": "72bb13b9-55e6-4007-b11c-0607c3f94f38",
        "frsTenantId": "34d381d*-****-****-****-*******60f90"
      },
      "resourceGroup": "DefaultMMsharepoint",
      "type": "Microsoft.FluidRelay/fluidRelayServers/fluidRelayContainers"
    }
  ]

That one now can be deleted quite similar to the get command:

az rest --method delete --uri https://management.azure.com/subscriptions/9ba8c0*-****-****-****-**c081cd91**/resourcegroups/DefaultMMsharepoint/providers/Microsoft.FluidRelay/FluidRelayServers/mmDemoRelay/FluidRelayContainers/72bb13b9-55e6-4007-b11c-0607c3f94f3?api-version=2021-08-30-preview

For further reference refer to the Microsoft documentation.

Summary and outlook

The application now looks only slightly different than in the first part. The main difference is, that it only establishes a container after a successful user login. This also works with different users of course. But the rest, especially the real-time data synchronization finally stays the same:

The app before a user login
The app after a user login
The app in action – with different users but still syncing data

While in the first part the client site usage of Microsoft FluidFramework was shown, in this part a more professional storage option with Microsoft Azure Fluid Relay service was described in detail. Especially the necessity for secure authentication was explained with several facets. In my opinion this could be further improved out-of-the-box by Microsoft Azure with serverless components. Currently too much coding still necessary. But remember that we are talking about public preview technology at the time of writing this post.

Next we are going to try to use both described parts, client-side and server-side storage option inside a Microsoft Teams application. Finally for your reference you can meanwhile find both of my code repositories in my GitHub:

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.
Restrict calls from SPFx in(side) Azure Functions

Restrict calls from SPFx in(side) Azure Functions

Some time ago I wrote a post how to restrict calls to Azure Functions inside SPFx. I therefore analyzed the token if it contains a specific group the user is member of. As this happened client side already it was directly able to disable a button for instance. On the opposite the well known webApiPermissionRequests to allow the app itself to call a 3rd party Api, that is the Azure Function, are regularly granted tenant wide. So a restriction inside “your” webpart is no security guarantee that no one else uses the Azure Function. To prevent this the same way, the token validation needs to take place backend (,too,) inside the Azure Function. This post shows how to achieve that.

The Azure Function is created the same way as in the previous post. Therefore here you find only the basics and important points.

  • Create an Azure Function
  • Create Environment variables like in local – Sample.settings.json
  • Enable CORS by adding your SharePoint tenant url
  • Establish Microsoft Authentication
  • Create a “speaking” AppIDUri for the app of Authentication Provider https://<Your Azure Subscription Tenant>.onmicrosoft.com/<YourAppID>

Finally edit the Identity Provider by removing the Issuer Url (in case of multi-tenant) and add your AppIDUri to the “Allowed token audiences”


Edit identity provider of Azure function

And the most important thing is to edit the app of your identity provider so the user token on authentication includes group memberships:

Configure app registration of your Azure function
Token configuration of the app registration – Add Groups claim

The code

First dependency injection is added to the Azure Function by adding several packages (Microsoft.Azure.Functions.Extensions, Microsoft.Azure.Functions.Extensions, Microsoft.Azure.Functions.Extensions) and adding a Startup.cs.

[assembly: FunctionsStartup(typeof(ResourceSpecificSPO.Startup))]
namespace ResourceSpecificSPO
{
  public class Startup : FunctionsStartup
  {
    public override void Configure(IFunctionsHostBuilder builder)
    {
      var config = builder.GetContext().Configuration;
      var appConfig = new CustomSettings();
      config.Bind(appConfig);
      builder.Services.AddSingleton(appConfig);
      builder.Services.AddScoped<controller.TokenValidation>();
    }
  }
}

Two things happen here. First the app configuration is bound to a Singleton and made available that way. Next a custom TokenValidation is added as a service.

Then comes the Azure Function class, first the intro:

 public class WriteListItem
  {
    private readonly controller.TokenValidation tokenValidator;
    private CustomSettings appConfig;
    public WriteListItem(CustomSettings appCnfg,
            controller.TokenValidation tknValidator)
    {
      appConfig = appCnfg;
      tokenValidator = tknValidator;
    }
    [FunctionName("WriteListItem")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestMessage req, ILogger log)
    {

Here it is no static class anymore as per default but now it also has a constructor receiving the app configuration and our service tokenValidator. Inside the function let’s only look at the token validation:

try
      {
        string authHeader = req.Headers.Authorization.Parameter;
        
        ClaimsPrincipal cp = await tokenValidator.ValidateTokenAsync(authHeader);
        if (cp == null)
        {
          // return new ForbidResult();
          log.LogError("Execution is forbidden as user is not member of group: " + appConfig.SecurityGroupID);
        }
        JWTSecurityToken jt = await tokenValidator.AnalyzeTokenAsync(authHeader);
        foreach(Claim c in jt.Claims)
        {
          log.LogInformation(c.Type + " : " + c.Value);
        }
        var roleClaims = y.Claims.Where(c => c.Type.ToLower() == "groups");
        if (!roleClaims.Any(c => c.Value.ToLower() == appConfig.SecurityGroupID.ToLower()))
        {
          return new ForbidResult();
        }
      }
      catch (Exception ex)
      {
        log.LogError(ex.Message);
      }

The code shows two alternatives in parallel. First a ClaimsPrincipal is gathered from the token with a “real” token validation (to be shown below in this post, but not only verifying specific group membership). If something goes wrong no ClaimsPrincipal but null is returned and error could be logged followed by a 403 potentially as result.

Afterwards as an alternative the token is simply analyzed while it’s returned as a simple JWTSecurityToken object. Its properties as Claims are iterated looking for a specific groupID. If this is not found a 403 can be returned for instance.

Last not least the TokenValidator class needs to be investigated:

public class TokenValidation
  {
    private CustomSettings appConfig;
    private const string scopeType = @"http://schemas.microsoft.com/identity/claims/scope";
    private ConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
    private ClaimsPrincipal _claimsPrincipal;
    private string _wellKnownEndpoint = string.Empty;
    private string _tenantId = string.Empty;
    private string _audience = string.Empty;
    private string _instance = string.Empty;
    private string _requiredScope = "user_impersonation";
    public TokenValidation(CustomSettings appCnfg)
    {
      appConfig = appCnfg;
      _tenantId = appConfig.TenantId;
      _audience = appConfig.Audience;
      _instance = appConfig.Instance;
      // _wellKnownEndpoint = $"{_instance}{_tenantId}/v2.0/.well-known/openid-configuration";
      _wellKnownEndpoint = $"{_instance}common/.well-known/openid-configuration";      
    }
    public async Task<ClaimsPrincipal> ValidateTokenAsync(string authorizationHeader)
    {
      if (string.IsNullOrEmpty(authorizationHeader))
      {
        return null;
      }
      var oidcWellknownEndpoints = await GetOIDCWellknownConfiguration();
      var tokenValidator = new JwtSecurityTokenHandler();
      var validationParameters = new TokenValidationParameters
      {
        RequireSignedTokens = false,
        ValidAudience = _audience,
        ValidateAudience = true,
        ValidateIssuer = true,
        ValidateIssuerSigningKey = false,
        ValidateLifetime = true,
        IssuerSigningKeys = oidcWellknownEndpoints.SigningKeys,
        ValidIssuer = oidcWellknownEndpoints.Issuer.Replace("{tenantid}", _tenantId)
      };
      try
      {
        SecurityToken securityToken;
        _claimsPrincipal = tokenValidator.ValidateToken(authorizationHeader, validationParameters, out securityToken);
        if (IsScopeValid(_requiredScope))
        {
          if (isGroupMember(appConfig.SecurityGroupID))
          {
            return _claimsPrincipal;
          }
        }
        return null;
      }
      catch (Exception ex)
      {
        throw ex;
      }
    }
...
  }

First let’s have a look at the start and next let’s investigate the other methods one by one. In the constructor some app config values are retrieved and an endpoint url is built. The necessity is explained a bit later when it’s used in another method.

In the ValidateTokenAsync some ValidationParameters are built and the token is validated based on that. After that some custom checks are done validating a valid scope (“user_impersonation”) and if the required group membership exists.

First helper method is for the token validation GetOIDCWellknownConfiguration. Any OIDC authority should offer a well known oidc configuration. This is requested here from the constructed wellKnownEndpoint. And the result is partially used in our ValidationParameters.

private async Task<OpenIdConnectConfiguration> GetOIDCWellknownConfiguration()
    {
        _configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
           _wellKnownEndpoint, new OpenIdConnectConfigurationRetriever());
        return await _configurationManager.GetConfigurationAsync();
    }

That constructed url used here is https://login.microsoftonline.com/common/.well-known/openid-configuration which means it is multi-tenant and for version 1. Alternatively you could use https://login.microsoftonline.com/<tenantID>/v2.0/.well-known/openid-configuration or a mix to receive tenant-specific values and for v2.0. But be aware what kind of tokens you receive. I detected v1.0 tokens used by SPFx on my Azure Function as I had an issuer like https://sts.windows.net/{tenantid}/

To verify it, there is also the necessity to replace the {tenantid} by the following line inside the ValidationParameters: ValidIssuer = oidcWellknownEndpoints.Issuer.Replace("{tenantid}", _tenantId)
I encourage you to simply try this Urls in browser and have a look what’s returned.

Maybe I should also add further packages necessary for the specific token validation and further implementation:

<PackageReference Include="Microsoft.Identity.Client" Version="4.35.1" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.12.0" />

I found out that for System.IdentityModel.Tokens.Jwt any newer version than 6.12.0 was not able to execute _configurationManager.GetConfigurationAsync(). This is why I had to use that version. Any hint to solve this would be more than welcome.
[Error: IDX20803: Unable to obtain configuration from: '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.] ]

After the token is validated the validScope can be checked by:

private bool IsScopeValid(string scopeName)
    {
      if (_claimsPrincipal == null)
      {
        return false;
      }
      var scopeClaim = _claimsPrincipal.HasClaim(x => x.Type == scopeType)
          ? _claimsPrincipal.Claims.First(x => x.Type == scopeType).Value
          : string.Empty;
      if (string.IsNullOrEmpty(scopeClaim))
      {
        return false;
      }
      if (!scopeClaim.Equals(scopeName, StringComparison.OrdinalIgnoreCase))
      {
        return false;
      }
      return true;
    }

First the general existence of a scope claim is verified followed by containing the right scope given as parameter. Next and final comes the check for the group membership:

private bool isGroupMember(string groupID)
    {
      var roleClaims = _claimsPrincipal.Claims.Where(c => c.Type.ToLower() == "groups");
      if (roleClaims.Any(c => c.Value.ToLower() == groupID.ToLower()))
      {
        return true;
      }
      return false;
    }

For better clarity I omit short notation and first extract all Claims of type “groups” and next and finally verify if one has the required groupID as value.

In parallel there was also the rudimentary token analysis method in this class which is finally shown:

public async Task<JwtSecurityToken> AnalyzeTokenAsync(string authorizationHeader)
    {
      if (string.IsNullOrEmpty(authorizationHeader))
      {
        return null;
      }
      var tokenValidator = new JwtSecurityTokenHandler();
      var token = tokenValidator.ReadJwtToken(authorizationHeader);
      return token;
    }

This method simply takes the token string from the header and builds a JWTSecurityToken out of it. No further validation takes place here. On return then the Claims are checked for the given GroupID but that happens in the Azure Function itself shown above, already.

I hope this little explanation helps to put more security in your Azure Functions. I only tried to concentrate on those things I feel most important but for your whole reference refer to the full solution available in my GitHub repository.

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.
Testing an Azure Function using delegated access with Postman

Testing an Azure Function using delegated access with Postman

When calling an Azure Function from SharePoint Framework you might want to use delegated access to let the Azure Function execute Api calls on behalf of the calling user. This can be easily done by using the AadHttpClient client-side and then running the on-behalf-flow inside the Azure Function based on the authenticating user token.
The calling code in SPFx might look like this:

const createGroup = async () => {
    const factory: AadHttpClientFactory = props.serviceScope.consume(AadHttpClientFactory.serviceKey);
    const client = await factory.getClient("https://<Tenant>.onmicrosoft.com/<AppID>");       
    const requestUrl = `http://localhost:7071/api/CreateGroup?groupName=${firstTextFieldValue}`;
    const result: any = await (await client.get(requestUrl, AadHttpClient.configurations.v1)).json();
    console.log(result);
    alert("Creation done!");
  };

While the called Azure Function basically looks like this:

[FunctionName("CreateGroup")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestMessage req,
        ILogger log)
    {
      NameValueCollection query = req.RequestUri.ParseQueryString();
      string groupName = query["groupName"];
      KeyVault keyVault = new KeyVault(new Uri(appConfig.KeyVaultUrl), log);
      string secret = await keyVault.retrieveSecret(appConfig.KeyVaultSecretName);
      SecureString clientSecret = new SecureString();
      foreach (char c in secret) clientSecret.AppendChar(c);
      string userToken = req.Headers.Authorization.Parameter;

      OnBehalfOfAuthenticationProvider onBehalfAuthProvider = new OnBehalfOfAuthenticationProvider(this.appConfig.ClientID, this.appConfig.TenantID, clientSecret, () => userToken);

      string accessToken = await onBehalfAuthProvider.GetAccessTokenAsync(new Uri("https://graph.microsoft.com"));
      GraphController graphController = new GraphController(accessToken);

      UnifiedGroup response = await graphController.CreateGroup(groupName);
      string siteUrl = await graphController.GetSiteUrl(response.id);
      response.siteUrl = siteUrl;
      return new OkObjectResult(response);
    }

You see in the highlighted lines that the Azure Function relies on the incoming authenticated request and its user access token coming from the SPFx AadHttpClient before it can make any Api call such as here a create group request inside a “GraphController” class.

And do not get confused as the Azure function in code allows Anonymous access. Of course the function itself requires authentication as configured in Azure portal.

But what if you simply want to test the Azure Function without your SharePoint Framework web part. Then you need two things

  • Another client able to “POST” (or GET eventually) the request
  • A user access token generated for your app registration

Both can be easily achieved with Postman. Assume you have your app registration with

  • An AppID
  • An AppSecret
  • A web app with callback url
  • A configured AppID Uri such as https://{{Tenant}}.onmicrosoft.com/{{AppID}}

Then you can open Postman and create a new request. Under Authorization configure OAuth2.0 the following way:

With that configuration replacing the placeholders with your values click on “Get Access Token” and login with your user (on that behalf the request will later go on). Having that successfully you can use that token and call your Azure Function:

Pay attention to select an actual token under “Available Tokens” and then use the debug url of your Azure Function with either POST or GET (how you configured your Azure Function).

Inside your Azure Function you can grab the just generated user access token from the Authorization Header and then use it for the next step(s) with the on-behalf flow. In my sample I use the OnBehalfAuthenticationProvider from the PnP Core Sdk to generate an access token for Microsoft Graph. Having that token I can execute my Api calls, either with the SDK or a plain HttpRequest. That’s it. And quite the same way an authentication against a SharePoint PnPContext would work.

So when would you need this? I’ve seen at least three scenarios where this will help:

  • A backend developer, only responsible for the Azure Function wants to test on its own (because SPFx web part is not ready or not available)
  • Testing Azure Function locally while SPFx web part is only available with server url configuration
  • Issues with Microsoft’s service principal which is responsible for the Api access of AadHttpClient. Not every organization has this enabled. So you might either be waiting to get this enabled or even need to consider to use your own client scenario.

Whatever you need this for I hope it helps you.

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.