An open-source control plane that makes AI agents enterprise-ready by enforcing user-scoped identity, policy, and audit trails on every model call.
The problem:
AI models and user data need each other... but can't safely connect.
- Users want AI to access their data (ChatGPT reading my calendar).
- Enterprises want to control who can use AI models (only doctors can use medical models, only employees can send sensitive prompts).
Both require cryptographic proof of user identity tied to every AI request... but AI platforms authenticate users on their side while your backend has no verified identity to enforce policies, filter data, or log actions.
Gatewaystack solves this.
Drop it between AI clients (ChatGPT, Claude, MCP) and your backend. It validates OAuth tokens, enforces scopes, and injects verified identity—so you can safely answer the two questions that matter most:
- Who did what, with which data, via which model?
- Was it authorized, bounded, and logged under policy?
Every AI request flows through six governance checkpoints:
Identified → Transformed → Validated → Constrained → Routed → Audited
Modern AI apps are really three-party systems:
👤 The User — a real human with identity, roles, and permissions
🤖 The LLM — a model acting on their behalf (ChatGPT, Claude)
🔒 Your Backend — the trusted data and tools the model needs to access
These three parties all influence each other, but they don’t share a common, cryptographically verified identity layer.
The gap: The LLM knows who the user is (they logged into ChatGPT). Your backend doesn't. So it can't:
- Filter data per-user ("show me my calendar" → returns everyone's calendar ❌)
- Enforce per-user policies ("only doctors use medical models" → anyone can ❌)
- Audit by user ("who made this query?" → can't answer ❌)
Without a unifying identity layer, you get:
- ❌ Shared API keys (everyone sees everything, or no one sees anything)
- ❌ No enforcement ("who can use which models for what")
- ❌ No audit trail (can't prove compliance)
- ❌ Enterprises block AI access entirely (too risky)
This instability across user ↔ LLM ↔ backend is what Gatewaystack calls the Three-Party Problem.
It shows up in two directions:
"How do I let ChatGPT read my calendar without exposing everyone's calendar?"
Without Gatewaystack:
app.get('/calendar', async (_req, res) => {
const events = await getAllEvents(); // ❌ Everyone sees everything
res.json(events);
});With Gatewaystack:
app.get('/calendar', async (req, res) => {
const userId = req.headers['x-user-id']; // ✅ Verified by gateway
const events = await getUserEvents(userId);
res.json(events);
});The gateway validates the OAuth token, extracts the user identity, and injects X-User-Id — so your backend can safely filter data per-user.
"How do I ensure only licensed doctors use medical models, only analysts access financial data, and contractors can't send sensitive prompts?"
Without Gatewaystack:
app.post('/chat', async (req, res) => {
const { model, prompt } = req.body;
const response = await openai.chat.completions.create({
model, // ❌ Anyone can use gpt-4-medical
messages: [{ role: 'user', content: prompt }]
});
res.json(response);
});With Gatewaystack:
app.post('/chat', async (req, res) => {
const userId = req.headers['x-user-id'];
const userRole = req.headers['x-user-role']; // "doctor", "analyst", etc.
const userScopes = req.headers['x-user-scopes']?.split(' ') || [];
// Gateway already enforced: only doctors with medical:write can reach here
const response = await openai.chat.completions.create({
model: req.body.model,
messages: [{ role: 'user', content: req.body.prompt }],
user: userId // OpenAI audit trail
});
res.json(response);
});Gateway policy:
{
"gpt-4-medical": {
"requiredRoles": ["doctor", "physician_assistant"],
"requiredScopes": ["medical:write"]
}
}The gateway enforces role + scope checks before forwarding to your backend. If a nurse tries to use gpt-4-medical, they get 403 Forbidden.
Without solving the Three-Party Problem, you can't:
- Filter data per-user (Direction 1: everyone sees everything)
- Enforce "who can use which models" (Direction 2: no role-based access)
- Audit "who did what" (compliance impossible)
- Rate limit per-user (shared quotas get exhausted)
- Attribute costs (can't charge back to teams/users)
Gatewaystack solves both by binding cryptographic user identity to every AI request:
- ✅ OAuth login per user (RS256 JWT, cryptographic identity proof)
- ✅ Per-user / per-tenant data isolation by default
- ✅ Deny-by-default authorization (scopes per tool/model/role)
- ✅ Immutable audit trails (who, what, when, which model)
- ✅ Rate limits & spend caps (per user/team/org)
- ✅ Drop-in between AI clients and your backend (no SDK changes)
Gatewaystack is composed of modular packages that can run standalone or as a cohesive six-layer pipeline for complete AI governance.
| Path | Highlights |
|---|---|
apps/gateway-server |
Express reference server wiring all six governance layers, /protected/* samples, demo/test routes, and a ready-to-build Docker image. |
apps/admin-ui |
Minimal Vite/React dashboard that polls /health so you can monitor the gateway while iterating. |
packages/ |
Publishable packages for each layer plus helpers like compat, request-context, and integrations. |
demos/ |
Working MCP issuer + ChatGPT Apps SDK connectors that mint demo JWTs and exercise the gateway. |
tools/ |
Supporting utilities (echo server, mock tool backend, Cloud Run deploy helper, smoke harnesses). |
tests/ |
Vitest entry points and placeholder smoke tests for parity. |
docs/ |
Auth0 walkthroughs, conformance output, endpoint references, troubleshooting notes. |
-
@gatewaystack/identifiabl– Trust & Identity Binding. Express middleware that verifies RS256 JWTs (viajose), enforcesiss/aud, and attaches a canonicalreq.userfor downstream policy, routing, and audit. -
@gatewaystack/transformabl– Content Safety & Normalization. Request/response normalization hook. Currently a no-op, reserved for redaction/classification/safety transforms that run before authorization and routing. -
@gatewaystack/validatabl-core/@gatewaystack/validatabl– Authorization & Policy Enforcement. Scope utilities plus Express helpers (e.g.requireScope) and the Protected Resource Metadata.well-knownroute. Enforces deny-by-default, fine-grained access to tools and models. -
@gatewaystack/limitabl– Spend Controls & Resource Governance. Rate limiting keyed onsub/org_id(falling back to IP) to prevent runaway agents, abuse, and unbounded cost at the user/tenant level. -
@gatewaystack/proxyabl– Execution Control & Identity-Aware Routing. Tool gateway + proxy router that:- Serves PRM/OIDC metadata for OAuth 2.1 / MCP / Apps SDK
- Enforces scope-to-tool mappings (
TOOL_SCOPES_JSON) - Injects verified identity into headers/queries (e.g.
X-User-Id) - Hosts Auth0 integration points for log streams / DCR
-
@gatewaystack/explicabl/@gatewaystack/explicabl-core– Runtime Audit & Conformance. Read-only on the RequestContext, write-only to external systems. Health endpoints, log/webhook handlers, and the conformance reporter (saveReport.ts) that emit correlated events to SIEM/observability stacks without blocking the critical path. -
@gatewaystack/compat– Interop & Parity Harness. Legacy/test router that mirrors the original/echoshape for quick interoperability and regression testing. -
@gatewaystack/request-context,@gatewaystack/integrations, and additional*-corefolders – Shared types, RequestContext helpers, and staging areas for upcoming connectors as the Agentic Control Plane expands.
apps/gateway-server/src/app.ts composes the six governance layers in order:
- Public Protected Resource Metadata via
protectedResourceRouter. /protected/*pipeline →identifiabl(JWT) →limitabl→transformabl.- Sample handlers (
GET /protected/ping,POST /protected/echo) withrequireScope("tool:write"). toolGatewayRouterfor PRM/OIDC well-knowns, MCP/Apps JSON-RPC,/proxy, and the Auth0 log webhook.explicablRouterfor/health,/health/auth0, and/webhooks/auth0.
Toggles worth noting:
DEMO_MODE=trueswaps inOAUTH_*_DEMOoverrides so demos can mint JWTs locally.ENABLE_TEST_ROUTES=true+TOOL_SCOPE_ALLOWLIST_JSONexpose/__test__/echofor conformance runs.RATE_LIMIT_WINDOW_MS/RATE_LIMIT_MAXtune limitabl without editing TypeScript..env.exampleplusapps/gateway-server/.env.exampleenumerate every knob.
npm run dev:admin launches a tiny Vite/React panel (apps/admin-ui/src/App.tsx) that fetches /health and renders the JSON so you can keep gateway status visible while iterating.
Every request flows through the same six-layer composable pipeline:
| Layer | Status | Purpose |
|---|---|---|
| identifiabl | ✅ | Foundational Trust & Identity Binding — verifies RS256 JWTs, pins issuer/audience, and establishes the canonical subject for downstream authorization and audit. |
| transformabl | ⚪ | Content Safety Preprocessing & Risk Mitigation — normalizes, redacts, or classifies inputs/outputs before policy and routing are applied. |
| validatabl | ✅ | Authorization & Policy Enforcement — deny-by-default, scope-driven access to protected resources, tools, and models. |
| limitabl | ✅ | Rate & Spend Governance — throttles per user/tenant to prevent runaway agents and unbounded cost. |
| proxyabl | 🧩 | Execution Control & Identity-Aware Routing — routes calls to the right tool/model backend, injects verified identity, and presents OAuth/PRM metadata. |
| explicabl | ⚪ | Accountability & Runtime Audit — emits immutable, correlated events to your SIEM/observability stack and exposes health/conformance endpoints. |
Drop it between AI clients and your backend — no SDK modification needed. Handles RS256 JWTs, audience/issuer checks, per-tool scopes, role-based policies, and optional DCR client promotion.
Verified against Apps SDK / MCP OAuth 2.1 + RS256 flow.
- ✅ JWT validation (iss/aud/sub/exp/nbf)
- ✅ Scope allowlist / deny-by-default
- ✅ Expiry handling
- ✅ Health & protected resource endpoints
▶️ Quickstart (10 minutes)- 🔐 Identity Provider Setup
- 🤝 Connect to ChatGPT / Claude (MCP)
- 🩺 Health & protected-resource metadata
- 🛡️ Production Checklist
- 🆘 Troubleshooting

A platform with 10,000+ doctors needs to ensure every AI-assisted diagnosis is tied to the licensed physician who requested it, with full audit trails for HIPAA and internal review.
Before Gatewaystack: All AI calls run through a shared OpenAI key — impossible to prove which physician made which request.
With Gatewaystack:
identifiablbinds every request to a verified physician (user_id,org_id= clinic/hospital)validatablenforcesrole:physicianandscope:diagnosis:writeper toolexplicablemits immutable audit logs with the physician's identity on every model call
Result: User-bound, tenant-aware, fully auditable AI diagnostics.
A global company rolls out an internal copilot that can search Confluence, Jira, Google Drive, and internal APIs. Employees authenticate with SSO (Okta / Entra / Auth0), but the copilot calls the LLM with a shared API key.
Before Gatewaystack: Security teams can't enforce "only finance analysts can run this tool" or audit which employee triggered which action.
With Gatewaystack:
identifiablbinds the copilot session to the employee's SSO identity (subfrom Okta)validatablenforces per-role tool access ("legal can see these repos, not those")limitablapplies per-user rate limits and spend capsexplicablproduces identity-level audit trails for every copilot interaction
Result: Full identity-level governance without changing the copilot's business logic.
A SaaS platform offers AI features across free, pro, and enterprise tiers. Today, all AI usage runs through a single OpenAI key per environment — making it impossible to answer "how much did Org X spend?" or "which users hit quota?"
Before Gatewaystack: One big shared key. No tenant-level attribution. Cost overruns are invisible until the bill arrives.
With Gatewaystack:
identifiablattachesuser_idandorg_idto every requestvalidatablenforces tier-based feature access (plan:free,plan:pro,feature:advanced-rag)limitablenforces per-tenant quotas and budgetsexplicablproduces per-tenant usage reports
Result: Per-tenant accountability without changing app logic.
Identity Providers (Auth0, Okta, Cognito, Entra ID)
Handle login and token minting, but stop at the edge of your app. They don't understand model calls, tools, or which provider a request is going to — and they don't enforce user identity inside the AI gateway.
API Gateways and Service Meshes (Kong, Apigee, AWS API Gateway, Istio, Envoy)
Great at path/method-level auth and rate limiting, but they treat LLMs like any other HTTP backend. You can build AI governance on top of them (Kong plugins, Istio policies, Lambda authorizers), but it requires 100+ hours of custom development to replicate what Gatewaystack provides out-of-the-box: user-scoped identity normalization, per-tool scope enforcement, pre-flight cost checks, Apps SDK / MCP compliance, and AI-specific audit trails.
Cloud AI Gateways (Cloudflare AI Gateway, Azure OpenAI + API Management, Vertex AI, Bedrock Guardrails)
Focus on provider routing, quota, and safety filters at the tenant or API key level. User identity is usually out-of-band or left to the application.
Hand-Rolled Middleware
Many teams glue together JWT validation, headers, and logging inside their app or a thin Node/Go proxy. It works... until you need to support multiple agents, providers, tenants, and audit/regulatory requirements.
Gatewaystack is different:
- User-scoped by default — every request is tied to a verified user, not a shared key
- Model-aware — understands tools, scopes, and provider semantics (Apps SDK, MCP, OpenAI, Anthropic)
- Composable governance — each layer (identity, policy, limits, routing, audit) can run standalone or as part of the full control plane
- Built for agents — prevents runaway loops, enforces per-workflow budgets, and tracks multi-step traces
Example: Kong + OpenAI
To get user-scoped AI governance with Kong, you'd need to:
- Install
jwtplugin (validate tokens) - Install
request-transformerplugin (inject headers) - Write custom Lua script to normalize identity claims
- Write custom Lua script for scope-to-tool mapping
- Write custom plugin for pre-flight cost estimation
- Build separate service for Protected Resource Metadata
- Configure DCR flow manually
- Build custom audit log forwarding
Estimate: 100-200 hours of development + ongoing maintenance.
With Gatewaystack: Configure .env file, deploy, done. (2 hours)
You can still run Gatewaystack alongside traditional API gateways — it's the user-scoped identity and governance slice of your AI stack.
| Feature | Kong/Apigee/AWS API Gateway | Gatewaystack |
|---|---|---|
| JWT validation | ✅ Built-in | ✅ Built-in |
| Rate limiting | ✅ Built-in | ✅ Built-in |
| Path/method routing | ✅ Built-in | ✅ Built-in |
| User identity normalization | ❌ Manual (custom plugin) | ✅ Built-in |
| Per-tool scope enforcement | ❌ Manual (custom policy) | ✅ Built-in |
| Apps SDK / MCP compliance | ❌ Manual (PRM endpoint) | ✅ Built-in |
| Pre-flight cost checks | ❌ Manual (custom plugin) | ✅ Roadmap |
| Model-specific policies | ❌ Manual (custom logic) | ✅ Built-in |
| AI audit trails | ❌ Manual (log forwarding) | ✅ Built-in |
| Setup time | 100+ hours (custom dev) | 2 hours (config) |
| Command | Components | What it proves |
|---|---|---|
npm run demo:mcp |
Runs the MCP issuer (demos/mcp-server on :5051), the gateway in demo mode (:8080), and the MCP JSON-RPC surface. |
401→PRM→token handshake, /protected/* isolation, per-tool scopes, /proxy identity injection. |
npm run demo:apps |
Adds the ChatGPT Apps SDK-style connector on :5052 (demos/chatgpt-connector) while reusing the issuer and gateway. |
Shows the same JWT/scope enforcement works for Apps SDK connectors. |
Both demos share the local issuer + JWKS hosted by demos/mcp-server. Mint reader/writer tokens with:
curl -s -X POST http://localhost:5051/mint \
-H 'content-type: application/json' \
--data '{"scope":"tool:read tool:write","sub":"demo-user"}'See demos/mcp-server/README.md and demos/chatgpt-connector/README.md for the curl walkthroughs and troubleshooting tips.
Set these in apps/gateway-server/.env to enable local demos without Auth0:
DEMO_MODE=true
OAUTH_ISSUER_DEMO=http://localhost:5051/
OAUTH_AUDIENCE_DEMO=https://gateway.local/api
OAUTH_JWKS_URI_DEMO=http://localhost:5051/.well-known/jwks.json
OAUTH_SCOPES_DEMO=tool:read tool:write
ENABLE_TEST_ROUTES=trueWith demo mode on you can run npm run demo:*, call /protected/ping, exercise /proxy/*, and run MCP JSON-RPC flows without touching Auth0.
Choose your path:
Quickstart (10 min) → Run local demos without Auth0
Production Setup (30 min) → Connect to Auth0/Okta + ChatGPT/Claude
Deployment → Cloud Run, Docker, Kubernetes
git clone <your-repo-url> gatewaystack
cd gatewaystack
npm install
npm run dev- ✅ RS256 JWT Verification via JWKS (issuer, audience, exp, nbf, sub checks)
- ✅ Per-tool scope enforcement (deny-by-default; 401/403 outcomes)
- ✅ Protected resource endpoint for smoke tests
- ✅ Verified Identity Injection — The gateway injects a cryptographically verified user ID (
X-User-Id) into every proxied request, so downstream services can enforce per-user/per-tenant filtering without ever handling JWTs or seeing upstream API keys. This turns "shared key chaos" into "every call is attributable." - ✅ Rate limiting (user/tenant aware)
- ✅ Health endpoints (
/health,/health/auth0) - ✅ (Optional) DCR webhook to auto-promote new OAuth clients from Auth0 logs
- ✅ Echo test servers to validate proxy/header injection
- Node.js 20+ (or 22)
- npm 10+ (or pnpm 9)
- An Auth0 tenant (or equivalent OIDC provider issuing RS256 access tokens)
- (Optional) Google Cloud SDK for Cloud Run deploys
🔓 Works with Any Identity Provider
Gatewaystack is IdP-agnostic. It works with any OAuth 2.1 / OIDC provider that issues RS256 JWTs: Auth0, Okta, Entra ID (Azure AD), Keycloak, Google OAuth, or custom implementations.
Auth0 examples are provided for quick setup—the same patterns apply to all providers. See Identity Provider Requirements for details.
Requirements for any IdP: See Identity Provider Requirements below.
Gatewaystack requires an OAuth 2.1 / OIDC provider that issues RS256 JWTs.
| Requirement | Why | Example Providers |
|---|---|---|
| RS256 JWT signing | Gateway validates signatures via JWKS | Auth0, Okta, Entra ID, Keycloak |
| Public JWKS endpoint | Gateway fetches public keys at runtime | https://tenant.auth0.com/.well-known/jwks.json |
| Standard claims | Gateway reads: iss, aud, sub, exp, nbf |
All OAuth 2.1 providers |
| Custom scopes | For authorization (tool:read, tool:write) |
Configure in your IdP's API settings |
| OAuth 2.1 + PKCE | Required for ChatGPT Apps SDK / MCP flows | Auth0, Okta, Entra ID |
| Provider | Status | Notes |
|---|---|---|
| Auth0 | ✅ Fully tested | See setup guide above |
| Okta | ✅ Compatible | Coming soon: docs/okta-setup.md |
| Entra ID | ✅ Compatible | Coming soon: docs/entra-id-setup.md |
| Keycloak | ✅ Compatible | Community guide available |
| Google OAuth | ✅ Compatible | Standard RS256 setup |
| Custom | ✅ Any RS256 provider | Must meet requirements above |
Need help with your IdP? Open a GitHub Discussion.
- Name:
Gateway API - Identifier (Audience):
https://gateway.local/api(any HTTPS URI string) - Signing algorithm:
RS256 - Enable RBAC and Add Permissions in the Access Token
tool:readtool:write
Create a Regular Web App or SPA to obtain tokens during development.
Your issuer will be:
https://<TENANT>.region.auth0.com/
Create a Machine-to-Machine application with scopes:
read:clients update:clients read:connections update:connections read:logs
- From your app’s Auth0 Test tab or via a quick PKCE flow.
- Ensure the token’s audience matches your API identifier and includes the scopes you want to test (e.g.,
tool:read).
⚠️ Auth0-Specific Feature
This section only applies if you're using Auth0 + ChatGPT Apps SDK.
Okta and Entra ID have equivalent mechanisms (Hooks, Custom Extensions).
Only required if you are using ChatGPT Apps SDK.
If you’re only using the gateway with your own OAuth client or MCP, you can skip this section.
If you are using this gateway with ChatGPT Apps SDK, you must add a Post-Login Action so that:
- The access token audience (
aud) is forced to your API Identifier. - The token is issued as an RS256 JWS (3-part JWT), not an encrypted JWE or opaque token.
- All scopes required by your tools are present on the token.
High-level steps:
- Go to Actions → Library → + Create Action and name it
auto-assign-openai-connector-role. - Choose Trigger: Login / Post Login.
- Paste the code from
docs/auth0/chatgpt-post-login-action.js. - Add secrets:
API_AUDIENCE→ your Auth0 API Identifier (e.g.,https://inner.app/api)CONNECTOR_ROLE_ID→ (optional) Role ID to auto-assign to connector users
- Click Deploy.
- Attach it to the flow: Actions → Flows → Login (Post Login), drag the Action between Start and Complete, then click Apply.
For a full walkthrough, screenshots, and troubleshooting checklist, see docs/auth0/chatgpt-post-login-action.md.
Core OAuth config uses the OAUTH_* prefix:
OAUTH_ISSUER,OAUTH_AUDIENCE,OAUTH_JWKS_URI,OAUTH_ENFORCE_ALG
Auth0-specific features use AUTH0_*:
AUTH0_DOMAIN,AUTH0_MGMT_CLIENT_ID,AUTH0_MGMT_CLIENT_SECRET(for DCR webhook and/health/auth0)
Demo mode uses OAUTH_*_DEMO variants:
OAUTH_ISSUER_DEMO,OAUTH_AUDIENCE_DEMO, etc.
See apps/gateway-server/.env.example for the full reference, including:
- Rate limiting (
RATE_LIMIT_*) - Proxy config (
PROXY_TARGET,PROXY_INJECT_*) - Tool scopes (
TOOL_SCOPES_JSON) - DCR webhook (
MGMT_*,LOG_WEBHOOK_SECRET)
These help prove proxy + header injection:
# Echo server that returns headers, query, and body
npm run -w @gatewaystack/echo-server dev
# default: http://localhost:3333These tests are your governance smoke test.
The echo server simply returns the headers, query, and body it receives. Combined with the /proxy routes in proxyabl, this lets you prove that the authenticated subject has been injected as a verified, canonical user identifier (for example X-User-Id) — so downstream services can enforce per-user/per-tenant data filtering without ever seeing upstream API keys.
Need a fake tool backend instead of an echo? Run tsx tools/mock-tools-server/index.ts to spin up JSON handlers (on :9090 by default) that mimic generateDreamSummary, chatWithEmbeddingsv3, etc. for end-to-end proxy tests.
From the repo root:
# Or individually:
npm run dev:server # apps/gateway-server
npm run dev:admin # apps/admin-uinpm run devstarts the gateway and Admin UI together.npm run dev:serverstarts the Express gateway only.npm run dev:adminstarts the Admin UI, which is primarily used to visualize/healthand related outputs.
# Health (served by healthRoutes at /health)
curl -s http://localhost:8080/health | jq .
# Auth0 checks (JWKS reachability, mgmt token if set)
curl -s http://localhost:8080/health/auth0 | jq .
# Protected resource metadata (expect 401 without token + WWW-Authenticate)
curl -i http://localhost:8080/.well-known/oauth-protected-resourceWhen called without a token, you should see a 401 with a WWW-Authenticate header that points ChatGPT / MCP to this URL as the Protected Resource Metadata (PRM) endpoint.
When called with a valid token, you should see JSON similar to:
{
"authorization_servers": ["https://<TENANT>.auth0.com/"],
"scopes_supported": [
"openid",
"email",
"profile",
"tool:read",
"tool:write"
],
"resource": "https://gateway.local/api"
}Where:
authorization_serverstells the client which issuer(s) can mint access tokens.scopes_supportedis derived from your configured tool scopes (TOOL_SCOPES→REQUIRED_SCOPESin the gateway).resourceis your API Identifier / audience (AUTH_AUDIENCE/OAUTH_AUDIENCE).
When you add a new tool scope in TOOL_SCOPES, the gateway automatically:
- Updates
REQUIRED_SCOPES - Exposes it in
scopes_supported - Includes it in the
scope=parameter of theWWW-Authenticateheader - Ensures the client grant includes the new scope (if using the Auth0 DCR helper)
/health→{ ok: true, ... }/health/auth0→ issuer/audience OK, JWKS reachable- Protected resource → 401 w/o token, 200 w/ token
Run the full test suite:
npm testThis runs Vitest plus the conformance report writer that updates docs/conformance.json.
For detailed testing workflows, see:
docs/testing.md—/__test__/echoroutes, scope checks, proxy validationCONTRIBUTING.md— Pre-PR checklist
The DCR helper is implemented inside the Explicabl router and exposed at:
POST /webhooks/auth0/logsWhen to use this: If you want new ChatGPT connectors to auto-register in Auth0 and immediately gain access to your API with the correct grant types, Google connection, and scopes, enable the DCR webhook.
This endpoint is typically wired as an Auth0 Log Stream target that listens for /oidc/register events (Dynamic Client Registration) and then:
- Promotes the new client to a public
regular_webapp with PKCE. - Enables the
google-oauth2connection for that client. - Ensures a client grant exists for your API (
OAUTH_AUDIENCE) with allREQUIRED_SCOPES.
For a detailed walkthrough and environment variable reference (MGMT_DOMAIN, MGMT_CLIENT_ID, MGMT_CLIENT_SECRET, LOG_WEBHOOK_SECRET, GOOGLE_CONNECTION_NAME, OAUTH_AUDIENCE), see docs/auth0/dcr-log-webhook.md.
If you want the same DCR “promotion” flow as the original:
Auth0 → Monitoring → Streams → Webhook
POST https://<your-gateway-domain>/webhooks/auth0/logs
X-Webhook-Secret: <WEBHOOK_SHARED_SECRET>
In your .env, set the Management API client creds & WEBHOOK_SHARED_SECRET.
Trigger a client registration event (or simulate via Auth0 log events).
Check /health/auth0 — you should see a recent webhook last-seen timestamp and successful management calls in your logs.
Using Auth0 + ChatGPT? For Auth0-specific issues (Post-Login Actions, JWE vs JWS tokens, scopes not showing up, etc.), see
docs/auth0/chatgpt-post-login-action.md→ “Troubleshooting checklist”.
401 with valid token
- Check
OAUTH_AUDIENCEmatches the tokenaud - Check
OAUTH_ISSUERmatches tokenissand the JWKS URL resolves - Ensure RS256 is used; HS256 will be rejected when
OAUTH_ENFORCE_ALG=RS256
403 on write
- Your token likely lacks
tool:write; confirm “Add Permissions in the Access Token” is enabled on the API
Proxy not injecting user
- Verify
PROXY_TARGETis reachable - Confirm
PROXY_INJECT_HEADER/PROXY_INJECT_QUERYare set and your route is going through the proxy handler
Rate limit never triggers
- Lower
RATE_LIMIT_MAXand ensure identifier (user/tenant) is parsed from the token’ssub/org_id
- ✅ RS256 enforced; JWKS timeout & caching tuned
- ✅ Strict CORS (exact origins)
- ✅ Deny-by-default policies per route/tool
- ✅ Rate-limit per user/tenant with sane ceilings
- ✅ Logs redact PII; audit fields:
{sub, tool, path, decision, latency} - ✅ Health probes hooked into your orchestrator
- ✅ (Optional) DCR webhook secret rotated; Mgmt API scopes minimal
Why Cloud Run:
- ✅ Serverless (no infrastructure management)
- ✅ Auto-scaling (0 to 1000+ instances)
- ✅ FedRAMP Moderate authorized (government/enterprise ready)
- ✅ Built-in HTTPS, load balancing, health checks
- ✅ Deploy in 3 commands
Quick Deploy:
# Use the included deploy script
./tools/deploy/cloud-run.sh apps/gateway-server
# Or manually:
gcloud builds submit --tag gcr.io/YOUR-PROJECT/gatewaystack
gcloud run deploy gatewaystack \
--image gcr.io/YOUR-PROJECT/gatewaystack \
--set-env-vars="OAUTH_ISSUER=https://your-tenant.auth0.com/"Cost: ~$5-50/month depending on usage (generous free tier)
See: docs/deployment/cloud-run.md for full walkthrough
Pre-built images:
docker pull ghcr.io/davidcrowe/gatewaystack:latestBuild yourself:
# Gateway server
docker build -f apps/gateway-server/Dockerfile -t gatewaystack .
# Admin UI
docker build -f apps/admin-ui/Dockerfile -t gatewaystack-admin .Run locally:
docker run -p 8080:8080 \
-e OAUTH_ISSUER=https://your-tenant.auth0.com/ \
-e OAUTH_AUDIENCE=https://gateway.local/api \
gatewaystackSee: docs/deployment/docker.md for Docker Compose, Kubernetes manifests, etc.
| Platform | Difficulty | Guide |
|---|---|---|
| AWS ECS/Fargate | Medium | docs/deployment/aws.md |
| Azure Container Instances | Medium | docs/deployment/azure.md |
| Fly.io | Easy | docs/deployment/fly.md |
| Railway | Easy | docs/deployment/railway.md |
| Kubernetes | Hard | docs/deployment/kubernetes.md |
Air-gapped / on-prem deployments: Fully supported (Docker + self-hosted)
.github/workflows/conformance.yml runs npm test and updates docs/conformance.json on every push to main.
To add deployment:
# Add to conformance.yml after tests pass
- name: Deploy to Cloud Run
if: github.ref == 'refs/heads/main'
run: ./tools/deploy/cloud-run.sh apps/gateway-server
env:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}If you’re using this gateway as an MCP server (e.g. Claude Desktop, Cursor, etc.), no code changes are required. The gateway is auth-initiator agnostic — it simply validates RS256 tokens and enforces scopes, regardless of who started the OAuth flow.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp", resource_metadata="https://<YOUR_GATEWAY>/.well-known/oauth-protected-resource"
Content-Type: application/json
{"error":"unauthorized","error_description":"Access token required"}The PRM URL tells the MCP client where to discover OAuth configuration for this resource.
Serve this JSON at /.well-known/oauth-protected-resource:
{
"resource": "https://<YOUR_GATEWAY>/mcp",
"authorization_servers": ["https://<YOUR_AUTH_SERVER>/"],
"scopes_supported": ["tool:read", "tool:write"]
}| Field | Description |
|---|---|
resource |
Identifier for this gateway’s MCP surface |
authorization_servers |
OAuth / OIDC issuer (e.g. Auth0, Okta) |
scopes_supported |
Scopes mapped to your route allowlist |
| Scope | Routes |
|---|---|
tool:read |
GET /v1/tools/* |
tool:write |
POST /v1/tools/* |
Requests with a valid token but missing scope will receive 403 Forbidden.
Register your client with the IdP and allow its redirect URI(s):
- Claude Desktop/Web: documented callback (e.g.
https://claude.ai/api/mcp/auth_callback) - Cursor IDE: their documented OAuth callback
If your IdP supports Dynamic Client Registration (DCR), you can enable it instead of pre-registering.
# No token → 401 + WWW-Authenticate header
curl -i https://<YOUR_GATEWAY>/protected
# Valid token with tool:read → 200
curl -i -H "Authorization: Bearer $TOKEN" https://<YOUR_GATEWAY>/protected
# Valid token, insufficient scope → 403
curl -i -H "Authorization: Bearer $TOKEN" https://<YOUR_GATEWAY>/writer-only- Always validate
iss, pinaud, enforcealg = RS256, and honorexp/nbf. - Keep tokens short-lived; rotate/revoke via your IdP.
- Use gateway identity injection (e.g.
X-User-Id) to pass user context downstream — never expose upstream API keys to the LLM client.
- 📖 Read the Architecture Guide
- 🧪 Run the conformance tests
- 🔧 Explore the API Reference
- 💬 Ask questions in GitHub Discussions
- 🏢 Review the Enterprise Features
- 🔒 See Compliance Documentation (SOC 2, HIPAA, FedRAMP)
- 📞 Contact us for deployment support
- 🤝 Read CONTRIBUTING.md
- 🐛 Report bugs via GitHub Issues
- ⭐ Star the repo if this helps you!
MIT License - see LICENSE for details.
Built by reducibl applied AI studio