← Back to Blog

Microsoft Graph API Access Tokens: User vs. App-Level (2026)

Microsoft Graph API access tokens: how to get a user-level (delegated) token vs. an app-level one, with working MSAL code and the actual 1-hour token lifetime.

Microsoft Graph API Access Tokens: User vs. App-Level (2026)


Want a user-level Graph API token — one scoped to whatever the signed-in person can see, not the whole tenant? That's a delegated token. Register the app, request delegated scopes like User.Read, and pull it with acquireTokenSilent (falling back to acquireTokenPopup or the Authorization Code flow) via MSAL. Skip ahead to Step 4 if that's all you came for — everything else here is the context that keeps it from breaking in production.

Microsoft Graph API is incredibly powerful. One REST endpoint, and you're into users, mail, calendar, SharePoint, Teams, OneDrive. But almost everyone hits the same wall on day one: auth. Get the permission model wrong and you're staring at an AADSTS code that means nothing until you've seen it three times.

Key Takeaways

- Delegated tokens act as the signed-in user and are capped by that user's own access; application tokens act as the app itself and need admin consent.

- Call acquireTokenSilent first, every time — it's the difference between a token that costs one cache lookup and one that costs a popup.

- A middle-tier API acting on a user's behalf (not just the SPA) needs the On-Behalf-Of (OBO) flow, not client credentials.

- In the browser, a Graph access token is good for 1 hour by default. MSAL's SPA refresh tokens are capped at 24 hours — shorter than you'd think, because browser storage isn't a safe place to keep a long-lived credential.

---

Step 1: Register Your Application in Azure AD

Every Graph API call requires a registered application in Microsoft Entra ID.

How to Register

  • Go to the Azure Portal → Microsoft Entra ID → App registrations → New registration.

  • Set a display name (e.g., "Graph API Demo App").

  • Choose a supported account type:

