Authorize remote servers with OAuth
Follow how a client discovers the authorization server, gets a token for one server, and steps up scopes.
- Walk through the MCP authorization flow from 401 to access token
- Explain why tokens are bound to one server (audience)
- Request only the scopes needed, and step up when challenged
Remote MCP servers usually protect their tools with OAuth 2.1. The roles:
- the MCP server is an OAuth resource server - it accepts and checks access tokens,
- the MCP client is an OAuth client acting for the user,
- an authorization server signs the user in and issues tokens (it may be run by the same company or a separate identity provider).
Authorization is optional in MCP and meant for HTTP. Local stdio servers instead get credentials from their environment.
Try it
From 401 to an access token
Step through the flow a client follows the first time it connects to a protected server. Predict the steps marked with a question.
Least privilege and step-up
Clients should ask for the smallest set of scopes: those named in the 401’s scope parameter if present. Later, when the user tries something that needs more - say, writing a file - the server answers 403 with error="insufficient_scope" and the scopes needed. The client then re-authorizes with the union of its previous scopes and the new ones (so it doesn’t lose what it had), and retries a limited number of times.
Client registration: the preferred method is a Client ID Metadata Document - the client_id is an HTTPS URL describing the client. Dynamic Client Registration still works but is deprecated.
granted = {"files:read"}
challenged = {"files:write"}
print(" ".join(sorted(granted | challenged)))files:read files:write
Key takeaways
401 → resource metadata → authorization server metadata → PKCE authorization with
resource→ token → Bearer requests.Tokens are bound to one MCP server (audience); servers reject everything else and never pass tokens through.
Start with minimal scopes; on 403
insufficient_scope, re-authorize with the union of scopes.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write Python
Write Python in the editor and run it against sample inputs. Python runs locally in your browser using a WebAssembly runtime.
Check an access token
The first line is this server’s canonical URI. The second line is the scope an operation needs. Each following line describes a request’s token as JSON (or the word none for no token): {"aud": ..., "scopes": [...], "expired": true/false}.
For each, print 401 missing token, 401 wrong audience, 401 expired, 403 insufficient_scope NEEDED, or 200 ok - checking in that order.
- Every outcome
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Step up scopes
The first line lists the scopes the client already requested. Each following line is the scope value from a 403 insufficient_scope challenge (space-separated). After each challenge, print the scope set to request next: the union so far, sorted and space-separated.
- Two step-ups
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…