Restrict calls to Azure Functions from SPFx

Restrict calls to Azure Functions from SPFx

In my last post I showed how to access SharePoint sites with resource specific consent (rsc) from an Azure function. In this post lets extend that scenario by calling this Azure function from a SharePoint Framework (SPFx) component and restrict the ability to call this function by allowing only to users belonging to a specific security group.

Last time we did not care for, as our sample demo could run locally to show it’s purpose, but this time the Azure function needs to be deployed to and set up in Azure. First an Azure function app needs to be created:

Content

Configure the Azure function

Create Azure function app

Nothing special here to chose. Next would be to add some configuration values which were used in local.settings.json last time: The app id, app secret and authority for calling Microsoft Graph.

Add Azure App configuration values

Next point is Authentication. This nowadays looks slightly different than in Microsoft’s SPFx documentation but everything mentioned there can be found again. First on the “Authentication” tab click “Add Identity Provider” and pick “Microsoft” then fill it out as shown here:

Add Microsoft Identity Provider to your Azure function

I am using the “more complex” multi-tenant scenario because in my very own demo environment I have a different Azure subscription and M365 tenant. If you don’t need that I will point out later which steps you can leave out.
Once that identity provider is added and with it a new app registration for the Azure function is created that one can be configured by clicking here:

Configure app registration of your Azure function

Not needed in a single-tenant scenario but I highly recommend to do it anyway is create a “speaking” app uri:

Expose app id uri

Use the format https://<Your Azure Subscription Tenant>.onmicrosoft.com/<YourAppID>

Next is a step special for our use case: To directly have the user’s security groups available as claims once the token is generated for the user’s access to the Azure function the token can be configured:

Token configuration of the app registration

On the tab “Token configuration” click “Add groups claim” and pick “Security groups”.

Last not least the “Identity provider” needs to be edited. Instead of the app registration link (2 pics above) more right click on the edit pencil icon to get here:

Edit identity provider of Azure function

For multi-tenant scenario only it is essential to empty the “Issuer URL”. The “Allowed token audiences” must include the application id uri I recommend you above to set to
https://<Your Azure Subscription Tenant>.onmicrosoft.com/<YourAppID>

Now the code from previous post can be published to that Azure function app.
And in a multi-tenant scenario it is necessary to call the function once and login with an account from the consuming tenant, that is the Microsoft 365 tenant where later the SPFx component will run. This one login will establish an enterprise application / service principal inside that Azure AD.

And one final thing may not be forgotten: CORS. You need to add your SharePoint tenant url to the CORS settings of the Azure function.

CORS settings for an Azure function to be called from SPFx

The SPFx webpart

Once that is done it is time to consume the Azure function and therefore create the little SharePoint Framework (SPFx) component. A little webpart will do it for now. The essential things take place inside the react root component which is realized here as React.FunctionComponent:

const ResourceSpecificSpo: React.FunctionComponent<IResourceSpecificSpoProps> = (props) => {
  const [aadHttpClient, setAadHttpClient] = React.useState<AadHttpClient>(null);
  const [itemTitle, setItemTitle] = React.useState<string>("");
  const [isMember, setIsMember] = React.useState<boolean>(false);

  const titleChanged = React.useCallback(
    (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
        setItemTitle(newValue || '');
    },
    [],
  );

  const createItem = () => {
    aadHttpClient
      .get(`${config.hostUrl}/api/WriteListItem?url=${props.siteUrl}&listtitle=${props.listTitle}&title=${itemTitle}`, AadHttpClient.configurations.v1)
      .then((res: HttpClientResponse): Promise<any> => {
        return res.json();
      });
  };

  React.useEffect(() => {
    const factory: AadHttpClientFactory = props.serviceScope.consume(AadHttpClientFactory.serviceKey);
    const tokenFactory: AadTokenProviderFactory = props.serviceScope.consume(AadTokenProviderFactory.serviceKey);
    tokenFactory.getTokenProvider()
    .then(async (tokenProvider) => {
      const token = await tokenProvider.getToken(config.appIdUri);
      const decoded: any = jwt_decode(token);
      if (decoded.groups && decoded.groups.length > 0) {
        if (decoded.groups.indexOf(config.secGroupId) > -1) {
          setIsMember(true);
        }
      }
    });
    factory.getClient(config.appIdUri)
    .then((client) => {
      setAadHttpClient(client);
    });
  }, []);

    return (
      <div className={ styles.resourceSpecificSpo }>
        <div className={ styles.container }>
          <div className={ styles.row }>
            <div className={ styles.column }>
              <TextField label="Item Title" value={itemTitle} maxLength={50} onChange={titleChanged} />              
            </div>
          </div>
          <div className={ styles.row }>
            <div className={ styles.column }>
              <DefaultButton onClick={createItem} disabled={!isMember} text="Create" />
            </div>
          </div>
        </div>
      </div>
    ); 
};