- Single tenant — Only your organization (most common for enterprise apps).
- Multitenant — Any Microsoft Entra ID organization.
- Personal accounts — Include Microsoft accounts (Outlook.com, Xbox).
  • Set a redirect URI (for web apps: http://localhost:3000/auth/callback; for SPAs: http://localhost:3000).

  • Click Register.

After Registration — Collect These Values

You'll need three values from the app's Overview page:

Application (client) ID:    xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Directory (tenant) ID:      xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Client Secret (or cert):    Created in "Certificates & secrets"

Save these securely. The client secret is only shown once.

---

Step 2: Delegated vs. Application Tokens — Which One Do You Need?

Graph API uses two permission models, and picking the wrong one is the single biggest source of auth failures.

Delegated Permissions (User-Level Token)

The app acts on behalf of a signed-in user. The token is scoped to the intersection of what the user can access and what the app was granted. A user-level token can never see more than the human behind it could see in the browser.

Example: A web app reading the signed-in user's inbox needs Mail.Read as a delegated permission, and the resulting token is a user-level token.

Application Permissions (Daemon/Service Token)

The app acts as itself — no user in the loop. This is what a scheduled job, a webhook processor, or a background sync service uses.

Example: A Power Automate flow reading every mailbox in the tenant needs Calendars.Read as an application permission, plus admin consent.

There's a Third Case: a Middle-Tier API Acting for a User

Say a SPA calls your API, and your API then needs to call Graph as that same user. A plain delegated token won't reach that far — the SPA's token is scoped to your API, not to Graph. This is what the On-Behalf-Of (OBO) flow exists for: your API exchanges the inbound token for a new Graph-scoped token, still user-level, still bound to that person's permissions.

Quick Reference

ScenarioPermission TypeAdmin Consent?MSAL Flow
User reads own profileDelegatedNoAuth Code
User reads team filesDelegatedYesAuth Code
Background job sends mailApplicationYesClient Credentials
SPFx web part reads listsDelegatedDependsImplicit/Auth Code
API calls Graph as the calling userDelegatedDependsOn-Behalf-Of
Power Automate connectorDelegated/AppYesVarious

---

Step 3: Configure API Permissions

  • In your app registration, go to API permissions → Add a permission → Microsoft Graph.

  • Choose Delegated or Application permissions.

  • Search for the specific scope (e.g., User.Read, Files.ReadWrite.All, Sites.Read.All).

  • Click Add permissions.

  • If the permission requires admin consent, click Grant admin consent for [tenant].

Common Permission Scopes

User.Read              — Read signed-in user's profile
User.Read.All          — Read all users' profiles (admin)
Mail.Read              — Read user's mail
Mail.Send              — Send mail as user
Files.ReadWrite.All    — Read/write all files in OneDrive/SharePoint
Sites.Read.All         — Read all SharePoint sites
Calendars.ReadWrite    — Read/write user's calendar
Team.ReadBasic.All     — Read Teams info

Need to check what a given permission actually returns before you burn a consent request on it? The Graph API Explorer on this site lets you build and preview the request first.

---

Step 4: Get a User-Level Token with MSAL.js (Browser Apps)

MSAL.js v5 (@azure/msal-browser) is the current release for single-page applications, and it's what actually issues the delegated token from Step 2. v4 is still in long-term support if you can't move yet. npm's own tags for the package don't point to v3 at all anymore — latest is 5.21.0, lts is 4.30.0 — so don't start a new project on it.

Installation

npm install @azure/msal-browser

Configuration

// authConfig.ts
import { Configuration, PublicClientApplication } from '@azure/msal-browser';

const msalConfig: Configuration = {
  auth: {
    clientId: 'YOUR_CLIENT_ID',
    authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID',
    redirectUri: 'http://localhost:3000',
  },
  cache: {
    cacheLocation: 'sessionStorage',
    storeAuthStateInCookie: false,
  },
};

export const msalInstance = new PublicClientApplication(msalConfig);

Acquiring a Token — Silent First, Interactive as the Fallback

acquireTokenSilent checks the MSAL cache and the refresh token before ever touching the network for a login prompt. Reach for acquireTokenPopup or acquireTokenRedirect only when the silent call throws — that's the pattern that keeps users from seeing a login popup on every page load.

// graphService.ts
import { msalInstance } from './authConfig';

const loginRequest = {
  scopes: ['User.Read', 'Mail.Read'],
};

export async function getToken(): Promise<string> {
  const accounts = msalInstance.getAllAccounts();
  
  if (accounts.length > 0) {
    // Silent token acquisition (from cache, or a silent refresh-token exchange)
    try {
      const response = await msalInstance.acquireTokenSilent({
        ...loginRequest,
        account: accounts[0],
      });
      return response.accessToken;
    } catch (error) {
      // Cache miss or consent required — fall through to interactive
    }
  }

  // Interactive login (popup or redirect) — only reached when silent fails
  const response = await msalInstance.acquireTokenPopup(loginRequest);
  return response.accessToken;
}

This token is a user-level token: pass it as-is to Graph, and Graph enforces the signed-in user's own access on top of whatever scopes you requested.

Making a Graph API Call

export async function getMyProfile() {
  const token = await getToken();
  
  const response = await fetch('https://graph.microsoft.com/v1.0/me', {
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error(`Graph API error: ${response.status}`);
  }

  return response.json();
}

The Pitfall That Causes InvalidAuthenticationToken

Two things break a token that looks fine on paper. First, audience mismatch: a token minted for your own API's App ID URI won't work against graph.microsoft.com. You need a token whose audience is Graph itself — that's exactly what the OBO exchange in Step 2 produces. Second, stale scope: if you add a new delegated permission in the portal but the cached token was issued before that change, acquireTokenSilent will happily hand back the old, narrower token. Call acquireTokenSilent with the new scope included and MSAL will detect the mismatch and re-consent instead of silently under-scoping the call.

---

Step 5: Authenticate in Node.js (Server-Side / Daemon)

For background services and APIs, use @azure/msal-node with the Client Credentials flow.

Installation

npm install @azure/msal-node

Client Secret Authentication

import { ConfidentialClientApplication } from '@azure/msal-node';

const msalConfig = {
  auth: {
    clientId: 'YOUR_CLIENT_ID',
    authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID',
    clientSecret: 'YOUR_CLIENT_SECRET',
  },
};

const cca = new ConfidentialClientApplication(msalConfig);

async function getAppToken(): Promise<string> {
  const result = await cca.acquireTokenByClientCredential({
    scopes: ['https://graph.microsoft.com/.default'],
  });
  
  if (!result) throw new Error('Token acquisition failed');
  return result.accessToken;
}

Certificate Authentication (Production Recommended)

For production workloads, use certificate-based auth over client secrets. A secret is a copy-pasteable string that ends up in a .env file, a CI log, or a Slack message sooner or later. A certificate's private key doesn't get pasted anywhere by accident.

import { readFileSync } from 'fs';
import { ConfidentialClientApplication } from '@azure/msal-node';

const msalConfig = {
  auth: {
    clientId: 'YOUR_CLIENT_ID',
    authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID',
    clientCertificate: {
      thumbprint: 'YOUR_CERT_THUMBPRINT',
      privateKey: readFileSync('./certs/private-key.pem', 'utf-8'),
    },
  },
};

const cca = new ConfidentialClientApplication(msalConfig);
// Usage is identical to client secret

A Middle-Tier API Acting as the User: On-Behalf-Of

Say this Node service is the API a SPA calls, not a standalone daemon. Swap client-credentials for the OBO grant so the resulting Graph token still carries the original user's identity and permissions, not the service's own:

async function getGraphTokenOnBehalfOfUser(userAssertionToken: string): Promise<string> {
  const result = await cca.acquireTokenOnBehalfOf({
    oboAssertion: userAssertionToken, // the token your API received from the SPA
    scopes: ['https://graph.microsoft.com/.default'],
  });

  if (!result) throw new Error('OBO token acquisition failed');
  return result.accessToken;
}

---

Step 6: Authenticate in SPFx Web Parts

SPFx provides a built-in AadHttpClient that handles authentication automatically — no need for MSAL directly.

import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';

export default class GraphWebPart extends BaseClientSideWebPart {
  
  public async render(): Promise<void> {
    const client: AadHttpClient = await this.context.aadHttpClientFactory
      .getClient('https://graph.microsoft.com');
    
    const response: HttpClientResponse = await client.get(
      'https://graph.microsoft.com/v1.0/me',
      AadHttpClient.configurations.v1
    );
    
    const profile = await response.json();
    // Render profile data
  }
}

Important: You still need to approve the API permissions in the SharePoint Admin Center under API access or via the spo serviceprincipal grant add CLI command.

---

Troubleshooting Common Auth Errors

Codes and meanings below match Microsoft's own Entra authentication error-code reference:

Error CodeMeaningFix
AADSTS700016App not found in tenantVerify Client ID and Tenant ID
AADSTS65001User hasn't consentedAdd consent redirect or grant admin consent
AADSTS7000215Invalid client secretRegenerate secret in Azure Portal
AADSTS50076MFA requiredUse interactive auth flow, not silent
AADSTS7000222Client secret has expiredRotate the secret, or switch to certificate-based auth
AADSTS50011Redirect URI mismatchUpdate redirect URI in app registration

Seeing InvalidAuthenticationToken instead of an AADSTS code? That's a Graph-side error, not an Entra sign-in error — see the audience/scope pitfall in Step 4 for the two most common causes.

---

Where to deploy this

If you're putting a Graph-backed web app behind authentication, Azure Static Web Apps with Microsoft Graph walks through the deployment side — managed identity, route-level auth, and the API conventions Static Web Apps expects.

FAQ

How do I get a user-level (delegated) Graph API access token?

Register an app in Entra ID, request delegated scopes like User.Read, and call MSAL's acquireTokenSilent first, falling back to acquireTokenPopup or the Authorization Code flow when there's no cached session. The result is a token scoped to that signed-in user — never broader than what they could see themselves.

What is the difference between delegated and application permissions?

Delegated permissions require a signed-in user and operate within that user's access level. Application permissions run without a user and typically have tenant-wide access. Application permissions always require admin consent.

Do I need a client secret or certificate for Graph API?

For browser apps (SPAs), you don't need either — MSAL.js uses the Authorization Code flow with PKCE. For server-side apps and daemons, you need either a client secret or certificate. Certificates are more secure and recommended for production.

How long do Graph API tokens last?

In the browser, an access token is good for 1 hour by default, and the refresh token behind it is capped at 24 hours for single-page apps (MSAL.js token lifetimes doc). That 24-hour window is non-adjustable — a browser tab just isn't a secure place to keep a credential alive longer than that. Server-side daemons using client-credentials tokens aren't bound by that SPA limit. Either way, call acquireTokenSilent first — it checks the cache before it ever asks for a new one.

Can I use Graph API without Microsoft Entra ID?

No. Microsoft Graph API exclusively uses Microsoft Entra ID for authentication. There is no API key or basic auth option. Every call requires a valid OAuth 2.0 bearer token.

How do I test Graph API calls without building an app?

Use the Graph API Explorer on this site, or Microsoft's own Graph Explorer — sign in and test any endpoint interactively before you write a line of auth code.

---

For more practical Graph API examples, check out our Microsoft Graph API: 10 Practical Examples guide. Need help with SPFx authentication specifically? See our SPFx CRUD tutorial for the full setup.

This guide covers authentication patterns current as of September 2026 (MSAL.js v5.x, Microsoft Entra ID).

Free Developer Tool

GUID Generator

Generate cryptographically secure GUIDs for SPFx manifests, Azure AD registrations, and Power Platform solutions — free and instant.

Try It Free →

We use cookies for analytics and personalised advertising. By accepting, you allow these uses. See our Privacy Policy and Cookie Policy.