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

  1. You register an Azure AD application for your API and expose a scope on it.
  2. Your SPFx solution requests permission to that scope; an admin approves it.
  3. At runtime SPFx's AadHttpClient gets a bearer token scoped to your API and attaches it to the request.
  4. 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));
    }
}
Validate the audience and issuer explicitly. A token signed by Azure AD is not automatically for your API — a misconfigured API that only checks "is this a valid AAD token?" will accept tokens minted for other resources. Always assert 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.

Prefer a managed identity for the Function's downstream calls (to Key Vault, Storage, SQL, Graph app-only) so there's no secret to rotate or leak. Combine with Key Vault references for any remaining secrets. The browser presents a user token; the Function presents its managed identity. Two separate trust boundaries, each least-privileged.

The complete picture

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.

DS

Dhaval Shah

Cloud Solution Architect — 18+ years across SharePoint, Microsoft 365, and Azure AI.