When we started building the NewzAI MCP server, security was non-negotiable from day one. MCP tools are powerful — they can fetch, filter, and surface live news data on behalf of users. We needed a proper authentication layer, not a shared API key sitting in a config file.
OAuth 2.0 was the obvious choice. It's the standard MCP clients expect, it gives us scoped access, token expiry, and a proper refresh cycle. And for the identity provider, we already use Google across our stack, so Google OAuth was a natural fit.
A quick note on the stack before we get into it: the NewzAI MCP server is built on FastMCP — the Python framework for building MCP servers — running on a Python backend. FastMCP handles the MCP protocol layer, tool registration, and request routing. The OAuth implementation described in this post sits on top of that as an auth middleware layer, plugging into FastMCP's auth hooks.
Then we hit a wall.
/register endpoint. Google requires every OAuth client to be created manually in the Google Cloud Console — no exceptions.
We had two options: fork our MCP clients to handle Google-specific quirks, or build an abstraction layer that lets the MCP ecosystem work normally while Google stays behind the scenes. We chose the latter.
01 The OAuth Proxy Pattern
The solution is an OAuth proxy that sits between the MCP client and Google OAuth. From the client's perspective, it's talking to a perfectly standards-compliant OAuth 2.0 authorization server. Internally, it's translating those requests into Google's OAuth flow using pre-configured credentials.
This pattern gives us clean separation: the MCP client never needs to know about Google's OAuth quirks, and Google never needs to know about MCP's Dynamic Client Registration expectations.
02 What the Client Sees: Discovery Metadata
The first thing any MCP client does when it boots up is hit the well-known discovery endpoint. This single request tells it everything it needs to know about how to authenticate.
Our proxy serves this live at api.newzai.ai/.well-known/oauth-authorization-server — you can open it in your browser right now and see the full document. Here's what it returns:
{
"issuer": "https://api.newzai.ai/",
"authorization_endpoint": "https://api.newzai.ai/authorize",
"token_endpoint": "https://api.newzai.ai/token",
"registration_endpoint": "https://api.newzai.ai/register",
"scopes_supported": [
"openid",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": [
"client_secret_post", "client_secret_basic"
],
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true
}
We implement RFC 8414 (OAuth 2.0 Authorization Server Metadata), not full OpenID Connect Discovery — so /.well-known/openid-configuration intentionally returns 404. MCP only needs RFC 8414, and implementing less surface area means fewer things to secure. PKCE with S256 is non-negotiable — every authorization request must include a code challenge.
03 Exposed Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /.well-known/oauth-authorization-server | Discovery — client reads this on boot |
| POST | /register | Dynamic client registration (RFC 7591) |
| GET | /authorize | Authorization — only browser-facing step |
| POST | /token | Code exchange and token refresh |
| POST | /revoke | Token revocation |
| GET | /userinfo | Returns authenticated user claims |
| GET | /jwks.json | Public keys for JWT verification |
04 The Full Authentication Flow
Here's what actually happens from the moment an MCP client connects to when it starts making authenticated tool calls.
Click a step to highlight the flow
/.well-known/oauth-authorization-server, reads all endpoint URLs into memory. Happens once per session./register with its name and redirect URIs. Proxy generates a client_id + client_secret, encrypts them, and persists to Redis. Client stores and reuses these forever./authorize with a PKCE code challenge. Proxy redirects to Google, user logs in, Google redirects back to the proxy, proxy redirects to the client with a proxy-issued code./token. Proxy verifies PKCE, calls Google to exchange for a Google token, then mints and returns a proxy-signed JWT. Google's token never leaves the proxy.Authorization: Bearer <jwt> on every MCP request. Proxy validates the signature. No Google round-trip needed./token. Gets a fresh JWT back. No user interaction required.The key insight in step 4: by minting its own JWT rather than forwarding Google's token, the proxy fully owns the token lifecycle. Changes to Google's token format, signing keys, or expiry policy don't affect MCP clients at all.
05 How Client Credentials Are Stored
When a client registers, we need to durably store its credentials so they survive proxy restarts and work across multiple proxy replicas. We use a two-layer storage stack:
GoogleProvider( client_storage=FernetEncryptionWrapper( key_value=RedisStore( host=db_cfg.host, port=db_cfg.port, password=db_cfg.password, ), fernet=Fernet(auth_cfg.mcp.storage_encryption_key), ), )
The flow on a write: the proxy generates credentials → FernetEncryptionWrapper encrypts with AES-128-CBC + HMAC-SHA256 → RedisStore persists the ciphertext. On a read, it runs in reverse. Redis only ever sees opaque ciphertext — the storage_encryption_key is the only thing that can decrypt it.
This means a Redis breach doesn't compromise client credentials. The encryption layer is transparent to the rest of the proxy — it just calls .get() and .set().
07 Not Everyone Needs OAuth: API Key Support
The full OAuth flow — discovery, registration, browser redirect, PKCE, token exchange — is the right model for interactive clients and user-facing MCP integrations. But a lot of real-world MCP usage isn't interactive at all. Think agentic pipelines, CI automations, server-to-server integrations, internal tooling. These callers don't have a browser. They just need a token they can drop into an Authorization header and move on.
But here's the thing: it's not just automated agents that benefit. Interactive users and developers who want a simpler integration path — without going through the full OAuth browser flow — can also use API keys. Keys are issued on request, valid immediately, and work the same way regardless of whether the caller is a human or a pipeline.
For these cases, NewzAI MCP supports pre-issued static API keys. Keys are scoped at issuance time and verified by a StaticTokenVerifier on every call. No OAuth dance required.
Two tiers of keys
We issue keys at two levels. Both can be held by agents, developers, or interactive users — the difference is capability, not who's asking:
static-client-<id>static-client-powerful-<id>Under the hood, each tier is backed by a StaticTokenVerifier that maps every issued token to a client identity at startup. Verification is a pure in-memory lookup on every request — no database, no network call:
StaticTokenVerifier(
each issued token → { client_id: "static-client-<id>", tier: "standard" }
)
// Powerful verifier — premium tier
StaticTokenVerifier(
each issued token → { client_id: "static-client-powerful-<id>", tier: "powerful" }
)
Authorization: Bearer <your-api-key>. The MultiAuth layer handles routing to the right verifier automatically.
08 MultiAuth: One Entry Point, Three Auth Paths
So we now have three distinct ways a caller can authenticate with NewzAI MCP: the full OAuth flow via the Google proxy, a standard static API key, or a powerful static API key. In a naive implementation, you'd check each method sequentially and handle failure cases manually. That gets messy fast — especially when you want to add a fourth method later.
Instead, we wrap everything in a MultiAuth layer that acts as a single authentication entry point:
return MultiAuth( server=provider, # GoogleProvider (OAuth proxy) verifiers=[ verifier, # StaticTokenVerifier — standard keys powerful_verifier, # StaticTokenVerifier — powerful keys ], required_scopes=[], )
Every incoming request goes through MultiAuth first. Here's how it resolves authentication:
MultiAuth reads the Authorization: Bearer <token> header from the request. If it's missing, the request is rejected immediately with a 401.verifier (standard keys), then powerful_verifier (powerful keys). These are fast in-memory lookups. If either matches, authentication succeeds.MultiAuth hands it off to the GoogleProvider OAuth server. The token is validated as a proxy-issued JWT.From the MCP tool's perspective, none of this is visible. By the time a tool handler runs, it just sees an authenticated request with a client ID and a set of resolved scopes. Whether that came from Google OAuth or a static API key is an implementation detail the tool doesn't care about.
Why this structure works well
The clean separation between server and verifiers in MultiAuth means adding a new auth method is as simple as appending to the verifiers list. The OAuth proxy handles all the protocol complexity; the static verifiers handle the zero-friction path; and MultiAuth just tries them in order.
09 Security Properties
Taken together, the three auth paths share a set of security guarantees that apply regardless of how a caller authenticates.
All OAuth flows require S256 code challenges. Prevents authorization code interception attacks, especially important for public MCP clients.
Client credentials are Fernet-encrypted before hitting Redis. AES-128-CBC + HMAC-SHA256. A Redis breach does not expose client credentials.
OAuth clients receive proxy-signed tokens, not Google's. The proxy owns the full token lifecycle — Google key rotations are invisible to MCP clients.
The Google client_id and client_secret are held exclusively by the proxy. No MCP client or API key holder ever touches them.
Standard and powerful keys are scoped at issuance. Capabilities are enforced by the verifier on every request — not self-declared by the caller.
MultiAuth means auth logic lives in one place. No scattered middleware, no per-tool checks. Adding or revoking an auth method is a single config change.
base_url is the single source of truth for the proxy's identity. It drives the issuer claim in every JWT, the redirect URI sent to Google, and every URL in the discovery document. If it's wrong — even a trailing slash — you'll see redirect_uri_mismatch errors from Google and issuer validation failures in clients.
10 Wrapping Up
The OAuth proxy pattern turned out to be a clean solution to what initially seemed like a hard constraint. Instead of fighting Google's static client model or forking our MCP clients, we built a thin translation layer that speaks NewzAI MCP OAuth natively while routing all actual authentication through Google.
On top of that, static API keys give agentic and server-to-server callers a zero-friction path — no browser, no token exchange, just a key and a header. And MultiAuth stitches all three paths together at a single entry point without any branching logic in the tools themselves.
If you're building MCP tooling and want to support both interactive users and automated agents, this combination — OAuth proxy + tiered static keys + MultiAuth — covers the full spectrum without requiring your tools to know anything about how a caller authenticated. You can see it live at api.newzai.ai/.well-known/oauth-authorization-server.
Try NewzAI MCP
NewzAI MCP exposes real-time news search, filtering, and personalisation as MCP tools — ready to drop into any MCP-compatible client with full OAuth support.
Questions or need an API key? Reach out at [email protected]
Try NewzAI MCP →