mcp-auth 0.5.0 → 0.6.1
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 +237 -2
- data/README.md +211 -76
- data/app/controllers/mcp/auth/oauth_controller.rb +269 -56
- 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 +67 -4
- 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 +18 -8
- data/lib/mcp/auth/services/token_service.rb +152 -23
- 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
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ OAuth 2.1 authorization for Model Context Protocol (MCP) servers in Rails applic
|
|
|
4
4
|
|
|
5
5
|
## What is MCP Authorization?
|
|
6
6
|
|
|
7
|
-
The Model Context Protocol (MCP) is an open standard that enables AI assistants to securely connect to external data sources and tools. MCP Auth implements the [MCP Authorization specification](https://modelcontextprotocol.io/specification/
|
|
7
|
+
The Model Context Protocol (MCP) is an open standard that enables AI assistants to securely connect to external data sources and tools. MCP Auth implements the [MCP Authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization), providing OAuth 2.1-based authentication for MCP servers.
|
|
8
8
|
|
|
9
9
|
### Why OAuth for MCP?
|
|
10
10
|
|
|
@@ -115,7 +115,10 @@ sequenceDiagram
|
|
|
115
115
|
- ✅ **Token Revocation** - RFC 7009 support
|
|
116
116
|
- ✅ **Token Introspection** - RFC 7662 support
|
|
117
117
|
- ✅ **OpenID Connect** - Basic OIDC Discovery support
|
|
118
|
-
- ✅ **Refresh Token Rotation** - OAuth 2.1 security requirement
|
|
118
|
+
- ✅ **Refresh Token Rotation** - OAuth 2.1 security requirement, with reuse detection
|
|
119
|
+
- ✅ **Secrets Hashed at Rest** - tokens, codes and client secrets stored as SHA-256 digests
|
|
120
|
+
- ✅ **Registration Policy** - https-or-loopback redirect URIs, optional allowlist, unknown scopes rejected
|
|
121
|
+
- ✅ **RS256 / ES256 Signing** - optional asymmetric JWTs with a JWKS endpoint and key rotation
|
|
119
122
|
- ✅ **Fully Configurable** - Paths, URLs, lifetimes, and user data
|
|
120
123
|
- ✅ **Beautiful Consent Screen** - Customizable UI
|
|
121
124
|
|
|
@@ -135,6 +138,11 @@ rails generate mcp:auth:install
|
|
|
135
138
|
rails db:migrate
|
|
136
139
|
```
|
|
137
140
|
|
|
141
|
+
**Upgrading an existing install?** Don't re-run the install generator. Use
|
|
142
|
+
`rails generate mcp:auth:upgrade` (schema, before deploying) and, once every
|
|
143
|
+
server runs the new version, `rails generate mcp:auth:hash_secrets`. See the
|
|
144
|
+
0.6.0 "Upgrade" section in [CHANGELOG.md](CHANGELOG.md) for the exact order.
|
|
145
|
+
|
|
138
146
|
## Quick Start
|
|
139
147
|
|
|
140
148
|
### 1. Mount Routes
|
|
@@ -162,12 +170,17 @@ Edit `config/initializers/mcp_auth.rb`:
|
|
|
162
170
|
|
|
163
171
|
```ruby
|
|
164
172
|
Mcp::Auth.configure do |config|
|
|
165
|
-
# OAuth secret for JWT signing
|
|
166
|
-
|
|
173
|
+
# OAuth secret for JWT signing (HS256) — required outside development/test,
|
|
174
|
+
# and must not be secret_key_base. Generate with `rails secret`.
|
|
175
|
+
config.oauth_secret = ENV['MCP_HMAC_SECRET']
|
|
167
176
|
|
|
168
177
|
# Authorization server URL (optional - defaults to same as resource server)
|
|
169
178
|
config.authorization_server_url = ENV.fetch('MCP_AUTHORIZATION_SERVER_URL', nil)
|
|
170
179
|
|
|
180
|
+
# Public origin of the MCP resource server (optional - defaults to the request
|
|
181
|
+
# origin). Pins the token audience and protected-resource metadata URLs.
|
|
182
|
+
config.mcp_server_url = ENV.fetch('MCP_SERVER_URL', nil)
|
|
183
|
+
|
|
171
184
|
# MCP Server Path - where your MCP server is mounted
|
|
172
185
|
# Change this if your MCP server is NOT at '/mcp'
|
|
173
186
|
config.mcp_server_path = ENV.fetch('MCP_SERVER_PATH', '/mcp')
|
|
@@ -231,6 +244,10 @@ class ApplicationController < ActionController::Base
|
|
|
231
244
|
end
|
|
232
245
|
```
|
|
233
246
|
|
|
247
|
+
**Login redirect:** when a user who isn't signed in reaches `/oauth/authorize`,
|
|
248
|
+
MCP Auth redirects to `main_app.new_user_session_path`, the route Devise defines.
|
|
249
|
+
Without Devise, define a route with that name that leads to your login page.
|
|
250
|
+
|
|
234
251
|
### 4. Set Environment Variables
|
|
235
252
|
|
|
236
253
|
```bash
|
|
@@ -272,16 +289,35 @@ MCP Auth provides these endpoints automatically:
|
|
|
272
289
|
|
|
273
290
|
### Protecting Your MCP Server
|
|
274
291
|
|
|
275
|
-
MCP Auth
|
|
292
|
+
MCP Auth does **not** protect any route by itself. Include the resource-server
|
|
293
|
+
concern in the controller that serves your MCP endpoint:
|
|
276
294
|
|
|
277
295
|
```ruby
|
|
278
|
-
|
|
279
|
-
|
|
296
|
+
class McpController < ApplicationController
|
|
297
|
+
include Mcp::Auth::ProtectedResource
|
|
298
|
+
|
|
299
|
+
before_action :authenticate_mcp_token!
|
|
300
|
+
before_action -> { require_mcp_scope!('mcp:write') }, only: :create
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`authenticate_mcp_token!` checks the Bearer token's signature, expiry,
|
|
305
|
+
revocation status and audience (`mcp_server_url` or the request origin, plus
|
|
306
|
+
`mcp_server_path`). On failure it returns a 401 whose `WWW-Authenticate` header
|
|
307
|
+
points MCP clients at the protected-resource metadata (RFC 9728).
|
|
308
|
+
`require_mcp_scope!` returns a 403 `insufficient_scope` when a scope is missing.
|
|
280
309
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
310
|
+
If your MCP server is mounted as Rack middleware (not a controller), validate
|
|
311
|
+
the token yourself and build the same header:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
payload = Mcp::Auth::Services::TokenService.validate_access_token(
|
|
315
|
+
token, resource: 'https://example.com/mcp'
|
|
316
|
+
)
|
|
317
|
+
unless payload
|
|
318
|
+
headers = { 'WWW-Authenticate' => Mcp::Auth::ProtectedResource.www_authenticate('https://example.com') }
|
|
319
|
+
return [401, headers, ['Unauthorized']]
|
|
320
|
+
end
|
|
285
321
|
```
|
|
286
322
|
|
|
287
323
|
### OAuth 2.1 Authorization Flow
|
|
@@ -310,10 +346,17 @@ Response:
|
|
|
310
346
|
"redirect_uris": ["https://client.example.com/callback"],
|
|
311
347
|
"grant_types": ["authorization_code", "refresh_token"],
|
|
312
348
|
"response_types": ["code"],
|
|
313
|
-
"scope": "mcp:read mcp:write"
|
|
349
|
+
"scope": "mcp:read mcp:write",
|
|
350
|
+
"token_endpoint_auth_method": "none",
|
|
351
|
+
"client_name": "My MCP Client"
|
|
314
352
|
}
|
|
315
353
|
```
|
|
316
354
|
|
|
355
|
+
The `client_secret` is returned only once; only its hash is stored. Clients are
|
|
356
|
+
public (`none`, PKCE only) unless they register `token_endpoint_auth_method` as
|
|
357
|
+
`client_secret_basic` or `client_secret_post`, in which case the token endpoint
|
|
358
|
+
requires the secret.
|
|
359
|
+
|
|
317
360
|
#### 2. Authorization Request with PKCE
|
|
318
361
|
|
|
319
362
|
Generate PKCE parameters:
|
|
@@ -391,7 +434,8 @@ curl -X POST https://example.com/oauth/token \
|
|
|
391
434
|
|
|
392
435
|
### Helper Methods in Controllers
|
|
393
436
|
|
|
394
|
-
|
|
437
|
+
These read what `authenticate_mcp_token!` stored for the request, so they are
|
|
438
|
+
only set in controllers that run it (see above):
|
|
395
439
|
|
|
396
440
|
```ruby
|
|
397
441
|
class MyController < ApplicationController
|
|
@@ -456,63 +500,109 @@ config.mcp_docs_url = 'https://docs.example.com/mcp-api'
|
|
|
456
500
|
|
|
457
501
|
### Custom Consent Screen
|
|
458
502
|
|
|
459
|
-
|
|
503
|
+
`rails generate mcp:auth:install` copies the consent page to
|
|
504
|
+
`app/views/mcp/auth/consent.html.erb`. That copy overrides the gem's view, so
|
|
505
|
+
edit it to match your branding. Optionally render it inside your app layout:
|
|
460
506
|
|
|
461
507
|
```ruby
|
|
462
|
-
#
|
|
463
|
-
config.use_custom_consent_view = true
|
|
464
|
-
|
|
465
|
-
# 2. Edit app/views/mcp/auth/consent.html.erb
|
|
508
|
+
# config/initializers/mcp_auth.rb
|
|
509
|
+
config.use_custom_consent_view = true # render with layout 'application'
|
|
510
|
+
config.consent_view_path = 'mcp/auth/consent' # the template to render
|
|
466
511
|
```
|
|
467
512
|
|
|
468
|
-
|
|
513
|
+
Variables available in the view:
|
|
469
514
|
|
|
470
|
-
- `@client_name` - Name
|
|
471
|
-
- `@
|
|
472
|
-
- `@
|
|
515
|
+
- `@client_name` - Name the client registered (self-asserted; not verified)
|
|
516
|
+
- `@redirect_host` - Host the authorization code will be sent to. **Show it.**
|
|
517
|
+
- `@redirect_verified` / `@redirect_loopback` - Whether that host is in
|
|
518
|
+
`verified_redirect_hosts`, or is a loopback address
|
|
519
|
+
- `@requested_scopes` - Array of hashes: `:key`, `:name`, `:description`,
|
|
520
|
+
`:required`, `:pre_selected`
|
|
521
|
+
- `@authorization_params` - OAuth parameters to send back unchanged
|
|
473
522
|
|
|
474
|
-
|
|
523
|
+
The approve form must POST the user's chosen scopes as `scopes[]` (required
|
|
524
|
+
scopes included) together with `approved=true`; with no scopes the request is
|
|
525
|
+
rejected. A minimal example:
|
|
475
526
|
|
|
476
527
|
```erb
|
|
477
|
-
|
|
478
|
-
<
|
|
479
|
-
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
528
|
+
<h1><%= @client_name %> wants to access your account</h1>
|
|
529
|
+
<p>You will be sent back to <strong><%= @redirect_host %></strong></p>
|
|
530
|
+
<% unless @redirect_verified || @redirect_loopback %>
|
|
531
|
+
<p><strong>Unverified application.</strong> Only continue if you started this connection.</p>
|
|
532
|
+
<% end %>
|
|
533
|
+
|
|
534
|
+
<%= form_with url: oauth_approve_path, method: :post, local: true do |f| %>
|
|
535
|
+
<% @authorization_params.each do |key, value| %>
|
|
536
|
+
<%= f.hidden_field key, value: value, id: nil %>
|
|
537
|
+
<% end %>
|
|
538
|
+
<% @requested_scopes.each do |scope| %>
|
|
539
|
+
<label>
|
|
540
|
+
<%= check_box_tag 'scopes[]', scope[:key], scope[:required] || scope[:pre_selected],
|
|
541
|
+
id: nil, disabled: scope[:required] %>
|
|
542
|
+
<%= scope[:name] %> — <%= scope[:description] %>
|
|
543
|
+
</label>
|
|
544
|
+
<%# disabled checkboxes aren't submitted, so send required scopes separately %>
|
|
545
|
+
<%= hidden_field_tag 'scopes[]', scope[:key], id: nil if scope[:required] %>
|
|
546
|
+
<% end %>
|
|
547
|
+
<%= f.button 'Allow', name: 'approved', value: 'true' %>
|
|
548
|
+
<% end %>
|
|
549
|
+
|
|
550
|
+
<%= form_with url: oauth_approve_path, method: :post, local: true do |f| %>
|
|
551
|
+
<% @authorization_params.each do |key, value| %>
|
|
552
|
+
<%= f.hidden_field key, value: value, id: nil %>
|
|
553
|
+
<% end %>
|
|
554
|
+
<%= f.hidden_field :approved, value: 'false' %>
|
|
555
|
+
<%= f.button 'Deny' %>
|
|
556
|
+
<% end %>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Scopes and Per-User Policy
|
|
560
|
+
|
|
561
|
+
Register the scopes your MCP server understands (requests naming any other
|
|
562
|
+
scope are rejected unless `strict_scope_validation = false`):
|
|
563
|
+
|
|
564
|
+
```ruby
|
|
565
|
+
config.register_scope 'mcp:read', name: 'Read Access', description: 'Read your data', required: true
|
|
566
|
+
config.register_scope 'mcp:write', name: 'Write Access', description: 'Modify data on your behalf'
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
To restrict which scopes a given user may grant, set a policy. It hides
|
|
570
|
+
scopes on the consent screen **and** is enforced when the code is issued:
|
|
571
|
+
|
|
572
|
+
```ruby
|
|
573
|
+
config.validate_scope_for_user = proc do |user, org, scope|
|
|
574
|
+
scope != 'mcp:admin' || user.admin?
|
|
575
|
+
end
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Client Registration Policy
|
|
579
|
+
|
|
580
|
+
`/oauth/register` is open (RFC 7591), so limit what a registered client can do:
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
# Only these redirect URIs can register. Strings match exactly; Regexps must
|
|
584
|
+
# match the WHOLE URI (end with .* to allow any path).
|
|
585
|
+
config.allowed_redirect_uri_patterns = [
|
|
586
|
+
%r{https://claude\.ai/api/mcp/auth_callback}
|
|
587
|
+
]
|
|
588
|
+
config.allow_loopback_redirects = true # localhost / 127.0.0.1 / [::1], any port
|
|
589
|
+
config.verified_redirect_hosts = %w[claude.ai] # others show "Unverified application"
|
|
590
|
+
config.strict_scope_validation = true # reject unknown scopes
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Plain `http://` redirect URIs are always rejected except on loopback hosts.
|
|
594
|
+
|
|
595
|
+
### Token Signing (RS256 / ES256)
|
|
596
|
+
|
|
597
|
+
Tokens are signed with HS256 and `oauth_secret` by default. To let other
|
|
598
|
+
services verify tokens without the shared secret, use asymmetric keys. The
|
|
599
|
+
public keys are published at `/.well-known/jwks.json`:
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
config.token_signing_algorithm = 'RS256' # or 'ES256'
|
|
603
|
+
config.token_signing_private_key = ENV.fetch('MCP_JWT_PRIVATE_KEY')
|
|
604
|
+
# Key rotation: keep previous public keys so issued tokens still verify
|
|
605
|
+
config.token_signing_additional_public_keys = [ENV['MCP_JWT_PREVIOUS_PUBLIC_KEY']].compact
|
|
516
606
|
```
|
|
517
607
|
|
|
518
608
|
### Separate Authorization Server
|
|
@@ -522,6 +612,10 @@ If you want to use a separate authorization server:
|
|
|
522
612
|
```ruby
|
|
523
613
|
# config/initializers/mcp_auth.rb
|
|
524
614
|
config.authorization_server_url = 'https://auth.example.com'
|
|
615
|
+
# Public origin of THIS app's MCP endpoint (the token audience). Optional —
|
|
616
|
+
# defaults to the request origin — but set it to pin the audience and metadata
|
|
617
|
+
# URLs against a forged Host header.
|
|
618
|
+
config.mcp_server_url = 'https://api.example.com'
|
|
525
619
|
```
|
|
526
620
|
|
|
527
621
|
This is useful for:
|
|
@@ -559,18 +653,21 @@ RSpec.describe 'MCP API', type: :request do
|
|
|
559
653
|
let(:user) { create(:user) }
|
|
560
654
|
let(:org) { create(:org) }
|
|
561
655
|
|
|
656
|
+
# Request specs use the host www.example.com, and the token audience must
|
|
657
|
+
# match the resource your controller expects (mcp_server_url, or the request
|
|
658
|
+
# origin, plus mcp_server_path).
|
|
562
659
|
let(:access_token) do
|
|
563
660
|
token_data = {
|
|
564
661
|
client_id: 'test-client',
|
|
565
662
|
scope: 'mcp:read mcp:write',
|
|
566
663
|
user_id: user.id,
|
|
567
664
|
org_id: org.id,
|
|
568
|
-
resource: 'http://
|
|
665
|
+
resource: 'http://www.example.com/mcp'
|
|
569
666
|
}
|
|
570
|
-
|
|
667
|
+
|
|
571
668
|
Mcp::Auth::Services::TokenService.generate_access_token(
|
|
572
669
|
token_data,
|
|
573
|
-
base_url: 'http://
|
|
670
|
+
base_url: 'http://www.example.com'
|
|
574
671
|
)
|
|
575
672
|
end
|
|
576
673
|
|
|
@@ -606,6 +703,9 @@ rake mcp_auth:revoke_client_tokens[CLIENT_ID]
|
|
|
606
703
|
|
|
607
704
|
# Revoke all tokens for a specific user
|
|
608
705
|
rake mcp_auth:revoke_user_tokens[USER_ID]
|
|
706
|
+
|
|
707
|
+
# Check the database schema and signing secret (exits non-zero on problems)
|
|
708
|
+
rake mcp_auth:doctor
|
|
609
709
|
```
|
|
610
710
|
|
|
611
711
|
### Scheduled Cleanup
|
|
@@ -631,6 +731,9 @@ rails secret
|
|
|
631
731
|
export MCP_HMAC_SECRET="your-256-bit-secret-here"
|
|
632
732
|
```
|
|
633
733
|
|
|
734
|
+
Outside development/test, HS256 signing fails if this secret is unset or equal
|
|
735
|
+
to `secret_key_base`. `rake mcp_auth:doctor` checks it.
|
|
736
|
+
|
|
634
737
|
### 2. Always Use HTTPS in Production
|
|
635
738
|
|
|
636
739
|
```ruby
|
|
@@ -638,7 +741,18 @@ export MCP_HMAC_SECRET="your-256-bit-secret-here"
|
|
|
638
741
|
config.force_ssl = true
|
|
639
742
|
```
|
|
640
743
|
|
|
641
|
-
MCP Auth
|
|
744
|
+
MCP Auth rejects plain-HTTP requests to the OAuth endpoints outside development/test.
|
|
745
|
+
|
|
746
|
+
### Pin Your Public URLs
|
|
747
|
+
|
|
748
|
+
Discovery metadata, the token issuer and the token audience come from
|
|
749
|
+
`authorization_server_url` and `mcp_server_url`. When they're unset, the request's
|
|
750
|
+
Host header is used, so set them or restrict `config.hosts`:
|
|
751
|
+
|
|
752
|
+
```ruby
|
|
753
|
+
config.authorization_server_url = 'https://example.com'
|
|
754
|
+
config.mcp_server_url = 'https://example.com'
|
|
755
|
+
```
|
|
642
756
|
|
|
643
757
|
### 3. Keep Token Lifetimes Short
|
|
644
758
|
|
|
@@ -646,20 +760,21 @@ MCP Auth automatically enforces HTTPS for OAuth endpoints in production.
|
|
|
646
760
|
# Recommended settings
|
|
647
761
|
config.access_token_lifetime = 3600 # 1 hour
|
|
648
762
|
config.refresh_token_lifetime = 2_592_000 # 30 days
|
|
649
|
-
config.authorization_code_lifetime =
|
|
763
|
+
config.authorization_code_lifetime = 600 # 10 minutes (OAuth 2.1 guidance; default 1800)
|
|
650
764
|
```
|
|
651
765
|
|
|
652
766
|
### 4. Validate Redirect URIs
|
|
653
767
|
|
|
654
|
-
|
|
768
|
+
Redirect URIs are matched exactly at `/oauth/authorize`. Restrict which ones can
|
|
769
|
+
register with `allowed_redirect_uri_patterns` (see Client Registration Policy).
|
|
655
770
|
|
|
656
771
|
### 5. Monitor Failed Authentications
|
|
657
772
|
|
|
658
773
|
Check logs regularly for suspicious activity:
|
|
659
774
|
|
|
660
775
|
```bash
|
|
661
|
-
grep "
|
|
662
|
-
grep "
|
|
776
|
+
grep "\[OAuth\] Error" log/production.log # every OAuth error response
|
|
777
|
+
grep "Refresh-token reuse detected" log/production.log
|
|
663
778
|
```
|
|
664
779
|
|
|
665
780
|
### 6. Implement Rate Limiting
|
|
@@ -668,12 +783,30 @@ Use rack-attack or similar to prevent brute force attacks:
|
|
|
668
783
|
|
|
669
784
|
```ruby
|
|
670
785
|
# config/initializers/rack_attack.rb
|
|
671
|
-
Rack::Attack.throttle('oauth/token', limit:
|
|
786
|
+
Rack::Attack.throttle('oauth/token', limit: 20, period: 1.minute) do |req|
|
|
672
787
|
req.ip if req.path == '/oauth/token' && req.post?
|
|
673
788
|
end
|
|
789
|
+
|
|
790
|
+
# Registration is open, so throttle it hard
|
|
791
|
+
Rack::Attack.throttle('oauth/register', limit: 5, period: 1.hour) do |req|
|
|
792
|
+
req.ip if req.path == '/oauth/register' && req.post?
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
Rack::Attack.throttle('oauth/other', limit: 60, period: 1.minute) do |req|
|
|
796
|
+
req.ip if req.path.start_with?('/oauth/authorize', '/oauth/revoke', '/oauth/introspect')
|
|
797
|
+
end
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### 7. After Upgrading to 0.6.0
|
|
801
|
+
|
|
802
|
+
Once `rails g mcp:auth:hash_secrets && rails db:migrate` has run, turn off the
|
|
803
|
+
transitional plaintext lookup:
|
|
804
|
+
|
|
805
|
+
```ruby
|
|
806
|
+
config.secret_dual_read = false
|
|
674
807
|
```
|
|
675
808
|
|
|
676
|
-
###
|
|
809
|
+
### 8. Regular Token Cleanup
|
|
677
810
|
|
|
678
811
|
Run cleanup task daily to remove expired tokens:
|
|
679
812
|
|
|
@@ -722,7 +855,8 @@ resource: 'https://example.com/mcp' # Must match mcp_server_path
|
|
|
722
855
|
**Problem**: Valid-looking tokens are rejected
|
|
723
856
|
|
|
724
857
|
**Solutions**:
|
|
725
|
-
- Check `oauth_secret` is set correctly and consistently
|
|
858
|
+
- Check `oauth_secret` is set correctly and consistently on every server
|
|
859
|
+
- Check `mcp_server_url` (or the request host) and `mcp_server_path` match the token's `aud`
|
|
726
860
|
- Ensure server clocks are synchronized (JWT exp validation is time-sensitive)
|
|
727
861
|
- Verify token hasn't expired (check `exp` claim)
|
|
728
862
|
- Check token audience matches your MCP server
|
|
@@ -734,9 +868,10 @@ ruby -rjwt -e "puts JWT.decode('YOUR_TOKEN', nil, false).inspect"
|
|
|
734
868
|
|
|
735
869
|
### "Missing template" Errors
|
|
736
870
|
|
|
737
|
-
**Problem**: Missing template
|
|
871
|
+
**Problem**: Missing template errors when the consent screen renders
|
|
738
872
|
|
|
739
|
-
**Solution**:
|
|
873
|
+
**Solution**: With `use_custom_consent_view = true` the page renders inside your
|
|
874
|
+
`application` layout, so that layout must exist. Otherwise, regenerate the view:
|
|
740
875
|
|
|
741
876
|
```bash
|
|
742
877
|
rails generate mcp:auth:install
|
|
@@ -789,7 +924,7 @@ MCP Auth implements the following specifications:
|
|
|
789
924
|
- [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) - Authorization Server Metadata
|
|
790
925
|
- [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) - Resource Indicators for OAuth 2.0
|
|
791
926
|
- [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) - OAuth 2.0 Protected Resource Metadata
|
|
792
|
-
- [MCP Authorization Spec](https://modelcontextprotocol.io/specification/
|
|
927
|
+
- [MCP Authorization Spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) - Model Context Protocol Authorization
|
|
793
928
|
|
|
794
929
|
## Development
|
|
795
930
|
|