export default ResourceSpecificSpo;

Essential things take place in the highlighted useEffect hook. On the one hand the AadHttpClient for later consuming the Azure function is instantiated here with the typical serviceScope pattern. But similar even before we get the bearer token for that call. Having that it can be simply decoded (not validated!) with jwt_decode here. And in case the token’s groups claim contains a specific groupID (which is defined in a .json config aside) the isMember state variable is set to true.

Only if this isMember state variable is true a Button is enabled which will finally pick an entered title in a TextField and call the Azure function to create a list item with that title.

The token that is retrieved will look like this:

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "nOo3ZDrODXEK1jKWhXslHR_KXEg",
  "kid": "nOo3ZDrODXEK1jKWhXslHR_KXEg"
}.{
  "aud": "https://markusmoeller*****.onmicrosoft.com/d10869e9-b047-4c0c-a3c6-e884b2c9f447",
  "iss": "https://sts.windows.net/b15374f7-f30c-487a-99c0-ddf606521b60/",
  "iat": 1629445416,
  "nbf": 1629445416,
  "exp": 1629449316,
  "acr": "1",
  "aio": "ATQAy/8TAAAAXhVeGq5qSE3oVkvvtcBp8VSkBqxozYYJpE2WFCVR7nAUjSUZsfKCaaXftlHrMDEs",
  "amr": [
    "pwd"
  ],
  "appid": "9c9a3043-365f-4c81-bde6-0558015ef59b",
  "appidacr": "0",
  "family_name": "Möller",
  "given_name": "Markus",
  "groups": [
    "a98c5c8d-1462-48e5-bed8-b6b9d9c75031",
    "8ee837f5-e785-43c4-ac12-ac526c2f5175",
    "5b14bc14-cce5-45e8-9bdb-68c50ec5f9e1"
  ],
  "ipaddr": "46.189.28.94",
  "name": "Markus Möller",
  "oid": "8303ba3d-a943-4fc2-934d-8c3f27956545",
  "rh": "0.AR8AW-7zW92VLE2RrB75pgZoBUMwmpxfNoFMveYFWAFe9ZsfADM.",
  "scp": "user_impersonation",
  "sub": "WeG8BhJxyDi6GHNeqgfIbawv6jg6CGDM6WCw9G63DBA",
  "tid": "b15374f7-f30c-487a-99c0-ddf606521b60",
  "unique_name": "Markus@mmsharepoint.onmicrosoft.com",
  "upn": "Markus@mmsharepoint.onmicrosoft.com",
  "uti": "kYmZOiuOr0CEPAvsjMALAA",
  "ver": "1.0"
}.[Signature]

While “aud” is the known appIdUri later all security “groups” as guids can be found in the claim. Also consider the multi-tenant scenario where the “upn” has a different domain here.

Finally a screenshot of this simple webpart in action:

Create list item webpart

To get it running you also need to take care to manage Api access for SharePoint Framework components. I prefer using PowerShell for this but for demo scenarios I always offer the webApiPermissions in package-solution.json as well. So simply bundle & package the solution with gulp bundle --ship && gulp package-solution --ship and upload it to your app catalog. Afterwards in your SharePoint admin center under Advanced | Api access approve the corresponding pending request for:

"webApiPermissionRequests": [
      {
        "resource": "mmoResourceSpecificSPO",
        "scope": "user_impersonation"
}

The “resource” either needs to be the name of the app registration created from your Azure function or the corresponding app id.

For the complete code reference I may redirect you to my github repository where I put this more advanced scenario simply to another branch than the former sample. Hope this helps in case you need a more secure way to especially call 3rd party Apis such as in Azure functions with elevated privileges in a more restricted scenario.

Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 and SharePoint Online development. He loves the new SharePoint Framework as well as some backend stuff around Azure Automation or Azure Functions and also has a passion for Microsoft Graph.
He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich.
Although if partially inspired by his daily work opinions are always personal.

One thought on “Restrict calls to Azure Functions from SPFx

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s