Claude-code-plugins-plus-skills onenote-security-basics

install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/onenote-pack/skills/onenote-security-basics" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-onenote-security-basics && rm -rf "$T"
manifest: plugins/saas-packs/onenote-pack/skills/onenote-security-basics/SKILL.md
source content

OneNote Security Basics

Overview

OneNote Graph API security changed fundamentally on March 31, 2025, when Microsoft deprecated app-only authentication for OneNote endpoints. Every integration must now use delegated authentication through MSAL, which means real users must sign in — no more background service accounts with client secrets. This skill covers the full security surface: permission scoping, token lifecycle management, MSAL cache serialization, credential storage, and multi-tenant hardening. Get any of these wrong and your integration either breaks silently (expired tokens returning 401s) or over-provisions access (Notes.ReadWrite.All when Notes.Read suffices).

Prerequisites

  • Azure AD app registration with redirect URI configured at https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps
  • Microsoft 365 license (E3/E5/Business) with OneNote enabled
  • Python:
    pip install msgraph-sdk azure-identity msal
    or Node:
    npm install @microsoft/microsoft-graph-client @azure/identity @azure/msal-node
  • Understanding of OAuth 2.0 authorization code flow and delegated permissions

Instructions

Permission Scope Matrix

Choose the minimum scope required for your use case:

ScopeRead notebooksRead pagesCreate pagesCreate notebooksAdmin consent?
Notes.Read
YesYesNoNoNo
Notes.ReadWrite
YesYesYesYesNo
Notes.ReadWrite.All
YesYesYesYesYes
Notes.Create
NoNoYesYesNo

Least-privilege recommendations:

  • Read-only dashboards:
    Notes.Read
    (user consent only)
  • Personal note creation:
    Notes.ReadWrite
    (user consent only)
  • Cross-user/organizational access:
    Notes.ReadWrite.All
    (requires tenant admin approval)
  • Write-only ingestion:
    Notes.Create
    (cannot read back what was written)

Delegated Authentication Setup (Post-2025 Mandatory)

CRITICAL: App-only authentication (ClientSecretCredential) was deprecated for OneNote endpoints on March 31, 2025. All code below uses delegated auth exclusively.

Python — Device Code Flow (headless/CLI environments):

from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
import os

CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
TENANT_ID = os.environ["AZURE_TENANT_ID"]

# Minimal scopes — only request what you need
scopes = ["Notes.ReadWrite"]

credential = DeviceCodeCredential(
    client_id=CLIENT_ID,
    tenant_id=TENANT_ID,
    # cache_persistence_options enables silent token renewal
)
client = GraphServiceClient(credentials=credential, scopes=scopes)

TypeScript — Interactive Browser Flow (web apps):

import { DeviceCodeCredential } from "@azure/identity";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider }
  from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";

const credential = new DeviceCodeCredential({
  clientId: process.env.AZURE_CLIENT_ID!,
  tenantId: process.env.AZURE_TENANT_ID!,
});

const scopes = ["Notes.ReadWrite"];
const authProvider = new TokenCredentialAuthenticationProvider(credential, { scopes });
const client = Client.initWithMiddleware({ authProvider });

Token Lifecycle Management

Access tokens expire after 1 hour. Refresh tokens last 90 days but can be revoked by admin policy. Your code must handle silent renewal:

# Python: MSAL token cache serialization for persistent sessions
import msal
import json
import os

CACHE_FILE = os.path.expanduser("~/.onenote-token-cache.json")

def get_msal_app():
    cache = msal.SerializableTokenCache()
    if os.path.exists(CACHE_FILE):
        cache.deserialize(open(CACHE_FILE).read())

    app = msal.PublicClientApplication(
        client_id=os.environ["AZURE_CLIENT_ID"],
        authority=f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}",
        token_cache=cache,
    )
    return app, cache

def acquire_token(app, cache):
    accounts = app.get_accounts()
    if accounts:
        # Silent renewal — no user interaction needed if refresh token valid
        result = app.acquire_token_silent(
            scopes=["https://graph.microsoft.com/Notes.ReadWrite"],
            account=accounts[0],
        )
        if result and "access_token" in result:
            save_cache(cache)
            return result["access_token"]

    # Fallback: device code flow requires user interaction
    flow = app.initiate_device_flow(
        scopes=["https://graph.microsoft.com/Notes.ReadWrite"]
    )
    print(flow["message"])  # "Go to https://microsoft.com/devicelogin..."
    result = app.acquire_token_by_device_flow(flow)
    save_cache(cache)
    return result.get("access_token")

