npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rakettitiede/mcp-oauth-proxy

v3.1.0

Published

Express middleware and router bridging Custom GPT's OAuth expectations to Google as IdP, with API-key fallback. Extracted from Rakettitiede MCP servers.

Readme

@rakettitiede/mcp-oauth-proxy

Express middleware and router that bridges Custom GPT's OAuth expectations to Google as the identity provider, with API-key fallback.

Extracted from Rakettitiede's MCP servers where identical copies of this code lived in two places. Published so both consumers can depend on a single source of truth — and so external projects with the same pattern can use it too.

What it does

Custom GPT Actions require OAuth 2.0, but most Express apps don't want to run a full OAuth provider. This package provides a minimal proxy that:

  1. Accepts Custom GPT's OAuth dance (/authorize, /callback, /token)
  2. Delegates identity to Google (existing IdP, existing consent screen)
  3. Returns Google's tokens directly — no custom JWT minting, no session layer
  4. Validates incoming Bearer tokens via Google's tokeninfo endpoint
  5. Falls back to API key auth for programmatic / service-to-service calls

Two separate factories — use either or both.

Install

npm install @rakettitiede/mcp-oauth-proxy

Requires Node.js ≥ 20. Express 4 or 5 as a peer dependency.

Quick start

import express from 'express';
import {
  createRequireAuth,
  createOAuthRouter,
} from '@rakettitiede/mcp-oauth-proxy';

const app = express();

const requireAuth = createRequireAuth({
  apiKey: process.env.API_KEY,
  googleClientId: process.env.GOOGLE_CLIENT_ID,
  // Required together for IAM service-to-service auth (fail closed if omitted):
  iamAudience: process.env.SERVICE_URL,
  allowedIamCallers: (process.env.ALLOWED_IAM_CALLERS || '').split(',').filter(Boolean),
});

const { oauthRouter, oauthMeta } = createOAuthRouter({
  googleClientId: process.env.GOOGLE_CLIENT_ID,
  googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
});

app.use('/oauth', oauthRouter);
app.use('/api', requireAuth, yourProtectedRoutes);

app.listen(8080, () => console.log(oauthMeta.startupLog));

createRequireAuth(config)

Returns an Express middleware that authenticates requests via Bearer token (Google-issued) OR API key. On success, req.user is populated; on failure, responds with 401.

Config

