engineering
FastMCP · Python · OAuth · API Keys · MultiAuth

How We Secured NewzAI MCP: OAuth, API Keys, and MultiAuth

Google OAuth is great for user auth but doesn't support Dynamic Client Registration. API keys are simpler but lack scoped delegation. Here's how we built a layer for NewzAI MCP that handles both — at the same time.

NewzAI Engineering May 2026 11 min read

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.

The problem: Google OAuth doesn't support Dynamic Client Registration (DCR). MCP clients expect to register themselves automatically at runtime by calling a /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.

MCP framework
FastMCP (Python)
Backend language
Python
Identity provider
Google OAuth 2.0
Auth layer
OAuth proxy + MultiAuth
Token storage
Redis + Fernet encryption
Discovery spec
RFC 8414 + RFC 7591 (DCR)
architecture

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.

MCP client initiates OAuth flow OAUTH PROXY Exposed to client /.well-known/... /authorize /token /register /userinfo /jwks.json /revoke Internal mapping Google client_id Google client_secret Fernet-encrypted Redis storage proxy-signed JWTs Google OAuth identity provider OAuth requests proxy-issued JWT mapped req auth response MCP client ↔ proxy proxy ↔ Google (internal) proxy boundary

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.

internals

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:

json
{
  "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.

endpoints

03 Exposed Endpoints

MethodPathPurpose
GET/.well-known/oauth-authorization-serverDiscovery — client reads this on boot
POST/registerDynamic client registration (RFC 7591)
GET/authorizeAuthorization — only browser-facing step
POST/tokenCode exchange and token refresh
POST/revokeToken revocation
GET/userinfoReturns authenticated user claims
GET/jwks.jsonPublic keys for JWT verification
flow

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

Click a step above to highlight the flow across all five layers.
01
Discovery
Client boots, hits /.well-known/oauth-authorization-server, reads all endpoint URLs into memory. Happens once per session.
02
Dynamic client registration (first time only)
Client POSTs to /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.
03
Authorization
User's browser is redirected to /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.
04
Token exchange
Client POSTs the code + PKCE verifier to /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.
05
Authenticated MCP calls
Client sends Authorization: Bearer <jwt> on every MCP request. Proxy validates the signature. No Google round-trip needed.
06
Token refresh
When the JWT expires, client silently POSTs the refresh token to /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.

crypto

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:

python
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().

api keys

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:

Standard key
Limited access to MCP tools
Powerful key
For premium users — extended capabilities
Sample client ID
static-client-<id>
Sample client ID
static-client-powerful-<id>
Who uses it
Agents, pipelines, developers, anyone wanting a simple integration path
Who uses it
Premium users and integrations that need richer access beyond the standard tier
Need a key? Keys are issued on request — reach out to [email protected] to get one. Mention whether you need a standard or powerful key and a brief description of your use case.

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:

// Standard verifier — limited access tier
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" }
)
How to use a static key: Pass it as a Bearer token — the same header you'd use with an OAuth JWT. Authorization: Bearer <your-api-key>. The MultiAuth layer handles routing to the right verifier automatically.
multiauth

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:

python
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:

1
Extract Bearer token
MultiAuth reads the Authorization: Bearer <token> header from the request. If it's missing, the request is rejected immediately with a 401.
2
Try static verifiers first
The token is checked against verifier (standard keys), then powerful_verifier (powerful keys). These are fast in-memory lookups. If either matches, authentication succeeds.
3
Fall through to OAuth
If no static verifier claims the token, MultiAuth hands it off to the GoogleProvider OAuth server. The token is validated as a proxy-issued JWT.
4
All verifiers exhausted → 401
If the token doesn't match any static key and fails JWT validation, the request is rejected. No partial authentication, no fallback.

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.

Auth method
Google OAuth (full flow)
Best for
Interactive clients, user-facing integrations, MCP-native clients
Auth method
Static key — standard
Best for
Agents, pipelines, developers, or any caller who wants a simple token
Auth method
Static key — powerful (premium users)
Best for
Premium users and integrations needing extended capabilities
security

09 Security Properties

Taken together, the three auth paths share a set of security guarantees that apply regardless of how a caller authenticates.

P
PKCE enforced

All OAuth flows require S256 code challenges. Prevents authorization code interception attacks, especially important for public MCP clients.

E
Encrypted at rest

Client credentials are Fernet-encrypted before hitting Redis. AES-128-CBC + HMAC-SHA256. A Redis breach does not expose client credentials.

J
Proxy-issued JWTs

OAuth clients receive proxy-signed tokens, not Google's. The proxy owns the full token lifecycle — Google key rotations are invisible to MCP clients.

G
Google creds isolated

The Google client_id and client_secret are held exclusively by the proxy. No MCP client or API key holder ever touches them.

K
Tiered key access

Standard and powerful keys are scoped at issuance. Capabilities are enforced by the verifier on every request — not self-declared by the caller.

M
Single auth entry point

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.

One thing to watch: 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.
closing

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 →