145 lines
6.5 KiB
Markdown
145 lines
6.5 KiB
Markdown
|
|
# Setup: Entra app registration for Microsoft To Do (delegated, browser auth-code)
|
||
|
|
|
||
|
|
Microsoft To Do has **no app-only access** — its Graph endpoints (`/me/todo/...`)
|
||
|
|
only accept a signed-in user's token. So this skill authenticates as **you**
|
||
|
|
(delegated) using the OAuth2 **authorization-code + PKCE** flow: `login` opens
|
||
|
|
your system browser, you sign in, and the browser redirects back to a temporary
|
||
|
|
local server. You create a *public client* app registration once, grant it the
|
||
|
|
delegated To Do permission, and sign in once per machine. There is no client
|
||
|
|
secret.
|
||
|
|
|
||
|
|
> Why browser auth-code and not device-code? Many tenants block the device-code
|
||
|
|
> flow with a Conditional Access policy (error **AADSTS53003**). The browser flow
|
||
|
|
> runs in your real browser session, satisfying far more policies. If a policy
|
||
|
|
> requires a *compliant/managed device*, the Mac must be registered/enrolled
|
||
|
|
> (Company Portal/Intune) first — no flow can bypass that.
|
||
|
|
|
||
|
|
You do **not** need to be an admin if your tenant allows users to consent to the
|
||
|
|
`Tasks.ReadWrite` delegated permission (it is a low-risk, user-consentable
|
||
|
|
permission by default). If admin consent is required in your org, ask an admin to
|
||
|
|
grant it once.
|
||
|
|
|
||
|
|
## 1. Register the application
|
||
|
|
|
||
|
|
1. Go to <https://entra.microsoft.com> → **Identity** → **Applications** →
|
||
|
|
**App registrations** → **New registration**.
|
||
|
|
2. Name it something recognizable, e.g. `claude-ms-todo`.
|
||
|
|
3. Supported account types:
|
||
|
|
- work/school only → **Accounts in this organizational directory only**
|
||
|
|
- also personal Microsoft accounts → **... and personal Microsoft accounts**
|
||
|
|
4. Leave **Redirect URI** blank for now — you'll add it in step 2.
|
||
|
|
5. **Register**.
|
||
|
|
|
||
|
|
On the app's **Overview** page, copy:
|
||
|
|
- **Application (client) ID** → `TODO_CLIENT_ID`
|
||
|
|
- **Directory (tenant) ID** → `TODO_TENANT_ID` (optional; `common` also works)
|
||
|
|
|
||
|
|
## 2. Add a loopback redirect URI (public client)
|
||
|
|
|
||
|
|
1. App → **Authentication** → **Add a platform** → **Mobile and desktop
|
||
|
|
applications**.
|
||
|
|
2. Under **Custom redirect URIs**, add **both**:
|
||
|
|
- `http://localhost`
|
||
|
|
- `http://127.0.0.1`
|
||
|
|
(Adding both avoids IPv4/IPv6 surprises. The skill redirects to
|
||
|
|
`http://127.0.0.1:<port>` at runtime; Entra ignores the port for loopback
|
||
|
|
redirects, so the OS-assigned port needs no registration.)
|
||
|
|
3. **Configure** / **Save**.
|
||
|
|
|
||
|
|
This marks the app as a public client for that redirect — no client secret is
|
||
|
|
needed; PKCE protects the code exchange. (You do **not** need "Allow public
|
||
|
|
client flows" for auth-code+PKCE.)
|
||
|
|
|
||
|
|
## 3. Add the delegated Graph permission
|
||
|
|
|
||
|
|
App → **API permissions** → **Add a permission** → **Microsoft Graph** →
|
||
|
|
**Delegated permissions**. Add:
|
||
|
|
|
||
|
|
| Permission | Why |
|
||
|
|
|---|---|
|
||
|
|
| **`Tasks.ReadWrite`** | Read/write the signed-in user's To Do tasks & lists |
|
||
|
|
| `offline_access` | Issue a refresh token so sign-in persists (usually added automatically) |
|
||
|
|
| `User.Read` | Identify the signed-in user in `test` (usually present by default) |
|
||
|
|
|
||
|
|
If your tenant requires it, click **Grant admin consent for <tenant>**.
|
||
|
|
Otherwise consent happens interactively during the first `login`.
|
||
|
|
|
||
|
|
> Use `Tasks.Read` instead of `Tasks.ReadWrite` if you want read-only access.
|
||
|
|
> The skill's write commands will then return `403`.
|
||
|
|
|
||
|
|
## 4. Give the skill the client id
|
||
|
|
|
||
|
|
Copy `.env.example` to `.env` in the skill directory and set `TODO_CLIENT_ID`
|
||
|
|
(and `TODO_TENANT_ID` if not `common`). The CLI loads `.env` automatically, and
|
||
|
|
`.env` is git-ignored. Alternatively, export them as environment variables.
|
||
|
|
|
||
|
|
## 5. Sign in (once)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
node scripts/todo.mjs login
|
||
|
|
```
|
||
|
|
|
||
|
|
This opens your **system browser** to the Microsoft sign-in page (and prints the
|
||
|
|
URL as a fallback if it can't auto-open). Sign in and approve; the browser
|
||
|
|
redirects to a temporary local server and you'll see "Signed in ✓". The refresh
|
||
|
|
token is cached to `scripts/.token-cache.json` (git-ignored, written with `0600`
|
||
|
|
perms). The CLI refreshes silently after that — you won't be asked again until
|
||
|
|
the refresh token expires or is revoked.
|
||
|
|
|
||
|
|
> Run `login` on the machine whose browser you'll use; the temporary redirect
|
||
|
|
> server listens on `127.0.0.1`, so the browser and the CLI must be on the same
|
||
|
|
> host (not over plain SSH without port forwarding).
|
||
|
|
|
||
|
|
## 6. Verify
|
||
|
|
|
||
|
|
```bash
|
||
|
|
node scripts/todo.mjs test
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: `{ "ok": true, "signedInAs": "you@contoso.com", "listCount": N, "lists": [...] }`.
|
||
|
|
|
||
|
|
Troubleshooting:
|
||
|
|
- **`AADSTS53003` (blocked by Conditional Access)** — a CA policy blocked the
|
||
|
|
sign-in. If it targets the *device-code* flow specifically, this browser flow
|
||
|
|
already avoids it. If it requires a **compliant/managed device** (your device
|
||
|
|
shows as *Unregistered*), enroll the Mac (Company Portal/Intune) or have an
|
||
|
|
admin exclude this app. Use the sign-in log's Correlation Id to find the exact
|
||
|
|
policy.
|
||
|
|
- **`redirect_uri` mismatch (`AADSTS50011`)** — add both `http://localhost` and
|
||
|
|
`http://127.0.0.1` under the Mobile-and-desktop platform (step 2).
|
||
|
|
- **Browser redirect never returns / connection refused** — something else holds
|
||
|
|
the port, or you're on a remote/SSH session. Run `login` locally, or pin a port
|
||
|
|
with `TODO_REDIRECT_PORT` and register `http://127.0.0.1:<port>`.
|
||
|
|
- **`AADSTS65001` / consent error during sign-in** — the org requires admin
|
||
|
|
consent for `Tasks.ReadWrite`; have an admin grant it (step 3).
|
||
|
|
- **`test` returns `403`** — the delegated `Tasks.ReadWrite` permission is
|
||
|
|
missing or only `Tasks.Read` was granted.
|
||
|
|
- **Any command returns `"needsLogin": true`** — the cached refresh token is gone
|
||
|
|
or expired; run `login` again.
|
||
|
|
- **Personal Microsoft account won't work** — set `TODO_TENANT_ID=consumers` (or
|
||
|
|
`common`) and register the app for personal accounts in step 1.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Note: graduating to an MCP server
|
||
|
|
|
||
|
|
When you want this available across all sessions as native tools rather than a
|
||
|
|
CLI, build a small MCP server that imports `scripts/lib/graph.mjs`:
|
||
|
|
|
||
|
|
```js
|
||
|
|
import { MSTodoClient } from "./graph.mjs";
|
||
|
|
const client = new MSTodoClient({
|
||
|
|
clientId: process.env.TODO_CLIENT_ID,
|
||
|
|
tenantId: process.env.TODO_TENANT_ID,
|
||
|
|
tokenCachePath: process.env.TODO_TOKEN_CACHE,
|
||
|
|
});
|
||
|
|
// Expose client.listTasks / getTask / createTask / updateTask / deleteTask
|
||
|
|
// (and the list/checklist methods) as MCP tools.
|
||
|
|
```
|
||
|
|
|
||
|
|
The client is dependency-free; its only state is the on-disk token cache. The one
|
||
|
|
difference from an app-only server: you must run `login` once on the host to seed
|
||
|
|
that cache before the server can call Graph. For a containerized HTTP transport,
|
||
|
|
mount the token-cache file as a volume and guard the `/mcp` endpoint with a bearer
|
||
|
|
token — the server acts as the single signed-in user.
|