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 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.

7 thoughts on “Restrict calls to Azure Functions from SPFx

  1. Hi Markus,

    Thank you for sharing this wonderful article as it has really helped us a lot in building our project.
    I have one query regarding the security of the Application and we have referred many articles but could not find a solution to it. So it would be really great if you could help us.

    In SPFX , package-solution file we have mentioned the name of reg app and scope as user_impersonation to trigger Azure Function. Now the issue is, what if someone else creates different SPFX webpart and use the same combination of registered app and scope in his package-solution file. Since our reg App has Sites.FullControl.All permission he might make use of it.

    So, Can we have 2 reg App, One used in SPFX(With limited permission) and another to trigger Azure Function(with higher permission)?

    We are stuck because of this issue. It would be really grateful if you could help us.

    Regards,
    Megha Jain
    meghaj473@gmail.com

    Like

    1. Hi Megha,
      that’s a very good question but the opposite is true. It is the “more secure” way.
      When you use the MSGraphClient way and directly request permissions to Graph like Sites.Read.All every other webpart would “benefit” as well. Nevertheless those are delegated permissions so the user “in front of the screen” decides what is available but …
      In your case while using user_impersonation and maybe elevated privileges it is different.
      You are right, any other webpart would be able to “benefit” and use the app reg for SPFx as well…
      …BUT this only enables to also use the Azure Function as well NOT the app registration 2 with Sites.FullControl.All …
      So the “security concern” does not foremost relate to the permission but to your implementation of the Azure Function
      And if you follow what I am doing (restrict Azure Function call to specific AD group), “the other webpart” additionally “must hope” one of those group members uses it 😉

      Like

  2. Hi Markus,
    Thank you very much for your prompt reply :).
    Just to be sure, Can you please also help with the below query-:
    If I have 2 reg app , one having User.Read permission which we can use in SPFx and another having Sites.FullControl- Is this possible to trigger Azure function and use the permissions of 2nd reg app(the one with higher permission)

    Or, is there any other solution through which we can keep minimal permission granted at SPFx level and full permission at Azure Function level.

    Like

  3. Yes, that’s exactly the way you can do it. It’s also described here:
    https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient-enterpriseapi
    You request only “user_impersonation” from SPFx side and that is consented to a given service principal in Azure AD but you can achieve this inside central administration or via PowerShell.

    Only inside the Azure Function you handle your own app permission having sites.fullcontrol.all
    If you use app (and not delegated) permissions find a proper way to authenticate and handle the needed resources (app id, secret or certificate) with AZure Key Vault maybe, wote about that as well

    Like

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 )

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