OneNote Sensor
What it ingests
The notebooks, sections, and pages in Microsoft OneNote, through the Microsoft Graph OneNote API:
| OneNote object | Ontology result | Signal |
|---|---|---|
| Notebook | CreativeWork (source_type: notebook) | notebook |
| Section | CreativeWork (source_type: section), PART_OF its notebook | section |
| Page | CreativeWork (source_type: page) with its text, PART_OF its section | page |
| Notebook / section creator and last editor | Person AUTHORED the notebook or section | notebook, section |
Pages carry their text, converted from OneNote's HTML, with secrets redacted. Names are paths, such as Engineering / Meeting Notes / Standup 2026-10-01, because a section called "Meeting Notes" exists in almost every notebook. A section inside a section group includes the group in its path (Engineering / 2026 / Planning).
People are matched to existing Person entities by their Entra object id and email when the account can look them up (see User.ReadBasic.All below). A person also ingested by the Microsoft Entra ID source, or seen in Teams, Outlook or SharePoint, becomes the same Person.
Read-only and poll-based.
Why this source uses a delegated sign-in
Every other Microsoft source authenticates app-only with the shared microsoft_graph credential. OneNote can't. Microsoft stopped accepting app-only tokens for the OneNote API on March 31, 2025. An app-only token now gets 401 Unauthorized on every OneNote call.
Entra still lets an administrator consent Notes.Read.All as an application permission, and Graph's per-API permission tables still list it, but the grant doesn't work. Microsoft documents delegated access as the only supported option.
So this source signs in as one account, normally a dedicated service account, and reads what that account can open:
- its own notebooks and the notebooks shared with it (
me) - the notebooks of Microsoft 365 groups it belongs to (
group:<id>) - the notebooks on SharePoint sites it can read (
site:<id>) - notebooks another user has shared with it (
user:<id or UPN>)
To bring a team's notebook in, share it with the service account or add the account to the group that owns it.
Credential
A microsoft_graph_delegated credential: the refresh token from the service account's sign-in, plus the app registration that redeems it.
{
"tenant_id": "72f988bf-86f1-41af-91ab-2d7cd011db47",
"client_id": "a1b2c3d4-e5f6-4789-8abc-def012345678",
"refresh_token": "1.AXoA…"
}client_secret is optional. Leave it out when the refresh token came from the recommended device-code sign-in on a dedicated public-client app (below): that token belongs to a public client, and Entra refuses it if a secret is sent with it. Include it only when the token came from the authorization-code sign-in on a confidential app.
curl -sS -X POST https://your-corveil-host/api/credentials \
-H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
-d '{"type":"microsoft_graph_delegated","name":"onenote-service-account","secret":"{\"tenant_id\":\"…\",\"client_id\":\"…\",\"refresh_token\":\"…\"}"}'The refresh token rotates. Entra issues a new refresh token every time the old one is used. Corveil writes each new one back to this credential after every poll, so the stored value changes over time. If you replace the credential by hand, paste a fresh token from a new sign-in.
Config fields
| Key | Required | Notes |
|---|---|---|
sources | no | Newline- or semicolon-separated: me, group:<group id>, site:<site id>, user:<user id or UPN>. Defaults to me. Don't separate with commas: a SharePoint site id contains commas. |
notebooks | no | Comma-separated notebook names to restrict ingestion to (case-insensitive). Defaults to every notebook the sources expose. |
There is no default_entity_type. Notebooks, sections, and pages form a PART_OF hierarchy, and retyping one level would break the edges to it.
There is no cloud setting either. See GCC-High and DoD.
Provider-side setup
1. Create a service account
Create (or pick) a licensed Microsoft 365 account for Corveil to sign in as, such as corveil-notes@contoso.com. It needs a license that includes OneNote. Share the notebooks to ingest with it, or add it to the groups that own them.
Using a person's own account works, but the source then reads everything that person can open, and it stops when they leave.
2. Create a dedicated app registration
Create a new app registration for this source rather than reusing the one your other Microsoft sources use. See the Teams guide's app-registration steps for the basics.
Don't turn on public client flows on your shared Graph app
The device-code sign-in in step 4 needs Allow public client flows. That switch lets anyone who knows the app's client_id, which is not a secret, run device-code (and password) sign-ins against every delegated permission ever consented on that app. A device-code phishing link built on your shared Teams / SharePoint / Outlook registration would carry all of them. Keep the switch on an app that holds only what this source needs, and restrict who can sign in to it. If you must reuse the existing confidential app, leave the switch off and use the authorization-code sign-in instead.
On the new registration:
- Authentication → Advanced settings → set Allow public client flows to Yes. This lets the device-code sign-in in step 4 run without a redirect URI or a secret.
- API permissions → Add a permission → Microsoft Graph → Delegated permissions, and add only these:
| Permission | Why |
|---|---|
Notes.Read.All | Read every notebook the signed-in account can open, including group and shared notebooks. Notes.Read reads only the account's own. |
User.ReadBasic.All | Optional. Looks up a notebook author's email, so they resolve to the right Person. Without it, authors match by display name only. |
offline_access | Required. Without it, Entra issues no refresh token. |
These are Delegated permissions, not Application. Don't add any Application permissions or client secrets to this registration. Click Grant admin consent for <tenant> so the service account isn't prompted, and so tenant policies that block user consent don't stop the sign-in.
- Enterprise applications → this app → Properties → set Assignment required? to Yes. Then, under Users and groups, assign only the service account. Sign-ins by anyone else are refused, so a phished user can't hand a token for this app to someone else.
3. Exempt the account from interactive-only policies
A Conditional Access policy with a sign-in frequency limit, or one that requires MFA on every token refresh, ends the refresh token's life early. The source then stops with an auth error until someone signs in again. Exclude the service account from those policies, or scope them so a refresh from Corveil's egress addresses is allowed.
4. Sign in once and copy the refresh token
Run the device-code flow as the service account. Replace <tenant> and <client> with your tenant id and the app's client id:
curl -sS -X POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/devicecode \
-d client_id=<client> \
-d scope="https://graph.microsoft.com/Notes.Read.All https://graph.microsoft.com/User.ReadBasic.All offline_access"Open the verification_uri it prints, enter the user_code, and sign in as the service account. Then exchange the device_code:
curl -sS -X POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d client_id=<client> \
-d device_code=<device_code>Copy refresh_token from the response (not access_token, which expires in about an hour) into the credential above, with no client_secret.
Alternative: authorization-code sign-in with a client secret
Use this when you reuse an existing confidential app registration (one with a client secret) and so must leave Allow public client flows off. Add the delegated permissions from step 2 to that app, and add a Web redirect URI such as http://localhost:8400/ under Authentication. Then open this URL in a browser and sign in as the service account:
https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize?client_id=<client>&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8400%2F&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2FNotes.Read.All%20https%3A%2F%2Fgraph.microsoft.com%2FUser.ReadBasic.All%20offline_accessThe browser lands on http://localhost:8400/?code=…. Nothing needs to be listening; copy the code value from the address bar and exchange it within a few minutes:
curl -sS -X POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token \
-d grant_type=authorization_code \
-d client_id=<client> \
-d client_secret=<secret value> \
-d redirect_uri=http://localhost:8400/ \
-d code=<code>Store the refresh_token from the response with the same client_secret in the credential. Setting Assignment required? to Yes on this app restricts every Microsoft source that uses it to assigned accounts, so only do that if the app has no other interactive users.
GCC-High and DoD
Not supported. Microsoft hasn't deployed the OneNote API to the US Government L4 (GCC-High) or L5 (DoD) clouds. Plain GCC tenants use the commercial endpoints and work normally.
Wire-up
POST /api/sensors
{
"sensor_type": "onenote",
"name": "Team notebooks",
"config": {},
"credentials": { "graph_credential": "<microsoft_graph_delegated credential id>" }
}The service account's notebooks plus a group's, restricted to two notebooks:
{
"sensor_type": "onenote",
"name": "Engineering notebooks",
"config": {
"sources": "me\ngroup:6f1c3b2a-0d4e-4f5a-9b8c-7d6e5f4a3b2c",
"notebooks": "Engineering Notebook, Design Reviews"
},
"credentials": { "graph_credential": "<microsoft_graph_delegated credential id>" }
}Verify
Run Test connection. It redeems the refresh token and reads one notebook from each source. An expired or revoked token fails as an auth error, and a group or site the account can't open is named in the error.
Once polling, a page in the ontology has source: onenote and source_type: page, and Graph shows it PART_OF its section, which is PART_OF its notebook.
How sync works
- First poll: history arrives. OneNote has no change feed, so the source lists every notebook and section, then reads each section's pages oldest first. A large account drains over several polls. Each section keeps its place (the modification time of the last page read), and the next poll continues from there.
- After that: only changes. Each poll asks each section only for pages modified since its place. An idle section costs one request and returns nothing. An edited page is ingested again as the same entity, with its text and properties refreshed.
- Notebooks and sections are re-emitted only when their own modification time changes.
- Budgets per poll. Up to 100 sections, 500 pages per section, and 200 page bodies. Sections are read least-recently-checked first, so an account with more sections than one poll covers rotates through all of them. When the body budget runs out, the walk stops and the remaining pages wait for the next poll rather than landing without their text.
- Throttling pauses, it doesn't fail. On a 429 or 503 the source keeps what it already read and resumes on the next poll. A revoked or expired refresh token stops the source with an auth error, since retrying can't fix it.
- Cadence. The source runs on the sensor worker's poll interval.
Notes and limits
Deleted pages, sections, and notebooks are not removed. OneNote's API doesn't report deletions, so an entity already in the ontology stays.
Renamed notebooks and sections keep their first name in the graph. The current name is on the entity's name property, but the entity's name, and the paths of the sections and pages under it, stay as they were first ingested. That keeps every later edge pointing at the same entity.
Pages have no author. Graph's page record names only the app that created it, not a person, so AUTHORED edges come from notebooks and sections.
Text only. Images, ink, audio, and file attachments in a page are not ingested. A page whose HTML is larger than 4 MiB is ingested without its text.
Sections and pages the account can't open are skipped. A password-protected section, or one page the account is denied, is passed over: a denied page lands without its text, a denied section is retried on the next poll, and the rest of the source keeps ingesting. Only a revoked refresh token, a 401, or being denied the account's own notebook list stops the source.
More than 1,000 notebooks or sections in one source. The source reads the first 1,000 of each and reports that it isn't ingesting the rest. Narrow it with the notebooks filter, or split it into several sources.
Very large libraries. The OneNote API refuses a source whose document library holds more than 5,000 OneNote items (error 10008). That source is reported as not ingesting, and the other sources continue.
Not included: page version history, page previews, and section groups as entities of their own.