mcp-auth 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +257 -2
- data/README.md +25 -7
- data/app/controllers/mcp/auth/oauth_controller.rb +306 -51
- data/app/controllers/mcp/auth/well_known_controller.rb +23 -8
- data/app/models/mcp/auth/access_token.rb +5 -0
- data/app/models/mcp/auth/authorization_code.rb +4 -0
- data/app/models/mcp/auth/oauth_client.rb +154 -4
- data/app/models/mcp/auth/refresh_token.rb +12 -1
- data/app/views/mcp/auth/consent.html.erb +13 -0
- data/lib/generators/mcp/auth/hash_secrets_generator.rb +48 -0
- data/lib/generators/mcp/auth/install_generator.rb +13 -1
- data/lib/generators/mcp/auth/templates/add_mcp_auth_confidential_client_and_reuse.rb.erb +39 -0
- data/lib/generators/mcp/auth/templates/hash_mcp_auth_secrets_at_rest.rb.erb +72 -0
- data/lib/generators/mcp/auth/templates/initializer.rb +75 -8
- data/lib/generators/mcp/auth/templates/views/consent.html.erb +13 -0
- data/lib/generators/mcp/auth/upgrade_generator.rb +60 -0
- data/lib/mcp/auth/engine.rb +35 -1
- data/lib/mcp/auth/protected_resource.rb +29 -6
- data/lib/mcp/auth/schema_guard.rb +67 -0
- data/lib/mcp/auth/scope_registry.rb +12 -0
- data/lib/mcp/auth/secret_hashing.rb +83 -0
- data/lib/mcp/auth/services/authorization_service.rb +30 -11
- data/lib/mcp/auth/services/token_service.rb +161 -24
- data/lib/mcp/auth/version.rb +1 -1
- data/lib/mcp/auth.rb +51 -4
- data/lib/tasks/mcp_auth_tasks.rake +40 -6
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fe9d6d6722194f34e61bc2b2fc1104997c41c6e54fe1d8493b556d1b2004119
|
|
4
|
+
data.tar.gz: f65dbf8e46cd6cedcdd39b2cdf963c33c165a533d0d6295f5d140bf3a6e0b4ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77d89518c11901729bc2001ce45b2e1ffb64c756fd267429b160e0fc2972c81f44261f8f4c208da3b1e3b4424efea5c752e0dd75a1c34e52866997a16b56a6a7
|
|
7
|
+
data.tar.gz: 23a62caa06957f5885433a6a4d780ebf3b85d2c1833d92a1a6583f940c35e9622a8848681cbbed2a11a05dd90f8bca74bcdbbf46b88c08e12035941e491da410
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,257 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
Second security-hardening round (audit follow-ups), delivered in two phases.
|
|
13
|
+
Phase 1 — code-level fixes, no migration:
|
|
14
|
+
|
|
15
|
+
### Security (breaking where noted)
|
|
16
|
+
- **Authorization-code TTL was 30 HOURS, not 30 minutes.** The lifetime (a value
|
|
17
|
+
in seconds, e.g. `1800`) was applied with `.minutes`. Now applied as seconds,
|
|
18
|
+
and read from the single canonical config source so configured and default
|
|
19
|
+
deployments agree. **Breaking:** codes now expire in ~30 min as intended.
|
|
20
|
+
- **Issuer / audience / discovery URLs are pinned to the configured origin.**
|
|
21
|
+
`iss` and all discovery/JWKS URLs derive from `authorization_server_url`, and
|
|
22
|
+
the MCP resource (token `aud`, protected-resource metadata, 401 challenge)
|
|
23
|
+
from the new `mcp_server_url`, instead of the raw request Host — closing a
|
|
24
|
+
Host/`X-Forwarded-Host` header-injection vector. The two are separate so a
|
|
25
|
+
deployment with a distinct authorization server keeps its resource on the MCP
|
|
26
|
+
host. **When unset**, each falls back to the request origin, so the host app
|
|
27
|
+
MUST restrict permitted hosts via Rails `config.hosts`.
|
|
28
|
+
- **JWT validation hardened.** Access tokens now carry a `token_use` claim and an
|
|
29
|
+
id_token can no longer be replayed as an access token; decode enforces
|
|
30
|
+
`required_claims` (`iss`/`aud`/`sub`/`exp`, also checked explicitly so older
|
|
31
|
+
ruby-jwt versions that ignore that option are covered); `exp` is strict and
|
|
32
|
+
`nbf` allows a bounded clock-skew leeway;
|
|
33
|
+
a missing audience is no longer silently accepted.
|
|
34
|
+
- **Confidential-client auth at the token endpoint.** A `client_secret` presented
|
|
35
|
+
on a code/refresh request is now verified (constant-time); an invalid secret is
|
|
36
|
+
rejected. (Full *requirement* of a secret for confidential clients lands with
|
|
37
|
+
the `token_endpoint_auth_method` column in Phase 2.)
|
|
38
|
+
- **Refresh-token rotation is atomic.** Rotation is gated on a conditional delete,
|
|
39
|
+
so two concurrent redemptions of one refresh token can no longer each mint a
|
|
40
|
+
new token family.
|
|
41
|
+
- **Consent enforces least privilege.** A client can no longer be granted a scope
|
|
42
|
+
it never requested; approved scopes are intersected with the requested set
|
|
43
|
+
(plus the server's registered required scopes). If nothing the user approved
|
|
44
|
+
was requested, the flow ends with `invalid_scope` instead of falling back to
|
|
45
|
+
the original request.
|
|
46
|
+
- **A dedicated `oauth_secret` is required outside development/test.** The gem
|
|
47
|
+
no longer signs HS256 tokens with `Rails.application.secret_key_base` (key
|
|
48
|
+
separation): an unset secret, or one equal to `secret_key_base`, now raises.
|
|
49
|
+
The generated initializer no longer falls back to `secret_key_base`, and the
|
|
50
|
+
unused `MCP_OAUTH_PRIVATE_KEY` engine setting is removed. Dev/test still fall
|
|
51
|
+
back. **Breaking** — see Upgrade.
|
|
52
|
+
|
|
53
|
+
Phase 2 — secrets hashed at rest (adds a migration) + medium fixes:
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
- **All persisted secrets are now hashed at rest.** Access-token JWTs, refresh
|
|
57
|
+
tokens, authorization codes, and client secrets are stored as SHA-256 digests
|
|
58
|
+
(prefixed `sha256$`); a database leak no longer yields usable credentials. The
|
|
59
|
+
plaintext client secret is returned exactly once at registration; tokens/codes
|
|
60
|
+
are returned once to the client and matched by digest thereafter.
|
|
61
|
+
- **Dynamic Client Registration rejects unsupported grant/response types**
|
|
62
|
+
(RFC 7591 §2) instead of storing arbitrary metadata.
|
|
63
|
+
- **Refresh-token reuse detection** (OAuth 2.1 §4.14.2). Rotation now marks the
|
|
64
|
+
presented token revoked (grouped by a `family_id`) instead of deleting it, so
|
|
65
|
+
an authenticated client replaying an already-rotated token is detected as theft
|
|
66
|
+
and the **entire family is revoked — both the refresh tokens and the access
|
|
67
|
+
tokens already issued to that principal** (immediate cut-off, not left valid
|
|
68
|
+
until expiry). Client authentication is checked *before* this reaction, so an
|
|
69
|
+
unauthenticated replay can't trigger family revocation. Rotation is atomic
|
|
70
|
+
(only the request that flips `revoked_at` wins), superseding the delete-based
|
|
71
|
+
race fix. The migration backfills a `family_id` for pre-existing tokens so
|
|
72
|
+
reuse detection covers them too. A configurable **rotation grace period**
|
|
73
|
+
(`refresh_token_reuse_grace_period`, default 10s) treats a rotated token
|
|
74
|
+
replayed moments later as a benign concurrent-refresh race (rejected softly,
|
|
75
|
+
family kept) rather than theft, so well-behaved MCP clients that fire several
|
|
76
|
+
refreshes when the access token expires aren't logged out.
|
|
77
|
+
- **Access tokens carry a unique `jti`** (RFC 7519) so two tokens issued in the
|
|
78
|
+
same second for the same principal/scope don't collide on the unique token
|
|
79
|
+
index (which previously raised on rapid/concurrent refreshes).
|
|
80
|
+
- **Confidential-client authentication is now enforced.** Clients carry a
|
|
81
|
+
`token_endpoint_auth_method`; a confidential client (`client_secret_basic` /
|
|
82
|
+
`client_secret_post`) MUST present a valid secret at the token endpoint, while
|
|
83
|
+
a public client (`none`, the default) relies on PKCE. **Existing clients
|
|
84
|
+
default to `none`, so nothing that worked before starts requiring a secret** —
|
|
85
|
+
a client opts into confidential auth explicitly at registration.
|
|
86
|
+
|
|
87
|
+
- **Refresh rotation and successor issuance are one transaction.** If minting
|
|
88
|
+
the new tokens fails (or yields no refresh token), the rotation rolls back and
|
|
89
|
+
the client can retry with the token it holds, instead of being left with no
|
|
90
|
+
valid refresh token (a forced logout).
|
|
91
|
+
- **`WWW-Authenticate` on a protected-resource 401 can be pinned.** The
|
|
92
|
+
`resource_metadata` URL derives from `mcp_server_url` when set, so a forged
|
|
93
|
+
Host header can't steer clients to attacker-controlled metadata.
|
|
94
|
+
- **Dangerous redirect-URI schemes are rejected at registration.**
|
|
95
|
+
`javascript:`, `data:`, `vbscript:`, `file:`, `about:` and `blob:` are refused
|
|
96
|
+
even when written with `://` (e.g. `javascript://%0aalert(1)`), which the
|
|
97
|
+
native-app-scheme check previously let through.
|
|
98
|
+
|
|
99
|
+
- **The per-user scope policy is enforced at approval.** `validate_scope_for_user`
|
|
100
|
+
only filtered what the consent screen showed; a user could POST a hidden scope
|
|
101
|
+
name to `/oauth/approve` and have it granted, and required scopes were re-added
|
|
102
|
+
even when the policy denied them. The policy now filters the granted set.
|
|
103
|
+
- **OAuth credentials are filtered from logs.** The engine adds `code`,
|
|
104
|
+
`code_verifier`, `client_secret`, `refresh_token`, `access_token`, `id_token`
|
|
105
|
+
and `token` to `filter_parameters` (Rails' defaults miss `code` and
|
|
106
|
+
`code_verifier`), and filters redirects carrying `?code=`.
|
|
107
|
+
- **Token responses are not cacheable** (RFC 6749 §5.1): `Cache-Control: no-store`
|
|
108
|
+
and `Pragma: no-cache` on token, register, introspect and userinfo.
|
|
109
|
+
- **The consent page can't be framed or read cross-origin.** `X-Frame-Options:
|
|
110
|
+
DENY` and `frame-ancestors 'none'` on authorize/approve, and CORS headers are no
|
|
111
|
+
longer sent there (RFC 9700 §2.6); the other endpoints keep CORS.
|
|
112
|
+
|
|
113
|
+
### Security (DCR hardening)
|
|
114
|
+
- **Redirect-URI policy at registration.** Plain `http` is rejected except for
|
|
115
|
+
loopback hosts (`localhost`, `127.0.0.1`, `[::1]`, any port; RFC 8252 §7.3);
|
|
116
|
+
optional `allowed_redirect_uri_patterns` restricts registration to known
|
|
117
|
+
clients (Regexps must match the whole URI, anchored or not), and
|
|
118
|
+
`allow_loopback_redirects` can disable loopback. The policy applies to every
|
|
119
|
+
registered redirect URI regardless of grant types, and `/oauth/authorize`
|
|
120
|
+
refuses clients that didn't register the `authorization_code` grant.
|
|
121
|
+
**Breaking:** non-loopback `http://` redirect URIs no longer register.
|
|
122
|
+
- **Unknown scopes are rejected** at registration (`invalid_client_metadata`) and
|
|
123
|
+
at `/oauth/authorize` (`invalid_scope` redirect, RFC 6749 §4.1.2.1) instead of
|
|
124
|
+
being stored/echoed or silently dropped. OIDC scopes and `offline_access` are
|
|
125
|
+
accepted. `strict_scope_validation = false` restores the old narrowing.
|
|
126
|
+
- **`client_name` is sanitized** (control/bidi/zero-width characters stripped,
|
|
127
|
+
100-char cap) and `client_uri` must be an http(s) URL, since both are
|
|
128
|
+
attacker-chosen at open registration and shown on the consent page.
|
|
129
|
+
- **Consent screen shows the redirect host** and flags applications whose host
|
|
130
|
+
isn't in `verified_redirect_hosts` as unverified (loopback is exempt).
|
|
131
|
+
**Existing installs:** the install generator copied the consent view into
|
|
132
|
+
your app (`app/views/mcp/auth/consent.html.erb`), and that copy overrides the
|
|
133
|
+
gem's, so you won't see these additions until you merge them in. Compare
|
|
134
|
+
your copy with the gem's `lib/generators/mcp/auth/templates/views/consent.html.erb`
|
|
135
|
+
(the "redirect-info" paragraph and the "Unverified application" box).
|
|
136
|
+
|
|
137
|
+
### Fixed
|
|
138
|
+
- The generated initializer closed the `Mcp::Auth.configure` block before the
|
|
139
|
+
JWT-signing and `secret_dual_read` sections, so uncommenting
|
|
140
|
+
`config.secret_dual_read = false` (as the upgrade generator instructs) raised
|
|
141
|
+
`NameError` at boot.
|
|
142
|
+
- The secrets-hashing backfill migration processes rows in batches of 1000
|
|
143
|
+
(keyset-paginated by primary key) instead of loading each table into memory.
|
|
144
|
+
- Re-enabled a dead spec file (`spec/services/authorization_service.rb` →
|
|
145
|
+
`…_spec.rb`) that RSpec never ran, restoring ~130 lines of coverage.
|
|
146
|
+
- `mcp_auth:revoke_*` rake tasks now delete across the three tables inside a
|
|
147
|
+
transaction (no partial revocation on mid-way failure).
|
|
148
|
+
|
|
149
|
+
### Added
|
|
150
|
+
- **`config.mcp_server_url`** — public origin of the MCP resource server. Pins the
|
|
151
|
+
token audience, protected-resource metadata, and 401 `resource_metadata` URL.
|
|
152
|
+
Optional (defaults to the request origin); set it whenever
|
|
153
|
+
`authorization_server_url` points at a different host.
|
|
154
|
+
- **`Mcp::Auth::ProtectedResource.www_authenticate(base_url, error:, description:)`**
|
|
155
|
+
— the RFC 9728 `WWW-Authenticate` challenge as a plain function, so a Rack
|
|
156
|
+
middleware guarding `/mcp` can emit the same header the controller concern
|
|
157
|
+
does (without it, spec-compliant MCP clients can't discover the metadata and
|
|
158
|
+
re-authorize).
|
|
159
|
+
- **Pending-migration guard.** If the gem is upgraded but its migrations haven't
|
|
160
|
+
run, mcp-auth now says so instead of failing with a cryptic `unknown attribute`:
|
|
161
|
+
a clear warning is logged at boot, the OAuth endpoints return an actionable
|
|
162
|
+
`server_error` ("run a pending migration"), and `rake mcp_auth:doctor` reports
|
|
163
|
+
schema drift and exits non-zero. Fully guarded — never breaks boot, CI, or
|
|
164
|
+
`db:*` tasks when the database is absent or unmigrated.
|
|
165
|
+
- **`rails generate mcp:auth:upgrade`** — copies the pending schema migration for
|
|
166
|
+
an existing install (no initializer/view overwrite prompts, unlike re-running
|
|
167
|
+
the full install generator); **`rails generate mcp:auth:hash_secrets`** copies
|
|
168
|
+
the secrets-hashing backfill, run once every server is upgraded.
|
|
169
|
+
- **`config.secret_dual_read`** (default `true`) — transitional dual-read: a
|
|
170
|
+
presented secret/token/code is matched against both its digest and any legacy
|
|
171
|
+
plaintext row not yet backfilled, so 0.6.0 keeps working before and during
|
|
172
|
+
the backfill. It does NOT make older versions read hashed rows (see Upgrade).
|
|
173
|
+
Set to `false` once every row is hashed to harden.
|
|
174
|
+
|
|
175
|
+
### Upgrade
|
|
176
|
+
|
|
177
|
+
Two migrations, applied in **two separate steps** so no server ever sees a
|
|
178
|
+
schema or data format it can't handle:
|
|
179
|
+
|
|
180
|
+
- `add_mcp_auth_confidential_client_and_reuse`: **additive columns**
|
|
181
|
+
(`token_endpoint_auth_method` on clients; `family_id` + `revoked_at` on refresh
|
|
182
|
+
tokens). 0.5.0 ignores them; 0.6.0 **requires** them (its OAuth endpoints
|
|
183
|
+
return a 500 with a "run a pending migration" message until they exist).
|
|
184
|
+
- `hash_mcp_auth_secrets_at_rest`: a **data backfill** that hashes existing
|
|
185
|
+
secrets in place. 0.6.0 reads both forms (`secret_dual_read`); **0.5.0 cannot
|
|
186
|
+
read hashed rows**.
|
|
187
|
+
|
|
188
|
+
**Before deploying (HS256 users):** set a dedicated `MCP_HMAC_SECRET` (`rails
|
|
189
|
+
secret`). Outside development/test, token signing now fails if `oauth_secret` is
|
|
190
|
+
unset **or equal to `secret_key_base`**, which is exactly what initializers
|
|
191
|
+
generated by earlier versions (`ENV.fetch('MCP_HMAC_SECRET', secret_key_base)`)
|
|
192
|
+
fall back to. Change that line to `config.oauth_secret = ENV['MCP_HMAC_SECRET']`.
|
|
193
|
+
`rake mcp_auth:doctor` and a boot-time warning report the problem. Changing the
|
|
194
|
+
secret invalidates outstanding access tokens (clients refresh); refresh tokens
|
|
195
|
+
are unaffected.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
bundle update mcp-auth
|
|
199
|
+
|
|
200
|
+
# Step 1 — schema. Run BEFORE the new code serves traffic (e.g. release phase).
|
|
201
|
+
rails generate mcp:auth:upgrade
|
|
202
|
+
rails db:migrate
|
|
203
|
+
# ...deploy 0.6.0 to every server...
|
|
204
|
+
bin/rails mcp_auth:doctor # schema + signing-secret check
|
|
205
|
+
|
|
206
|
+
# Step 2 — once NO server runs <= 0.5.0 (a later deploy is fine):
|
|
207
|
+
rails generate mcp:auth:hash_secrets
|
|
208
|
+
rails db:migrate
|
|
209
|
+
# then: config.secret_dual_read = false
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The backfill hashes the plaintext already present in each column, so **existing
|
|
213
|
+
clients and tokens keep working without re-registration** — the client still
|
|
214
|
+
presents its original value and the gem re-hashes it to match. Existing clients
|
|
215
|
+
get `token_endpoint_auth_method = 'none'` (public), so none of them suddenly
|
|
216
|
+
requires a secret.
|
|
217
|
+
|
|
218
|
+
**During the step-1 rollout** (both versions serving), tokens and codes *issued
|
|
219
|
+
by* 0.6.0 servers are stored hashed, so a 0.5.0 server can't validate them until
|
|
220
|
+
it is replaced; keep that window short. After step 2, rolling back to ≤ 0.5.0
|
|
221
|
+
signs every client out. The hashing backfill is idempotent and irreversible.
|
|
222
|
+
|
|
223
|
+
## [0.5.0] - 2026-06-15
|
|
224
|
+
|
|
225
|
+
Security-hardening release. Closes five OAuth 2.1 / MCP authorization
|
|
226
|
+
vulnerabilities found in an adversarial audit of the authorization server and
|
|
227
|
+
protected-resource layer. Each fix ships with an RSpec test that fails before
|
|
228
|
+
and passes after.
|
|
229
|
+
|
|
230
|
+
### Security (breaking where noted)
|
|
231
|
+
- **Consent can no longer be bypassed.** `GET /oauth/authorize` previously issued
|
|
232
|
+
an authorization code immediately when `approved=true` was present on the
|
|
233
|
+
request URL — a GET, so not even CSRF-protected — skipping the consent screen
|
|
234
|
+
entirely. The authorization endpoint now always renders consent; a code is
|
|
235
|
+
granted only via the CSRF-protected `POST /oauth/approve`. **Breaking:** clients
|
|
236
|
+
that appended `approved=true` to the authorize URL to auto-approve must go
|
|
237
|
+
through the consent/approve step.
|
|
238
|
+
- **Refresh tokens are bound to the issuing client** (OAuth 2.1 §4.3.1). The
|
|
239
|
+
refresh grant now rejects redemption unless the requesting `client_id` (Basic
|
|
240
|
+
auth or body) matches the client the token was issued to, and does not rotate
|
|
241
|
+
the token on a failed check. **Breaking:** a refresh request must include the
|
|
242
|
+
matching `client_id` — the documented flow already does.
|
|
243
|
+
- **Authorization codes are consumed atomically** (OAuth 2.1 §4.1.2). Consumption
|
|
244
|
+
now deletes the code in a single atomic operation and the token grant aborts
|
|
245
|
+
unless it won that deletion, eliminating a race that could mint two token sets
|
|
246
|
+
from one code.
|
|
247
|
+
- **Resource indicators are validated** (RFC 8707 / MCP authorization spec).
|
|
248
|
+
`authorize` and both token grants reject any `resource` that does not identify
|
|
249
|
+
this server (`invalid_target`), so the server can no longer mint a token whose
|
|
250
|
+
audience is some other — possibly attacker-controlled — resource. **Breaking:**
|
|
251
|
+
requests carrying a `resource` for a different host/path are rejected.
|
|
252
|
+
- **`api_key_secret` is no longer embedded in access tokens.** A bearer JWT is
|
|
253
|
+
decodable by anyone holding it and is stored at rest, so only the non-sensitive
|
|
254
|
+
`api_key_id` is now included; resolve the matching secret server-side from that
|
|
255
|
+
id. Any `api_key_secret` returned by `fetch_user_data` is ignored.
|
|
256
|
+
|
|
257
|
+
### Changed
|
|
258
|
+
- README and the generated initializer document that `fetch_user_data` must not
|
|
259
|
+
return secrets (they are ignored and never written into the token).
|
|
260
|
+
|
|
10
261
|
## [0.4.0] - 2026-05-29
|
|
11
262
|
|
|
12
263
|
Security-hardening release. Closes four OAuth correctness bugs and adds the
|
|
@@ -158,7 +409,8 @@ keep `HS256` until refresh tokens cycle out.
|
|
|
158
409
|
- Protected Resource Metadata (RFC 9728)
|
|
159
410
|
- Resource Indicators support (RFC 8707) for token audience binding
|
|
160
411
|
- OpenID Connect Discovery support
|
|
161
|
-
-
|
|
412
|
+
- Opt-in resource-server protection for MCP routes via the
|
|
413
|
+
`Mcp::Auth::ProtectedResource` concern
|
|
162
414
|
- JWT access tokens with proper audience validation
|
|
163
415
|
- Refresh token rotation for enhanced security
|
|
164
416
|
- Database-backed token storage for revocation support
|
|
@@ -177,7 +429,10 @@ keep `HS256` until refresh tokens cycle out.
|
|
|
177
429
|
- Token audience validation to prevent confused deputy attacks
|
|
178
430
|
- WWW-Authenticate header with resource metadata on 401 responses
|
|
179
431
|
|
|
180
|
-
[Unreleased]: https://github.com/SerhiiBorozenets/mcp-auth/compare/v0.
|
|
432
|
+
[Unreleased]: https://github.com/SerhiiBorozenets/mcp-auth/compare/v0.6.0...HEAD
|
|
433
|
+
[0.6.0]: https://github.com/SerhiiBorozenets/mcp-auth/compare/v0.5.0...v0.6.0
|
|
434
|
+
[0.5.0]: https://github.com/SerhiiBorozenets/mcp-auth/compare/v0.4.0...v0.5.0
|
|
435
|
+
[0.4.0]: https://github.com/SerhiiBorozenets/mcp-auth/compare/v0.3.0...v0.4.0
|
|
181
436
|
[0.3.0]: https://github.com/SerhiiBorozenets/mcp-auth/compare/v0.2.0...v0.3.0
|
|
182
437
|
[0.2.0]: https://github.com/SerhiiBorozenets/mcp-auth/compare/v0.1.0...v0.2.0
|
|
183
438
|
[0.1.0]: https://github.com/SerhiiBorozenets/mcp-auth/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -135,6 +135,11 @@ rails generate mcp:auth:install
|
|
|
135
135
|
rails db:migrate
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
**Upgrading an existing install?** Don't re-run the install generator. Use
|
|
139
|
+
`rails generate mcp:auth:upgrade` (schema, before deploying) and, once every
|
|
140
|
+
server runs the new version, `rails generate mcp:auth:hash_secrets`. See the
|
|
141
|
+
0.6.0 "Upgrade" section in [CHANGELOG.md](CHANGELOG.md) for the exact order.
|
|
142
|
+
|
|
138
143
|
## Quick Start
|
|
139
144
|
|
|
140
145
|
### 1. Mount Routes
|
|
@@ -162,12 +167,17 @@ Edit `config/initializers/mcp_auth.rb`:
|
|
|
162
167
|
|
|
163
168
|
```ruby
|
|
164
169
|
Mcp::Auth.configure do |config|
|
|
165
|
-
# OAuth secret for JWT signing
|
|
166
|
-
|
|
170
|
+
# OAuth secret for JWT signing (HS256) — required outside development/test,
|
|
171
|
+
# and must not be secret_key_base. Generate with `rails secret`.
|
|
172
|
+
config.oauth_secret = ENV['MCP_HMAC_SECRET']
|
|
167
173
|
|
|
168
174
|
# Authorization server URL (optional - defaults to same as resource server)
|
|
169
175
|
config.authorization_server_url = ENV.fetch('MCP_AUTHORIZATION_SERVER_URL', nil)
|
|
170
176
|
|
|
177
|
+
# Public origin of the MCP resource server (optional - defaults to the request
|
|
178
|
+
# origin). Pins the token audience and protected-resource metadata URLs.
|
|
179
|
+
config.mcp_server_url = ENV.fetch('MCP_SERVER_URL', nil)
|
|
180
|
+
|
|
171
181
|
# MCP Server Path - where your MCP server is mounted
|
|
172
182
|
# Change this if your MCP server is NOT at '/mcp'
|
|
173
183
|
config.mcp_server_path = ENV.fetch('MCP_SERVER_PATH', '/mcp')
|
|
@@ -186,18 +196,22 @@ Mcp::Auth.configure do |config|
|
|
|
186
196
|
config.authorization_code_lifetime = 1800 # 30 minutes
|
|
187
197
|
|
|
188
198
|
# User data fetcher - CUSTOMIZE THIS
|
|
199
|
+
#
|
|
200
|
+
# SECURITY: only NON-sensitive values are embedded into the access token (a
|
|
201
|
+
# bearer JWT is decodable by anyone holding it and is stored at rest). Return
|
|
202
|
+
# an api_key_id (an opaque reference) and resolve the matching secret
|
|
203
|
+
# server-side from that id at request time. Never return a raw secret here —
|
|
204
|
+
# any `api_key_secret` is intentionally ignored and NOT placed in the token.
|
|
189
205
|
config.fetch_user_data = proc do |data|
|
|
190
206
|
user = User.find(data[:user_id])
|
|
191
207
|
org = Org.find(data[:org_id]) if data[:org_id]
|
|
192
|
-
|
|
193
|
-
# Return user data + API key (if you have one)
|
|
208
|
+
|
|
194
209
|
{
|
|
195
210
|
email: user.email,
|
|
196
|
-
api_key_id: org&.api_key&.id
|
|
197
|
-
api_key_secret: org&.api_key&.secret
|
|
211
|
+
api_key_id: org&.api_key&.id
|
|
198
212
|
}
|
|
199
213
|
rescue ActiveRecord::RecordNotFound
|
|
200
|
-
{ email: 'unknown@example.com', api_key_id: nil
|
|
214
|
+
{ email: 'unknown@example.com', api_key_id: nil }
|
|
201
215
|
end
|
|
202
216
|
|
|
203
217
|
# Methods for authentication
|
|
@@ -518,6 +532,10 @@ If you want to use a separate authorization server:
|
|
|
518
532
|
```ruby
|
|
519
533
|
# config/initializers/mcp_auth.rb
|
|
520
534
|
config.authorization_server_url = 'https://auth.example.com'
|
|
535
|
+
# Public origin of THIS app's MCP endpoint (the token audience). Optional —
|
|
536
|
+
# defaults to the request origin — but set it to pin the audience and metadata
|
|
537
|
+
# URLs against a forged Host header.
|
|
538
|
+
config.mcp_server_url = 'https://api.example.com'
|
|
521
539
|
```
|
|
522
540
|
|
|
523
541
|
This is useful for:
|