From 13c81096956f71a2780b4cb7d60d64a01daa43a4 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 19 May 2026 13:33:43 +0800 Subject: [PATCH 1/3] docs(readme): surface MCP-grade security highlights - Add an MCP / multi-resource security callout at the end of "Why AuthGate?" highlighting the JWT type-claim refresh-as-access guard, mandatory device-code resource confirmation, and RFC compliance baseline - Extend the "What AuthGate Protects" checklist with refresh-token confusion, device-flow phishing, and cross-resource replay coverage - Add an "MCP & multi-resource hardening" subsection under Security with the JWT_AUDIENCE operational constraint and the full RFC stack (8628 / 6749 / 7636 / 8707 / 8414 / 7591 / 7009 / 7662) Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 929d03b4..723ee0ed 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ - [🔒 Security](#-security) - [Production Checklist](#production-checklist) - [What AuthGate Protects](#what-authgate-protects) + - [MCP \& multi-resource hardening](#mcp--multi-resource-hardening) - [📊 Performance](#-performance) - [Benchmarks (Reference)](#benchmarks-reference) - [Scalability](#scalability) @@ -137,6 +138,12 @@ AuthGate also serves as a lightweight **centralised identity gateway** for inter - Revoke any user's authorisation instantly. - Respond to audits and security requests without digging through disparate system logs. +> ⚠️ **Security highlights for MCP / multi-resource deployments** +> +> - **Refresh-as-access confusion blocked at the protocol layer** — every JWT carries a `type` claim (`access` vs `refresh`) that AuthGate enforces on both validation and `/oauth/tokeninfo`, so a refresh token cannot be replayed as an access token by a resource server that only verifies signature/`iss`/`exp`/`aud`. See [MCP Integration Guide → Audience binding (RFC 8707)](docs/MCP.md#audience-binding-via-resource-indicators-rfc-8707) and the [JWT Verification Guide](docs/JWT_VERIFICATION.md). +> - **Device-code phishing surface eliminated** — resource-bound device codes always route through an explicit confirmation screen displaying the client and requested resource(s), regardless of whether the user landed via `verification_uri_complete` or typed the user code by hand (documented in the same MCP guide). +> - **MCP-ready out of the box** — [RFC 8414][rfc8414] AS metadata at `/.well-known/oauth-authorization-server`, [RFC 7591][rfc7591] Dynamic Client Registration at `/oauth/register`, [RFC 8707][rfc8707] per-request audience binding, and PKCE S256 (rejecting `plain`) for all public clients. See the [MCP Integration Guide](docs/MCP.md). + --- ## ✨ Key Features @@ -565,6 +572,25 @@ docker run -d \ - ✅ Token tampering (JWT signature verification) - ✅ Brute force attacks (rate limiting) - ✅ Session hijacking (encrypted cookies, CSRF protection) +- ✅ Refresh-token-as-access-token confusion (mandatory `type` claim on every JWT) +- ✅ Device-flow phishing (forced confirmation page for resource-bound device codes) +- ✅ Cross-resource token replay (per-request [RFC 8707][rfc8707] audience binding) + +### MCP & multi-resource hardening + +**Refresh tokens cannot masquerade as access tokens.** Refresh JWTs are signed with the same key as access JWTs, so a resource server that only verifies signature/`iss`/`exp`/`aud` would silently accept a refresh token as a valid access token whenever `JWT_AUDIENCE` happens to match its resource identifier. AuthGate hard-codes the distinction: every issued JWT carries a `type` claim (`access` or `refresh`), `ValidateToken` rejects any token whose `type` is not `"access"`, and `/oauth/tokeninfo` strips `aud` from refresh-token introspection so it cannot be mistaken for an access-token response. **Operational constraint: `JWT_AUDIENCE` MUST be either unset or set to an AS-only identifier — never a resource server's `aud`.** See [docs/MCP.md → Configuration checklist](docs/MCP.md#configuration-checklist) and [docs/CONFIGURATION.md](docs/CONFIGURATION.md#environment-variables). + +**Device-code resource confirmation is mandatory.** When a device-code request is bound to a resource ([RFC 8707][rfc8707]), AuthGate routes the user through an explicit confirmation screen that displays the requesting client and the resource(s) being authorized **before** the device code is marked authorized — regardless of whether the user landed via `verification_uri_complete` or typed the user code into the verification form by hand. This prevents silent resource binding to an attacker-controlled MCP server. See [docs/MCP.md → Audience binding](docs/MCP.md#audience-binding-via-resource-indicators-rfc-8707). + +**Standards-compliance baseline.** AuthGate implements the OAuth 2.0 / OIDC stack required for MCP and multi-resource deployments. For the full integrator walkthrough, see [docs/MCP.md](docs/MCP.md). + +- [RFC 8628][rfc8628] — Device Authorization Grant +- [RFC 6749][rfc6749] + [RFC 7636][rfc7636] — Authorization Code Flow with PKCE S256 (`plain` rejected) +- [RFC 8707][rfc8707] — Resource Indicators, bound at issuance and verified per refresh +- [RFC 8414][rfc8414] — Authorization Server Metadata at `/.well-known/oauth-authorization-server` +- [RFC 7591][rfc7591] — Dynamic Client Registration at `/oauth/register` +- [RFC 7009][rfc7009] — Token Revocation +- [RFC 7662][rfc7662] — Token Introspection --- From 490034f6166e3d180b1db980e034ed11b646d26b Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 19 May 2026 13:47:42 +0800 Subject: [PATCH 2/3] docs(readme): correct type-claim scope and DCR opt-in note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Scope the `type`-claim guarantee to OAuth bearer tokens (access / refresh); OIDC ID tokens are JWTs without a `type` claim and follow a separate code path in `internal/token/idtoken.go` - Correct the `/oauth/tokeninfo` description — it returns 401 for refresh tokens via `ValidateToken` rather than stripping `aud`; note that `/oauth/introspect` is the RFC 7662 endpoint that omits `aud` on refresh tokens - Qualify the Dynamic Client Registration mention with the `ENABLE_DYNAMIC_CLIENT_REGISTRATION=true` opt-in (default is false in `config.go:586`) Addresses Copilot review on #191. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 723ee0ed..cb1a41a3 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,9 @@ AuthGate also serves as a lightweight **centralised identity gateway** for inter > ⚠️ **Security highlights for MCP / multi-resource deployments** > -> - **Refresh-as-access confusion blocked at the protocol layer** — every JWT carries a `type` claim (`access` vs `refresh`) that AuthGate enforces on both validation and `/oauth/tokeninfo`, so a refresh token cannot be replayed as an access token by a resource server that only verifies signature/`iss`/`exp`/`aud`. See [MCP Integration Guide → Audience binding (RFC 8707)](docs/MCP.md#audience-binding-via-resource-indicators-rfc-8707) and the [JWT Verification Guide](docs/JWT_VERIFICATION.md). +> - **Refresh-as-access confusion blocked at the protocol layer** — every OAuth bearer token (access and refresh) carries a `type` claim that AuthGate enforces at validation, so `/oauth/tokeninfo` returns 401 for refresh tokens and a refresh token cannot be replayed as an access token by a resource server that only verifies signature/`iss`/`exp`/`aud`. See [MCP Integration Guide → Audience binding (RFC 8707)](docs/MCP.md#audience-binding-via-resource-indicators-rfc-8707) and the [JWT Verification Guide](docs/JWT_VERIFICATION.md). > - **Device-code phishing surface eliminated** — resource-bound device codes always route through an explicit confirmation screen displaying the client and requested resource(s), regardless of whether the user landed via `verification_uri_complete` or typed the user code by hand (documented in the same MCP guide). -> - **MCP-ready out of the box** — [RFC 8414][rfc8414] AS metadata at `/.well-known/oauth-authorization-server`, [RFC 7591][rfc7591] Dynamic Client Registration at `/oauth/register`, [RFC 8707][rfc8707] per-request audience binding, and PKCE S256 (rejecting `plain`) for all public clients. See the [MCP Integration Guide](docs/MCP.md). +> - **MCP-ready out of the box** — [RFC 8414][rfc8414] AS metadata at `/.well-known/oauth-authorization-server`, [RFC 8707][rfc8707] per-request audience binding, and PKCE S256 (rejecting `plain`) for all public clients. [RFC 7591][rfc7591] Dynamic Client Registration at `/oauth/register` is opt-in via `ENABLE_DYNAMIC_CLIENT_REGISTRATION=true`. See the [MCP Integration Guide](docs/MCP.md). --- @@ -572,13 +572,13 @@ docker run -d \ - ✅ Token tampering (JWT signature verification) - ✅ Brute force attacks (rate limiting) - ✅ Session hijacking (encrypted cookies, CSRF protection) -- ✅ Refresh-token-as-access-token confusion (mandatory `type` claim on every JWT) +- ✅ Refresh-token-as-access-token confusion (mandatory `type` claim on access and refresh tokens) - ✅ Device-flow phishing (forced confirmation page for resource-bound device codes) - ✅ Cross-resource token replay (per-request [RFC 8707][rfc8707] audience binding) ### MCP & multi-resource hardening -**Refresh tokens cannot masquerade as access tokens.** Refresh JWTs are signed with the same key as access JWTs, so a resource server that only verifies signature/`iss`/`exp`/`aud` would silently accept a refresh token as a valid access token whenever `JWT_AUDIENCE` happens to match its resource identifier. AuthGate hard-codes the distinction: every issued JWT carries a `type` claim (`access` or `refresh`), `ValidateToken` rejects any token whose `type` is not `"access"`, and `/oauth/tokeninfo` strips `aud` from refresh-token introspection so it cannot be mistaken for an access-token response. **Operational constraint: `JWT_AUDIENCE` MUST be either unset or set to an AS-only identifier — never a resource server's `aud`.** See [docs/MCP.md → Configuration checklist](docs/MCP.md#configuration-checklist) and [docs/CONFIGURATION.md](docs/CONFIGURATION.md#environment-variables). +**Refresh tokens cannot masquerade as access tokens.** Refresh JWTs are signed with the same key as access JWTs, so a resource server that only verifies signature/`iss`/`exp`/`aud` would silently accept a refresh token as a valid access token whenever `JWT_AUDIENCE` happens to match its resource identifier. AuthGate hard-codes the distinction: every OAuth bearer token (access and refresh) carries a `type` claim, `ValidateToken` rejects any token whose `type` is not `"access"`, and `/oauth/tokeninfo` therefore returns 401 for refresh tokens — it never advertises a refresh-token `aud`. (OIDC ID tokens are a separate JWT class with no `type` claim; resource servers distinguish them as usual via `aud` and `iss`. `/oauth/introspect` follows RFC 7662 and returns metadata for both token classes, but omits `aud` on refresh tokens for the same reason.) **Operational constraint: `JWT_AUDIENCE` MUST be either unset or set to an AS-only identifier — never a resource server's `aud`.** See [docs/MCP.md → Configuration checklist](docs/MCP.md#configuration-checklist) and [docs/CONFIGURATION.md](docs/CONFIGURATION.md#environment-variables). **Device-code resource confirmation is mandatory.** When a device-code request is bound to a resource ([RFC 8707][rfc8707]), AuthGate routes the user through an explicit confirmation screen that displays the requesting client and the resource(s) being authorized **before** the device code is marked authorized — regardless of whether the user landed via `verification_uri_complete` or typed the user code into the verification form by hand. This prevents silent resource binding to an attacker-controlled MCP server. See [docs/MCP.md → Audience binding](docs/MCP.md#audience-binding-via-resource-indicators-rfc-8707). @@ -588,7 +588,7 @@ docker run -d \ - [RFC 6749][rfc6749] + [RFC 7636][rfc7636] — Authorization Code Flow with PKCE S256 (`plain` rejected) - [RFC 8707][rfc8707] — Resource Indicators, bound at issuance and verified per refresh - [RFC 8414][rfc8414] — Authorization Server Metadata at `/.well-known/oauth-authorization-server` -- [RFC 7591][rfc7591] — Dynamic Client Registration at `/oauth/register` +- [RFC 7591][rfc7591] — Dynamic Client Registration at `/oauth/register` (opt-in via `ENABLE_DYNAMIC_CLIENT_REGISTRATION=true`) - [RFC 7009][rfc7009] — Token Revocation - [RFC 7662][rfc7662] — Token Introspection From 5bc7d35a8a795415b93996da4ff9e16efce9750a Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 19 May 2026 13:58:49 +0800 Subject: [PATCH 3/3] docs(readme): narrow refresh-token and device-code claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clarify the AS / RS division of responsibility for the `type` claim: AuthGate enforces it at its own endpoints, but resource servers validating JWTs locally must also check `type == "access"` (otherwise a refresh token can be accepted if `JWT_AUDIENCE` collides with the RS audience) - Narrow the device-code callout from "phishing surface eliminated" to "silent resource binding blocked" — the confirmation page only fires for resource-bound device codes (`handlers/device.go:266-280`), so the broader phishing claim overstated the mitigation - Apply the same scope correction to the "What AuthGate Protects" bullet Addresses Copilot review on #191. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cb1a41a3..e99cbf9f 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ AuthGate also serves as a lightweight **centralised identity gateway** for inter > ⚠️ **Security highlights for MCP / multi-resource deployments** > -> - **Refresh-as-access confusion blocked at the protocol layer** — every OAuth bearer token (access and refresh) carries a `type` claim that AuthGate enforces at validation, so `/oauth/tokeninfo` returns 401 for refresh tokens and a refresh token cannot be replayed as an access token by a resource server that only verifies signature/`iss`/`exp`/`aud`. See [MCP Integration Guide → Audience binding (RFC 8707)](docs/MCP.md#audience-binding-via-resource-indicators-rfc-8707) and the [JWT Verification Guide](docs/JWT_VERIFICATION.md). -> - **Device-code phishing surface eliminated** — resource-bound device codes always route through an explicit confirmation screen displaying the client and requested resource(s), regardless of whether the user landed via `verification_uri_complete` or typed the user code by hand (documented in the same MCP guide). +> - **Refresh-as-access confusion: clear AS / RS boundary** — every OAuth bearer token (access and refresh) carries a `type` claim. AuthGate's own endpoints (`ValidateToken`, `/oauth/tokeninfo`) reject any token whose `type` is not `"access"`, and `JWT_AUDIENCE` must be configured AS-only (never a resource server identifier) so refresh tokens cannot collide with an RS's expected audience. **Resource servers validating JWTs locally must also enforce `type == "access"`** — a refresh token can otherwise be accepted if the RS only checks signature/`iss`/`exp`/`aud`. See [MCP Integration Guide → Audience binding (RFC 8707)](docs/MCP.md#audience-binding-via-resource-indicators-rfc-8707) and the [JWT Verification Guide](docs/JWT_VERIFICATION.md). +> - **Silent device-code resource binding blocked** — resource-bound device codes always route through an explicit confirmation screen displaying the client and requested resource(s) before authorization, regardless of whether the user landed via `verification_uri_complete` or typed the user code by hand (documented in the same MCP guide). > - **MCP-ready out of the box** — [RFC 8414][rfc8414] AS metadata at `/.well-known/oauth-authorization-server`, [RFC 8707][rfc8707] per-request audience binding, and PKCE S256 (rejecting `plain`) for all public clients. [RFC 7591][rfc7591] Dynamic Client Registration at `/oauth/register` is opt-in via `ENABLE_DYNAMIC_CLIENT_REGISTRATION=true`. See the [MCP Integration Guide](docs/MCP.md). --- @@ -573,7 +573,7 @@ docker run -d \ - ✅ Brute force attacks (rate limiting) - ✅ Session hijacking (encrypted cookies, CSRF protection) - ✅ Refresh-token-as-access-token confusion (mandatory `type` claim on access and refresh tokens) -- ✅ Device-flow phishing (forced confirmation page for resource-bound device codes) +- ✅ Silent device-code resource binding (mandatory confirmation page for resource-bound device codes) - ✅ Cross-resource token replay (per-request [RFC 8707][rfc8707] audience binding) ### MCP & multi-resource hardening