---
title: "Security & scopes"
description: "The authorization model — live is_staff/superuser gate, nine least-privilege scopes, additive destructive, dry-run/confirm, rate limits and audit."
source: "https://docs.blend-ed.com/openedx-mcp/security/"
---

# Security & scopes

## Authentication
Two accepted credentials:

- **`X-MCP-Key`** — an `MCPKey` bearer minted from Django admin.
- The platform’s own **JWT / session** — an Open edX JWT already encodes `is_staff` / `is_superuser` , so the same endpoints work with a raw platform
token too.

## Authorization — one live gate
Whichever credential authenticates, the rule is identical and re-checked **live on every request**: the acting user must be `is_staff` or `is_superuser` right now. Demote them and every key they hold stops working on the next call. A key never caches privilege; scopes only ever **narrow** it.

## Scopes
Tick scopes per key for least privilege. A superuser bypasses narrowing.

| Scope | Gates |
| --- | --- |
| `read` | whoami, analytics, listings, roles, grades, cert/report status |
| `write:enrollment` | enroll, unenroll, bulk_enroll |
| `write:users` | create_user, reset_student_attempts |
| `write:roles` | set_role (course roles), instructor_access |
| `grant:admin` | set_role for `global_staff` / `superuser` (escalation) |
| `write:certificates` | generate / regenerate certificates |
| `write:reports` | submit async reports (grade export, …) |
| `write:courses` | block CRUD, create_block_tree, update_course_settings |
| `destructive` | **additive** — deactivate_user, request_retirement, invalidate_certificate, delete block |

`destructive` is **additive**: a destructive tool needs its domain scope **and** `destructive`, so a key can never delete without also holding the relevant write scope. Granting platform admin is isolated behind `grant:admin`, so an ops key can manage course staff without ever being able to mint a superuser.

## Write rails
Every state-changing tool passes through four rails:

1. **Live authority re-check** — the gate above, per request.
2. **Rate limit** — per- `(key, tool)` fixed window; turns a runaway agent into ~N
calls instead of thousands. Mass tools (bulk enroll, retire) get the tightest
budgets.
3. **Dry-run + confirm token** — high-impact / destructive tools first return a
preview plus a single-use token bound to a fingerprint of the exact payload.
Re-send with the token to apply; changing any argument invalidates it.
4. **Append-only audit** — intent is recorded **before** the write, and the write
is refused if that record can’t be persisted.

> **Production**
> The confirm-token store and rate-limit counter use the ambient Django cache. Point it at a **shared backend (Redis / memcached)**. Under `LocMemCache` the limits multiply per worker and tokens are per-process.
