Model Context Protocol (MCP): Connect AI to Your Tools and DataAuthorization and security · Lesson 11 of 18
Authorization for remote MCP servers: OAuth done right
Video lecture
Authorization for remote MCP servers: OAuth done right
The narrated lecture is in production
Every chapter is scripted and ready. Browse the chapters and read the full transcript now — the video will appear here when it’s published.
Chapters
Transcript of the narration, chapter by chapter.
0:00 Authorization for remote servers
The moment an MCP server goes remote and touches real business data, authorization stops being optional in practice. In this lesson you'll learn how MCP uses OAuth, step by step, the two rules teams most often break, and how to verify tokens in the Python SDK.
0:20 Why authorization matters
Why get authorization exactly right? Because an MCP server is often a door into your most sensitive systems, and it's used by software acting on behalf of people. Think of a hotel key card. It opens your room, only your room, and only during your stay. It doesn't open the room next door, and the hotel doesn't give your card to the laundry company so they can get in. OAuth for MCP is that key card system: tokens scoped to one server, with specific permissions, for a limited time.
0:59 Roles
First, scope. Authorization in MCP is optional and applies to HTTP based transports. Standard I O servers shouldn't use the OAuth flow at all; they take credentials from their environment. For remote servers, there are three roles. The MCP server is an OAuth two point one resource server that accepts and validates tokens. The authorization server, your identity provider such as Microsoft Entra ID, Okta or Auth0, issues tokens. And the MCP client inside the host is the OAuth client acting for the user.
1:36 Discovery and registration
Here's the flow. The client calls the server without a token and gets a four oh one, pointing to the server's protected resource metadata. That document names the authorization server and scopes. The client then fetches the authorization server's own metadata. Next it needs a client id. The preferred way is a client ID metadata document, where the client id is an https URL describing the client. Dynamic client registration still works but is now deprecated.
2:09 Authorization code + PKCE
Then comes the authorization code flow with PKCE. Crucially, the client sends a resource parameter with the MCP server's canonical URL, in both the authorization and token requests, so the token is bound to that specific server. When the browser redirects back, the client checks the issuer parameter matches the authorization server it expected before redeeming the code. That closes a class of mix up attacks. Finally, the client sends the token as a bearer header on every request, and never in a URL.
2:46 Simple example: the wrong-audience token
A simple example of audience validation. Your company uses one identity provider for everything. An employee's AI app gets a token for the HR system. By mistake, or through an attack, that token is sent to your CRM MCP server. The signature is valid, it's not expired, and it came from your identity provider. Should the CRM server accept it? No. The token's audience says HR, not CRM. A server that only checks the signature would accept it. A server that checks the audience rejects it with a four oh one.
3:26 Server-side validation
On the server side, validate every token: signature or introspection, expiry, scopes, and, critically, audience. The token must have been issued for this server. Invalid tokens get four oh one. Missing scopes get four oh three with a scope challenge, so the client can ask the user for more permission, which is called step up authorization. And stored client credentials are tied to the authorization server that issued them.
3:56 Two critical rules
Two rules get broken all the time. Rule one, audience: accept only tokens issued for your server. A token for the HR API, even from the same identity provider, must be rejected. Rule two, no token passthrough: never forward the client's token to downstream APIs. If your server calls a CRM vendor's API, it gets its own credential for that API. Passthrough creates a confused deputy: downstream services can't tell who is really acting, and audit trails and rate limits break.
4:31 Scopes and least privilege
Scopes are how you apply least privilege. Define them around capabilities, like crm read, crm write and campaigns pause. Map each tool to a scope and check it in the handler. Give people read scopes by default and request write scopes through step up only when they try a write. Enterprises can use the enterprise managed authorization extension, so the company identity provider centrally controls which servers and scopes employees get, and machine to machine jobs can use the client credentials extension.
5:07 Example: agency CRM on Entra ID
A worked example. An agency with offices in Lahore and London secures its CRM server with Microsoft Entra ID. The server publishes metadata listing Entra and two scopes. Hosts discover it, register and run PKCE flows with the server's URL as the resource. The server validates signature, issuer, audience and expiry, and returns four oh three with a write scope challenge when an analyst tries to update a deal stage. To reach the CRM vendor, it uses its own service credential and logs the real user. The hands on code shows a JWT verifier plugged into the Python SDK's auth settings.
5:51 API keys vs OAuth
A question from developers: can I use an API key instead of OAuth for my remote server? For internal machine to machine use, like a scheduled job calling your server, a well scoped credential or the OAuth client credentials extension can be appropriate. But for people using AI hosts, OAuth is what hosts expect, because it gives each user their own consent, scopes and revocation. Shared API keys make it impossible to know which person did what.
6:24 Deeper: the Entra rollout (illustrative)
Let's deepen the Lahore and London agency's Entra setup. In the first month, illustrative numbers, analysts triggered the write scope step up a handful of times, always when trying to update a deal stage they didn't own, and each was correctly refused. The security team's negative tests caught one configuration mistake before launch: the server initially accepted tokens issued for the agency's general API because both used the same tenant. Adding the audience check fixed it. They also reviewed audit logs for the CRM vendor calls and could see the real analyst behind each action, because the server logged the user from the token while calling the vendor with its own credential.
7:13 Watch me do it: JwtVerifier
Watch me do it. Let's walk through the JWT verifier. At the top, I read the resource URL, the issuer and the JWKS URL from the environment, and create a JWKS client that fetches the identity provider's signing keys. Verify token gets the signing key for this token, then decodes it with RS two fifty six, requiring the audience to equal my resource URL and the issuer to equal my expected issuer. Any error returns none, and the SDK answers four oh one. If it passes, I read scopes from either the scp or the scope claim, depending on the provider, and return an access token with the client id, scopes, expiry and resource. The server is created with this verifier and auth settings: the issuer URL, the resource server URL, a required read scope, and resource validation on. Now I test: a token for a different audience, rejected; an expired token, rejected; a valid read token, accepted.
8:22 Try this now
Try this now. Draw the OAuth flow for your remote server with your actual identity provider: the first four oh one, the metadata, discovery, registration, the PKCE flow with the resource parameter, the issuer check and the calls with the token. List each tool and the scope it needs. Then write three negative tests you can run against staging: a token for the wrong audience, an expired token, and a read token calling a write tool. If all three fail correctly, you're in good shape.
8:59 Recap
To recap: OAuth applies to remote servers. Servers publish protected resource metadata and validate audience, expiry and scopes. Clients use PKCE, resource indicators and issuer checks, and prefer client ID metadata documents. Never pass tokens through. Your next step: draw the flow for your server with your identity provider, map scopes to tools, and write three negative tests: wrong audience, expired token and missing scope.
When authorization applies
Authorization in MCP is optional and applies to HTTP-based transports. stdio servers should not use the OAuth flow; they take credentials from the environment. For any remote server that touches non-public data, you need it.
The roles
- The MCP server is an OAuth 2.1 resource server: it accepts and validates access tokens.
- The authorization server (your identity provider or a dedicated auth service: Microsoft Entra ID, Okta, Auth0, Keycloak, Google, etc.) issues tokens.
- The MCP client (inside the host) is an OAuth client acting on behalf of the user.
The flow, step by step (2026-07-28)
- The client calls the server without a token; the server returns 401 with a
WWW-Authenticateheader pointing to its Protected Resource Metadata (RFC 9728). Servers must implement this metadata; it names the authorization server(s) and supported scopes. - The client fetches the authorization server's metadata via OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery; clients must support both.
- The client obtains a client ID. Preferred: Client ID Metadata Documents (CIMD), where the client ID is an HTTPS URL pointing to a JSON document describing the client (name, redirect URIs). Alternatives: pre-registration, or Dynamic Client Registration (RFC 7591), which 2026-07-28 deprecates in favor of CIMD (still available for compatibility; clients must set an appropriate
application_type). - Authorization code flow with PKCE. The client sends the
resourceparameter (RFC 8707 Resource Indicators) with the MCP server's canonical URI in both the authorization and token requests, so the token is bound to that server. - On the redirect back, the client validates the
issparameter (RFC 9207) against the authorization server it expected before redeeming the code, closing mix-up attacks. - The client calls the MCP server with
Authorization: Bearer <token>on every request (never in the query string). - The server validates the token: signature/introspection, expiry, audience (issued for this server), and scopes. Invalid or expired tokens get 401; insufficient scopes get 403 with a scope challenge so clients can do step-up authorization.
Client credentials are bound to the authorization server that issued them: clients must key stored credentials by issuer and re-register if the authorization server changes.
The two rules people break
- Audience validation: a server must only accept tokens issued for it. A token for another API, even from the same identity provider, must be rejected.
- No token passthrough: a server must not accept tokens intended for others and must not forward the client's token to downstream APIs. If your server calls a downstream API (say, a CRM), it obtains its own credential for that API (for example via its own OAuth client, a token exchange, or URL-mode elicitation for the user to connect that account). Passthrough creates a confused deputy: downstream services can't tell who is really acting, and audit trails and rate limits break.
Scopes and least privilege
Define scopes around capabilities, for example crm:read, crm:write, campaigns:pause. Map tools to scopes and check them in every handler. Start users with read scopes; request write scopes via step-up only when they try a write tool. Enterprise deployments can use the Enterprise-Managed Authorization extension so the company IdP centrally controls which MCP servers and scopes employees can use; machine-to-machine jobs can use the OAuth client credentials extension.
Worked example: securing a CRM MCP server for a Lahore–London agency
- Authorization server: the agency's Microsoft Entra ID tenant.
- Server publishes Protected Resource Metadata listing Entra as the authorization server and scopes
crm.read,crm.write. - Hosts (Claude connector, VS Code) discover, register via CIMD where supported, and run PKCE flows with
resource=https://mcp.agency.example/mcp. - The server validates JWT signature, issuer, audience and expiry; maps tools to scopes; returns 403 with
scope="crm.write"when an analyst triescrm_update_deal_stage. - To call the CRM vendor's API, the server uses its own OAuth client credentials with a service account limited to the agency's workspace, and records the end user's identity in its audit log.
Hands-on: token verification in the Python SDK
import os
import jwt # pip install pyjwt[crypto]
from jwt import PyJWKClient
from pydantic import AnyHttpUrl
from mcp.server import MCPServer
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings
RESOURCE = os.environ["MCP_RESOURCE_URL"] # e.g. https://mcp.agency.example/mcp
ISSUER = os.environ["OAUTH_ISSUER"] # your authorization server's issuer URL
jwks = PyJWKClient(os.environ["OAUTH_JWKS_URL"])
class JwtVerifier(TokenVerifier):
async def verify_token(self, token: str) -> AccessToken | None:
try:
key = jwks.get_signing_key_from_jwt(token).key
claims = jwt.decode(token, key, algorithms=["RS256"], audience=RESOURCE, issuer=ISSUER)
except jwt.PyJWTError:
return None # SDK responds 401
scopes = claims.get("scp", claims.get("scope", "")).split()
return AccessToken(token=token, client_id=claims.get("azp", claims.get("client_id", "unknown")),
scopes=scopes, expires_at=claims.get("exp"), resource=RESOURCE)
mcp = MCPServer(
"crm",
token_verifier=JwtVerifier(),
auth=AuthSettings(issuer_url=AnyHttpUrl(ISSUER), resource_server_url=AnyHttpUrl(RESOURCE),
required_scopes=["crm.read"], validate_token_resource=True),
)The SDK serves the Protected Resource Metadata and enforces the required scopes; inside tools, check finer-grained scopes before writes. Claim names differ between identity providers (for example scp vs scope), and some issue opaque tokens that require introspection instead of local JWT validation: check your provider's documentation. The TypeScript SDK offers equivalent middleware (requireBearerAuth) in its Express package.
Pitfalls
- Accepting any valid JWT from your IdP without checking audience.
- Forwarding the user's token to downstream APIs.
- Putting tokens in URLs or logs.
- Granting write scopes by default.
- Skipping
issvalidation on the client side.
Measuring success
Security tests: tokens for other audiences rejected, expired tokens rejected, scope enforcement per tool, no tokens in logs. Operationally: auth failure rates and step-up prompts per week.
Key takeaways
- MCP authorization applies to HTTP transports; stdio servers use environment credentials.
- The MCP server is an OAuth 2.1 resource server publishing Protected Resource Metadata (RFC 9728).
- Clients use PKCE, resource indicators (RFC 8707) and issuer validation (RFC 9207); CIMD is preferred over deprecated DCR.
- Servers must validate audience and never pass client tokens through to downstream APIs.
- Map tools to scopes, start with read scopes, and use step-up for writes.
Check your understanding
Quick questions to lock in the lesson. They don’t count towards your certificate.
Put it into practice
Draw the OAuth flow for your remote server with your identity provider, list scopes per tool, and write three negative security tests (wrong audience, expired token, missing scope).
Enrol for free to save your progress
Reading is always free. Enrol to keep your place, take the final assessment and earn a verifiable certificate.