def save_cache(cache):
    if cache.has_state_changed:
        with open(CACHE_FILE, "w") as f:
            f.write(cache.serialize())
        os.chmod(CACHE_FILE, 0o600)  # Owner-only read/write

Secure Credential Storage

Never store client IDs or tenant IDs in source code. Use environment variables at minimum, Azure Key Vault for production:

# Development: .env file (add to .gitignore FIRST)
echo ".env" >> .gitignore
cat > .env << 'EOF'
AZURE_CLIENT_ID=your-app-registration-client-id
AZURE_TENANT_ID=your-directory-tenant-id
EOF
chmod 600 .env
# Production: Azure Key Vault integration
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential

vault_url = "https://your-vault.vault.azure.net"
kv_client = SecretClient(vault_url=vault_url, credential=DefaultAzureCredential())

client_id = kv_client.get_secret("onenote-client-id").value
tenant_id = kv_client.get_secret("onenote-tenant-id").value

Multi-Tenant Security Considerations

For apps serving multiple organizations:

  • Register as a multi-tenant app (set
    supportedAccountTypes
    to
    AzureADMultipleOrgs
    )
  • Validate the
    tid
    (tenant ID) claim in every token — reject tokens from unexpected tenants
  • Store per-tenant token caches separately (never mix tenant tokens)
  • Handle Conditional Access policies: catch
    claims
    challenge in 401 responses and re-authenticate with the required claims

Security Checklist for Production

  • Using delegated auth (NOT app-only/ClientSecretCredential — deprecated March 2025)
  • Minimum required scopes (Notes.Read unless writes needed)
  • Token cache file has 0600 permissions (owner-only)
  • MSAL cache serialized to disk for silent renewal
  • Client ID and tenant ID sourced from environment or Key Vault
  • .env file in .gitignore
  • Token claims validated (aud, tid, exp)
  • Refresh token rotation monitored (90-day expiry alert)
  • Admin consent obtained for Notes.ReadWrite.All (if needed)
  • Conditional Access error handling implemented

Output

After applying this skill, your OneNote integration will have: least-privilege permission scoping matched to actual usage, persistent MSAL token cache with silent renewal, secure credential storage using environment variables or Key Vault, and a verified security checklist. Authentication failures will produce actionable error messages instead of silent 401 loops.

Error Handling

ErrorCauseFix
AADSTS65001: user needs to consent
Scope not yet granted by userRedirect to consent URL or use admin consent endpoint
AADSTS700016: app not found
Wrong client ID or wrong tenantVerify AZURE_CLIENT_ID matches portal registration
AADSTS50076: MFA required
Conditional Access policyUse InteractiveBrowserCredential (device code cannot handle MFA prompts)
403 Forbidden
on OneNote calls
Missing Notes.* permission or using app-only authCheck scope in token; switch to delegated auth
401 Unauthorized
after working
Access token expired, silent renewal failedCheck refresh token validity; re-serialize cache
Token cache file empty after restartCache not serialized on shutdownCall
save_cache()
in atexit handler

Examples

Verify your current token scopes:

import requests

def check_token_scopes(access_token: str) -> list[str]:
    """Decode token to inspect granted scopes (without validation)."""
    import base64, json
    payload = access_token.split(".")[1]
    payload += "=" * (4 - len(payload) % 4)  # pad base64
    claims = json.loads(base64.urlsafe_b64decode(payload))
    return claims.get("scp", "").split(" ")

# Usage
scopes = check_token_scopes(token)
if "Notes.ReadWrite" not in scopes:
    raise PermissionError(f"Token only has: {scopes}. Need Notes.ReadWrite.")

Rotate to new credentials without downtime:

# 1. Register new app in Azure portal
# 2. Update Key Vault with new credentials
az keyvault secret set --vault-name your-vault --name onenote-client-id --value NEW_CLIENT_ID
# 3. Clear MSAL cache to force re-auth with new app
rm ~/.onenote-token-cache.json
# 4. First request will trigger device code flow with new app

Resources

Next Steps

  • Apply
    onenote-prod-checklist
    for full production readiness review
  • Use
    onenote-reference-architecture
    to understand API path differences across notebook locations
  • See
    onenote-rate-limits
    for throttling and Retry-After handling