Eventually a web part needs to do something the browser shouldn't: call a database, use an app-only permission, hold a secret, or run server-side business logic. The answer is a custom API — an Azure Function — secured by Azure AD, called from SPFx with a token the framework mints for you. Done right, no secret ever touches the browser and the API verifies every caller. Here's the whole chain, end to end.
The trust chain
- You register an Azure AD application for your API and expose a scope on it.
- Your SPFx solution requests permission to that scope; an admin approves it.
- At runtime SPFx's
AadHttpClientgets a bearer token scoped to your API and attaches it to the request. - Your Azure Function validates the token — signature, audience, issuer, scope — before doing any work.
Step 1 — Register the API app & expose a scope
In Azure AD, create an app registration for the API. Under Expose an API, set the Application ID URI (e.g. api://<client-id>) and add a delegated scope such as access_as_user:
# Azure CLI sketch
az ad app create --display-name "Contoso Orders API"
# Set the Application ID URI
az ad app update --id <appId> --identifier-uris "api://<appId>"
# (Add the 'access_as_user' OAuth2 permission scope in the portal or manifest)
Step 2 — Request the scope from SPFx
In config/package-solution.json, declare the permission so an admin can approve it in API access:
"webApiPermissionRequests": [
{
"resource": "Contoso Orders API",
"scope": "access_as_user"
}
]
resource must match the API app registration's display name (or its Application ID URI). After deploying the .sppkg, approve the request in the SharePoint admin center → Advanced → API access, or with Approve-PnPTenantServicePrincipalPermissionRequest.
Step 3 — Call the API with AadHttpClient
From the web part, ask the factory for a client scoped to your API's URI. SPFx performs the OAuth on-behalf flow and attaches a valid bearer token automatically:
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
private async getOrders(): Promise<any[]> {
const client: AadHttpClient =
await this.context.aadHttpClientFactory.getClient('api://<appId>');
const res: HttpClientResponse = await client.get(
'https://contoso-orders.azurewebsites.net/api/orders',
AadHttpClient.configurations.v1);
if (!res.ok) { throw new Error(`API returned ${res.status}`); }
return res.json();
}
Step 4 — Validate the token in the Function
This is the step people skip — and it's the one that matters. The Function must independently verify the JWT: signature against Azure AD's published keys, the aud (audience) equals your API's app ID/URI, the iss (issuer) is your tenant, the token hasn't expired, and the required scope (scp) is present.
The cleanest approach is to let the platform do the heavy lifting — configure App Service Authentication ("Easy Auth") on the Function with the Microsoft identity provider, which validates the token before your code runs. When you need in-code validation, use the Microsoft Identity Web library:
// Program.cs / Startup — validate AAD tokens
services.AddMicrosoftIdentityWebApiAuthentication(configuration);
// Function / controller — require the scope
[Authorize]
public class OrdersController : ControllerBase
{
[HttpGet("api/orders")]
[RequiredScope("access_as_user")]
public IActionResult Get()
{
// User identity is available via HttpContext.User
var upn = User.FindFirst("preferred_username")?.Value;
return Ok(OrderService.ForUser(upn));
}
}
aud = your app and iss = your tenant, and require your specific scp.
CORS
The Function is called from the browser (the SharePoint origin), so configure CORS to allow your tenant's domain — e.g. https://contoso.sharepoint.com. Don't use * in production; scope it to the origins that should call the API.
App-only operations behind the user call
Recall that SPFx tokens are always delegated. When your API needs to act with application permissions — reading data the signed-in user can't see, or running as the service — the Function holds those credentials (a certificate-based AAD app, secret in Key Vault, or a managed identity) and performs the privileged call server-side after validating the user's token. The browser never sees those credentials. This "API as a confidential broker" pattern is exactly why you stand up a custom API in the first place.
The complete picture
- SPFx →
AadHttpClient→ bearer token for your API. - Function validates the token (Easy Auth or Identity Web): signature, audience, issuer, scope.
- Function does privileged work using a managed identity, not a browser secret.
- CORS scoped to your SharePoint origin; permissions least-privileged and admin-approved.
This pattern is the backbone of serious SPFx solutions — it's how you safely bridge the browser to databases, line-of-business systems, app-only Graph, and AI services. Every secret stays server-side, every request is authenticated, and the user's identity flows all the way through. Master this chain and there's very little you can't build on top of SharePoint.