| Field | Type | Required | Default | Description | |---|---|---|---|---| | apiKey | string | ✅ | — | The API key to accept. Throws at factory time if missing. | | googleClientId | string | | — | Optional. Your Google OAuth 2.0 client ID (used to validate the aud claim on OAuth access tokens). When omitted, the OAuth access token validation path is disabled and non-JWT Bearer tokens fall through to API key or 401. Use this if you do not need Custom GPT integration. | | iamAudience | string | for IAM | — | Expected aud on IAM identity tokens (typically this service's URL). Required together with allowedIamCallers for IAM auth to succeed. | | allowedIamCallers | string[] | for IAM | — | Exact service-account emails allowed for IAM auth. Empty or omitted → IAM path never accepts (fail closed). | | googleTokeninfoUrl | string | — | https://oauth2.googleapis.com/tokeninfo | Google's tokeninfo endpoint. Override for testing. | | logger | object | — | console | Any object with .log() and .error(). | | nodeEnv | string | — | process.env.NODE_ENV | Controls log verbosity. Non-production logs more. |

Authentication flow

The middleware evaluates these in order and uses the first that succeeds:

  1. JWT-shaped Bearer token → validated as a Google IAM identity token (service-to-service). Requires both iamAudience and a non-empty allowedIamCallers. The token's aud must equal iamAudience, and email must be an exact allowlist match. If either config is missing, IAM auth is skipped (fail closed) and the request falls through. Callers must mint identity tokens with audience = iamAudience.
  2. Non-JWT Bearer token → validated as a Google OAuth access token. Must have aud equal to the configured googleClientId.
  3. API key → accepted from X-API-Key header or api_key query parameter, matched against the configured apiKey.
  4. Fallback → 401 Unauthorized.

If googleClientId is not provided, step 2 (OAuth access token validation) is skipped entirely. OAuth-only or API-key-only deployments can omit the IAM fields; IAM Bearer tokens will not be accepted until both are set.

Migrating to v3 (IAM)

v3 is a breaking change for IAM callers: audience is no longer derived from X-Forwarded-* / Host, and any service account is no longer accepted. Pass iamAudience (your service URL) and allowedIamCallers (caller SA emails). OAuth and API key paths are unchanged.

req.user shape

{
  id: string,              // Google sub, or 'api-key'
  email: string | null,
  authMethod: 'iam' | 'oauth' | 'api-key',
}

createOAuthRouter(config)

Returns { oauthRouter, oauthMeta }:

  • oauthRouter — The Express Router. Mount with app.use('/oauth', oauthRouter).
  • oauthMeta — A frozen metadata object describing what this router mounts:
    • oauthMeta.startupLog — string suitable for printing in your app.listen callback (e.g. "🔐 OAuth: GET /oauth/authorize, ...")
    • oauthMeta.endpoints — { authorize, callback, token } map of route paths

Use oauthMeta to compose your server's startup logs without hardcoding OAuth route paths in consumer code.

Endpoints

The router contains three endpoints that together form an OAuth 2.0 authorization server, delegating the actual authentication to Google. Mount wherever you want the OAuth endpoints to live (conventionally /oauth).

Config

| Field | Type | Required | Default | Description | |---|---|---|---|---| | googleClientId | string | ✅ | — | Your Google OAuth 2.0 client ID. | | googleClientSecret | string | ✅ | — | Your Google OAuth 2.0 client secret. | | tokenStore | object | — | in-memory Map | Storage for the short-lived code↔tokens mapping. See below. | | tokenTtlMs | number | — | 300_000 (5 min) | How long stored codes are valid. | | fetchImpl | function | — | global fetch | HTTP client used for Google token exchanges. Primarily injectable for tests. | | logger | object | — | console | Any object with .log() and .error(). |

Endpoints

| Method | Path | Description | |---|---|---| | GET | /authorize | Redirects the user to Google OAuth, encoding client's redirect_uri and state into Google's state param. | | GET | /callback | Google redirects back here; the router exchanges the code for tokens, stores them under a fresh one-time code, and redirects the client with that code. | | POST | /token | authorization_code: exchanges a previously-issued code for Google's tokens (one-time use). refresh_token: forwards Google's refresh grant and returns the new access token. |

Google OAuth setup

You'll need a Google OAuth 2.0 client from the Google Cloud Console with:

  • Authorized redirect URI: https://<your-domain>/oauth/callback (or wherever you mount the router)
  • Scopes: openid email profile (the router requests these automatically)

TokenStore contract

Storage for the short-lived code↔tokens mapping during the OAuth dance. The default is an in-memory Map that lazy-expires on get. For multi-instance deployments or anything requiring persistence, implement this interface:

const tokenStore = {
  // Return the stored entry, or undefined. May lazy-expire.
  async get(code) { /* returns { tokens, expiresAt } | undefined */ },

  // Store an entry.
  async set(code, entry) { /* entry = { tokens, expiresAt } */ },

  // Remove an entry (called after successful /token).
  async delete(code) {},
};

All methods may be sync or async — the router awaits both.

Example: Firestore-backed store

import { Firestore } from '@google-cloud/firestore';

function createFirestoreTokenStore({ collection = 'oauth-codes' } = {}) {
  const db = new Firestore();
  const col = db.collection(collection);

  return {
    async get(code) {
      const doc = await col.doc(code).get();
      if (!doc.exists) return undefined;
      const entry = doc.data();
      if (entry.expiresAt < Date.now()) {
        await col.doc(code).delete();
        return undefined;
      }
      return entry;
    },
    async set(code, entry) {
      await col.doc(code).set(entry);
    },
    async delete(code) {
      await col.doc(code).delete();
    },
  };
}

API-key-only mode

If you're not using OAuth or IAM, just don't mount the OAuth router and omit googleClientId, iamAudience, and allowedIamCallers:

const requireAuth = createRequireAuth({
  apiKey: process.env.API_KEY,
});

app.use('/api', requireAuth, yourRoutes);

IAM service-to-service mode

const requireAuth = createRequireAuth({
  apiKey: process.env.API_KEY,
  iamAudience: process.env.SERVICE_URL, // e.g. https://mcp-xxx.run.app
  allowedIamCallers: [
    '[email protected]',
  ],
});

What this package deliberately doesn't do

  • No custom JWT signing — Google's tokens are returned directly, by design.
  • No PKCE — server-to-server OAuth doesn't need it.
  • No custom session tokens — Google's access and refresh tokens are returned as-is; /token proxies the refresh grant to Google.
  • No session management — stateless by design.
  • No user info endpoint — clients get Google's ID token and can decode it themselves.
  • No consent screen — Google provides that.

Limitations

  • The default createMemoryTokenStore is per-process. In a multi-instance deployment (Cloud Run with --min-instances > 1, Kubernetes with replicas > 1, etc.), an OAuth dance that lands on a different instance than it started on will fail. Use a persistent TokenStore implementation (Firestore, Redis, etc.) for multi-instance setups.
  • Bearer token validation hits Google's tokeninfo endpoint on every request. No local caching. For high-throughput services, consider adding a cache layer around the middleware.

Development

git clone https://github.com/rakettitiede/mcp-oauth-proxy.git
cd mcp-oauth-proxy
npm install
npm test

40 tests across auth middleware, OAuth router, oauthMeta, and in-memory token store suites, using node:test — no external runner.

License

MIT © 2026 Rakettitiede Oy