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
- App registration
- Graph Operation
- User permissions vs delegated Sites.Selected scope
- Azure Function considerations
- Consuming web part
- Summary
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:

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

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

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
























