MCP auth. OAuth 2.1 and Magic Link without pain
How to make your MCP server publicly accessible without shipping bearer tokens around. The control center for any SaaS variant.
If you only run your MCP server for yourself, you don't need authentication. You trust yourself. The moment you make it public for other users, auth is mandatory, otherwise the first random attacker writes into your database. In this lesson we show how that's done cleanly today.
Why OAuth 2.1 and not just bearer tokens
The simple way would be: every user gets an API key, drops it into their claude_desktop_config.json, done. Many MCP servers actually accept that. The problem:
- The key sits as plaintext in a config file. User loses the device, key is lost.
- You can't easily revoke the key without the user pulling a new one and re-entering it everywhere.
- No scopes (just "everything" or "nothing").
- No refresh flow on expiry.
OAuth 2.1 with PKCE solves all of these. The user clicks "log in" once, gets a magic link to email, the client trades that automatically for a token, all behind the scenes. Almost invisible to the user. As operator it gives you full control.
What's different about MCP versus normal OAuth
Normal web OAuth runs in a browser. MCP clients aren't always browsers, they're apps like Claude Desktop, Codex, Cursor. The spec requires:
- The MCP server exposes its OAuth metadata at
/.well-known/oauth-authorization-server. - The client realizes auth is needed on first tool call, opens the user's browser, redirects to the authorization endpoint.
- PKCE is mandatory (code challenge + verifier), because mobile and desktop apps can't safely hold a client secret.
- Refresh-token flow has to be supported so the session lasts longer than an hour without constant re-login.
The Magic Link trick
Instead of managing passwords, use magic links. The user enters their email, you send an email with a one-time link, the link goes to the auth server and releases the code. The client trades it for a token.
Pros:
- No password reset flow (biggest support source in SaaS).
- User can't have a "wrong password".
- The email address is both login and account identifier.
- Works when the user switches device.
Cons:
- You have to set up SMTP cleanly (DKIM, SPF, good sender score). Otherwise your magic links land in spam.
- No offline login (no email access, no login).
For most MCP server applications the trade-off is clear: magic link wins.
The four endpoints to build
GET /.well-known/oauth-authorization-server. Metadata. Static JSON, write once, never touch again.GET /authorize. User-facing login page. Shows an email form, sends the magic link, shows "check your email".GET /callback(target of the magic link), consumes the one-time code, generates the authorization code, redirects back to the client.POST /token, trades authorization code for access + refresh token. Has to verify PKCE.
That's it. No more endpoints. Access token has 1 hour lifetime, refresh token a few days to weeks.
Scopes
Define at least three:
read, can only call read tools.write, write tools allowed.admin, management tools (delete, export, change config).
Most users get read + write. Admin is for you or explicitly granted cases.
On the authorization request the client lists the desired scopes. You show them on the login screen ("this app would like to: read, write"), the user accepts or denies.
Token storage in the MCP client
The client (Claude Desktop, Codex) typically stores the token encrypted in the system keychain (Mac) or credential manager (Windows). You as server operator don't have to worry about that.
What you should do: clearly communicate token expiry. When a token expired, return a clean 401 with WWW-Authenticate: Bearer error="invalid_token", then the client knows to refresh.
Mistakes you'll make
- Forgetting the state parameter. OAuth state guards against CSRF. Without it an attacker can redirect you into a login. Mandatory.
- PKCE verifier not checked. If your server doesn't actually verify the verifier, PKCE is worthless.
- Refresh token with short lifetime. If refresh expires after an hour, the user has to re-login constantly. Refresh isn't access. 14 days or more is normal.
- No token revocation. When a user loses their device, they need to be able to revoke the token. Build a dashboard endpoint.
- Forgot scope check in the handler. Each tool handler has to verify the submitted token has the necessary scope. Otherwise you bypass all the work in one line of code.
Tenant isolation, the most important mistake to avoid
If your server has multiple users, EVERY database query must have a tenant_id filter. User A must not see user B's data. That sounds obvious but is forgotten somewhere almost always: in findUnique, in search functions, in batch deletes.
Recommended pattern: tenant_id as a mandatory field in every DB model + a static CI scanner that checks all Prisma/SQL queries contain tenant_id. A bug here is a data leak across user boundaries, the worst error a SaaS server can make.
Minimum viable example
The core flow in pseudocode:
POST /authorize (with email, client_id, code_challenge, state)
-> generate magic_token (random), store {email, client_id, code_challenge, state, expires: +15min}
-> send email with link: /callback?magic_token=xxx
-> show "check your email" page
GET /callback?magic_token=xxx
-> lookup magic_token, invalidate immediately
-> generate authorization_code (random, 5min lifetime)
-> redirect to client_redirect_uri?code=xxx&state=original_state
POST /token (with code, code_verifier)
-> verify code exists, not expired, not consumed
-> verify PKCE: SHA256(code_verifier) == original code_challenge
-> generate access_token + refresh_token, return
-> mark code as consumed
That's the complete flow. Everything else is decoration.
What you still need in production
Rate limiting on /authorize (otherwise spam). HMAC-signed tokens (pepper + secret). Logging per login attempt. Admin dashboard for token overview and revocation. All offloaded concerns, no show-stoppers. You can start with the minimal flow and add as you go.
Onwards
The StudioMeyer Memory MCP (memory.studiomeyer.io) uses exactly this pattern. The OSS mirror for the portable memory pattern is at github.com/studiomeyer-io/local-memory-mcp. The auth layer itself lives in the private main repo, but the pattern is documented and portable.
Lesson 7 shows how to combine multiple authenticated MCP servers into a multi-agent setup. That's the scaling chapter.