# 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 → **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:` 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:`. - **`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.