Set up Microsoft Graph API authentication for OneNote using delegated credentials via MSAL. This skill walks through Azure AD app registration, SDK installation, permission scope selection, token caching, and connection verification for both Python and TypeScript.
BREAKING CHANGE (March 31, 2025): App-only authentication (ClientSecretCredential) was deprecated for OneNote APIs. All integrations MUST use delegated auth — DeviceCodeCredential or InteractiveBrowserCredential. If your existing code uses ClientSecretCredential with OneNote endpoints, it will receive 403 Forbidden on every call. This skill provides the correct migration path.
onenote-integration-dev)http://localhost
| Scope | Use Case |
|---|---|
Notes.Read |
Read-only access to user's notebooks |
Notes.ReadWrite |
Read and write to user's notebooks |
Notes.ReadWrite.All |
Read/write all notebooks the user can access (including shared) |
Notes.Read.All |
Read all notebooks the user can access (including shared) |
Python:
pip install msgraph-sdk azure-identity
TypeScript/Node:
npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node
# .env file — NEVER commit this to version control
AZURE_CLIENT_ID=your-application-client-id
AZURE_TENANT_ID=your-directory-tenant-id
# Do NOT set AZURE_CLIENT_SECRET — app-only auth is deprecated for OneNote
import os
from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
TENANT_ID = os.environ["AZURE_TENANT_ID"]
# DeviceCodeCredential prompts user to visit a URL and enter a code
# This is the recommended flow for CLI tools and headless environments
credential = DeviceCodeCredential(
client_id=CLIENT_ID,
tenant_id=TENANT_ID,
)
scopes = ["Notes.ReadWrite"]
client = GraphServiceClient(credentials=credential, scopes=scopes)
# Verify connection
notebooks = await client.me.onenote.notebooks.get()
if notebooks and notebooks.value:
for nb in notebooks.value:
print(f"Notebook: {nb.display_name} (id: {nb.id})")
else:
print("No notebooks found — connection succeeded but account has no notebooks")
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from
"@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { DeviceCodeCredential } from "@azure/identity";
const credential = new DeviceCodeCredential({
clientId: process.env.AZURE_CLIENT_ID!,
tenantId: process.env.AZURE_TENANT_ID!,
userPromptCallback: (info) => {
// Display the device code login instructions to the user
console.log(info.message);
},
});
const scopes = ["Notes.ReadWrite"];
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes,
});
const client = Client.initWithMiddleware({ authProvider });
// Verify connection
const notebooks = await client.api("/me/onenote/notebooks").get();
console.log(`Found ${notebooks.value.length} notebooks`);
for (const nb of notebooks.value) {
console.log(` - ${nb.displayName} (${nb.id})`);
}
Without token caching, users must re-authenticate on every run. MSAL supports persistent token caching:
import { PublicClientApplication } from "@azure/msal-node";
import * as fs from "fs";
const CACHE_PATH = "./.msal-token-cache.json";
const pca = new PublicClientApplication({
auth: {
clientId: process.env.AZURE_CLIENT_ID!,
authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
},
});
// Load cached tokens on startup
const cache = pca.getTokenCache();
if (fs.existsSync(CACHE_PATH)) {
cache.deserialize(fs.readFileSync(CACHE_PATH, "utf-8"));
}
// After acquiring a token, persist the cache
const result = await pca.acquireTokenByDeviceCode({
scopes: ["Notes.ReadWrite"],
deviceCodeCallback: (response) => console.log(response.message),
});
fs.writeFileSync(CACHE_PATH, cache.serialize());
// Add .msal-token-cache.json to .gitignore immediately
| Configuration | Authority URL | When to use |
|---|---|---|
| Single tenant | https://login.microsoftonline.com/{tenant-id} |
Internal enterprise tools |
| Multi-tenant | https://login.microsoftonline.com/common |
SaaS apps serving multiple orgs |
| Personal accounts | https://login.microsoftonline.com/consumers |
Consumer OneNote only |
For multi-tenant apps, replace TENANT_ID in the authority URL with common or organizations.
After completing these steps you will have:
| Error | Code | Root Cause | Solution |
|---|---|---|---|
AADSTS7000218 |
403 | App configured for app-only auth | Reconfigure for delegated auth — app-only was deprecated March 2025 |
Insufficient privileges |
403 | Missing or wrong permission scope | Add Notes.ReadWrite in Azure Portal, then re-consent |
AADSTS50011 |
400 | Redirect URI mismatch | Set redirect URI to http://localhost for device code flow |
Token expired |
401 | Access token has expired (1 hour default) | Implement token refresh or use MSAL's built-in token cache |
AADSTS700016 |
400 | Application not found in tenant | Verify CLIENT_ID matches the registered app |
InteractionRequired |
— | Cached token invalid, need re-auth | Clear .msal-token-cache.json and re-authenticate |
Diagnosing 403 errors: If you get 403 after March 2025 on code that previously worked, check whether your credential uses ClientSecretCredential. This is the single most common cause — app-only auth for OneNote is permanently deprecated. Switch to DeviceCodeCredential.
Quick connectivity test (Python one-liner):
# Paste into Python REPL after setting AZURE_CLIENT_ID and AZURE_TENANT_ID
from azure.identity import DeviceCodeCredential; from msgraph import GraphServiceClient
c = GraphServiceClient(credentials=DeviceCodeCredential(client_id="YOUR_ID", tenant_id="YOUR_TENANT"), scopes=["Notes.Read"])
print(await c.me.onenote.notebooks.get())
Switching from deprecated app-only auth:
// BEFORE (broken after March 2025):
// const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
// AFTER (correct):
const credential = new DeviceCodeCredential({ clientId, tenantId });
onenote-hello-world to create your first notebook, section, and pageonenote-common-errors for a complete error decoder referenceonenote-sdk-patterns for production retry logic and rate limit handling