mcp-auth 0.1.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 +7 -0
- data/CHANGELOG.md +43 -0
- data/CONTRIBUTING.md +107 -0
- data/LICENSE.txt +21 -0
- data/README.md +869 -0
- data/Rakefile +8 -0
- data/app/controllers/mcp/auth/oauth_controller.rb +494 -0
- data/app/controllers/mcp/auth/well_known_controller.rb +147 -0
- data/app/models/mcp/auth/access_token.rb +30 -0
- data/app/models/mcp/auth/authorization_code.rb +33 -0
- data/app/models/mcp/auth/oauth_client.rb +60 -0
- data/app/models/mcp/auth/refresh_token.rb +32 -0
- data/app/views/mcp/auth/consent.html.erb +527 -0
- data/config/routes.rb +43 -0
- data/lib/generators/mcp/auth/install_generator.rb +80 -0
- data/lib/generators/mcp/auth/templates/README +114 -0
- data/lib/generators/mcp/auth/templates/create_access_tokens.rb.erb +23 -0
- data/lib/generators/mcp/auth/templates/create_authorization_codes.rb.erb +26 -0
- data/lib/generators/mcp/auth/templates/create_oauth_clients.rb.erb +22 -0
- data/lib/generators/mcp/auth/templates/create_refresh_tokens.rb.erb +22 -0
- data/lib/generators/mcp/auth/templates/initializer.rb +199 -0
- data/lib/generators/mcp/auth/templates/views/consent.html.erb +527 -0
- data/lib/mcp/auth/engine.rb +32 -0
- data/lib/mcp/auth/scope_registry.rb +113 -0
- data/lib/mcp/auth/services/authorization_service.rb +102 -0
- data/lib/mcp/auth/services/token_service.rb +230 -0
- data/lib/mcp/auth/version.rb +7 -0
- data/lib/mcp/auth.rb +109 -0
- data/lib/tasks/mcp_auth_tasks.rake +89 -0
- metadata +254 -0
data/README.md
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
# MCP Auth
|
|
2
|
+
|
|
3
|
+
OAuth 2.1 authorization for Model Context Protocol (MCP) servers in Rails applications.
|
|
4
|
+
|
|
5
|
+
## What is MCP Authorization?
|
|
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/2025-06-18/basic/authorization), providing OAuth 2.1-based authentication for MCP servers.
|
|
8
|
+
|
|
9
|
+
### Why OAuth for MCP?
|
|
10
|
+
|
|
11
|
+
MCP servers often need to access user data or perform actions on behalf of users. OAuth 2.1 provides:
|
|
12
|
+
|
|
13
|
+
- **Delegated Authorization**: Users can grant MCP clients access without sharing credentials
|
|
14
|
+
- **Scope-Based Permissions**: Fine-grained control over what data/actions are allowed
|
|
15
|
+
- **Token-Based Security**: Short-lived access tokens with automatic refresh
|
|
16
|
+
- **PKCE Protection**: Enhanced security for public clients (AI assistants)
|
|
17
|
+
|
|
18
|
+
## How MCP Auth Works
|
|
19
|
+
|
|
20
|
+
### Architecture Overview
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
24
|
+
│ │ │ │ │ │
|
|
25
|
+
│ MCP Client │────────▶│ Your Rails App │◀────────│ End User │
|
|
26
|
+
│ (AI Assistant) │ │ (MCP Server + │ │ (Browser) │
|
|
27
|
+
│ │ │ OAuth Server) │ │ │
|
|
28
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
29
|
+
│ │ │
|
|
30
|
+
│ │ │
|
|
31
|
+
│ 1. Discover OAuth Server │ │
|
|
32
|
+
│────────────────────────────▶│ │
|
|
33
|
+
│ │ │
|
|
34
|
+
│ 2. Request Authorization │ │
|
|
35
|
+
│────────────────────────────▶│ │
|
|
36
|
+
│ │ │
|
|
37
|
+
│ │ 3. Show Consent Screen │
|
|
38
|
+
│ │────────────────────────────▶│
|
|
39
|
+
│ │ │
|
|
40
|
+
│ │ 4. User Approves │
|
|
41
|
+
│ │◀────────────────────────────│
|
|
42
|
+
│ │ │
|
|
43
|
+
│ 5. Receive Auth Code │ │
|
|
44
|
+
│◀────────────────────────────│ │
|
|
45
|
+
│ │ │
|
|
46
|
+
│ 6. Exchange for Token │ │
|
|
47
|
+
│────────────────────────────▶│ │
|
|
48
|
+
│ │ │
|
|
49
|
+
│ 7. Access MCP Server │ │
|
|
50
|
+
│────────────────────────────▶│ │
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### OAuth 2.1 Flow Sequence
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
sequenceDiagram
|
|
57
|
+
participant Client as MCP Client
|
|
58
|
+
participant Server as MCP Server
|
|
59
|
+
participant AuthServer as OAuth Server
|
|
60
|
+
participant User as End User
|
|
61
|
+
|
|
62
|
+
Note over Client,User: Discovery Phase
|
|
63
|
+
Client->>Server: Request without token
|
|
64
|
+
Server-->>Client: 401 + WWW-Authenticate header
|
|
65
|
+
Client->>Server: GET /.well-known/oauth-protected-resource
|
|
66
|
+
Server-->>Client: Protected Resource Metadata
|
|
67
|
+
Client->>AuthServer: GET /.well-known/oauth-authorization-server
|
|
68
|
+
AuthServer-->>Client: OAuth Server Metadata
|
|
69
|
+
|
|
70
|
+
Note over Client,User: Authorization Phase
|
|
71
|
+
Client->>Client: Generate PKCE challenge
|
|
72
|
+
Client->>User: Open browser with authorization URL
|
|
73
|
+
User->>AuthServer: Authorization request (+ PKCE challenge)
|
|
74
|
+
AuthServer->>User: Show consent screen
|
|
75
|
+
User->>AuthServer: Approve access
|
|
76
|
+
AuthServer-->>User: Redirect with authorization code
|
|
77
|
+
User->>Client: Return authorization code
|
|
78
|
+
|
|
79
|
+
Note over Client,User: Token Exchange
|
|
80
|
+
Client->>AuthServer: Exchange code for token (+ PKCE verifier)
|
|
81
|
+
AuthServer->>AuthServer: Validate PKCE
|
|
82
|
+
AuthServer-->>Client: Access token + Refresh token
|
|
83
|
+
|
|
84
|
+
Note over Client,User: Access MCP Resources
|
|
85
|
+
Client->>Server: MCP request + Bearer token
|
|
86
|
+
Server->>Server: Validate token audience
|
|
87
|
+
Server-->>Client: MCP response
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Key Security Features
|
|
91
|
+
|
|
92
|
+
1. **PKCE (Proof Key for Code Exchange)**: Required for all authorization flows
|
|
93
|
+
- Prevents authorization code interception
|
|
94
|
+
- Secure for public clients (AI assistants)
|
|
95
|
+
|
|
96
|
+
2. **Resource Indicators (RFC 8707)**: Token audience binding
|
|
97
|
+
- Tokens are explicitly bound to your MCP server
|
|
98
|
+
- Prevents token misuse across different services
|
|
99
|
+
|
|
100
|
+
3. **Token Rotation**: OAuth 2.1 requirement
|
|
101
|
+
- Refresh tokens are rotated on each use
|
|
102
|
+
- Limits impact of token theft
|
|
103
|
+
|
|
104
|
+
4. **Short-Lived Access Tokens**: Default 1 hour lifetime
|
|
105
|
+
- Reduces security window for stolen tokens
|
|
106
|
+
- Automatic refresh via refresh tokens
|
|
107
|
+
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
- ✅ **OAuth 2.1 Compliant** - Latest draft specification
|
|
111
|
+
- ✅ **PKCE Required** - S256 method for enhanced security
|
|
112
|
+
- ✅ **Dynamic Client Registration** - RFC 7591 support
|
|
113
|
+
- ✅ **Resource Indicators** - RFC 8707 for token audience binding
|
|
114
|
+
- ✅ **Protected Resource Metadata** - RFC 9728 for server discovery
|
|
115
|
+
- ✅ **Token Revocation** - RFC 7009 support
|
|
116
|
+
- ✅ **Token Introspection** - RFC 7662 support
|
|
117
|
+
- ✅ **OpenID Connect** - Basic OIDC Discovery support
|
|
118
|
+
- ✅ **Refresh Token Rotation** - OAuth 2.1 security requirement
|
|
119
|
+
- ✅ **Fully Configurable** - Paths, URLs, lifetimes, and user data
|
|
120
|
+
- ✅ **Beautiful Consent Screen** - Customizable UI
|
|
121
|
+
|
|
122
|
+
## Installation
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Add to Gemfile
|
|
126
|
+
gem 'mcp-auth'
|
|
127
|
+
|
|
128
|
+
# Install
|
|
129
|
+
bundle install
|
|
130
|
+
|
|
131
|
+
# Generate migrations and config
|
|
132
|
+
rails generate mcp:auth:install
|
|
133
|
+
|
|
134
|
+
# Run migrations
|
|
135
|
+
rails db:migrate
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Quick Start
|
|
139
|
+
|
|
140
|
+
### 1. Mount Routes
|
|
141
|
+
|
|
142
|
+
**CRITICAL**: Mount at the TOP of `config/routes.rb`:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
Rails.application.routes.draw do
|
|
146
|
+
# Mount MCP Auth FIRST - before any catch-all routes
|
|
147
|
+
mount Mcp::Auth::Engine => '/'
|
|
148
|
+
|
|
149
|
+
# Then your other routes
|
|
150
|
+
devise_for :users
|
|
151
|
+
root to: 'dashboard#index'
|
|
152
|
+
|
|
153
|
+
# ... rest of your routes
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
⚠️ **Why at the top?** The gem's routes (like `/.well-known/oauth-*` and `/oauth/*`) need to be registered before any catch-all routes or they'll be intercepted by your app's routing.
|
|
158
|
+
|
|
159
|
+
### 2. Configure MCP Auth
|
|
160
|
+
|
|
161
|
+
Edit `config/initializers/mcp_auth.rb`:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
Mcp::Auth.configure do |config|
|
|
165
|
+
# OAuth secret for JWT signing
|
|
166
|
+
config.oauth_secret = ENV.fetch('MCP_HMAC_SECRET', Rails.application.secret_key_base)
|
|
167
|
+
|
|
168
|
+
# Authorization server URL (optional - defaults to same as resource server)
|
|
169
|
+
config.authorization_server_url = ENV.fetch('MCP_AUTHORIZATION_SERVER_URL', nil)
|
|
170
|
+
|
|
171
|
+
# MCP Server Path - where your MCP server is mounted
|
|
172
|
+
# Change this if your MCP server is NOT at '/mcp'
|
|
173
|
+
config.mcp_server_path = ENV.fetch('MCP_SERVER_PATH', '/mcp')
|
|
174
|
+
|
|
175
|
+
# MCP Documentation URL (optional)
|
|
176
|
+
# Default: {mcp_server_path}/docs (e.g., /mcp/docs)
|
|
177
|
+
# Can be a path or full URL:
|
|
178
|
+
config.mcp_docs_url = ENV.fetch('MCP_DOCS_URL', nil)
|
|
179
|
+
# Examples:
|
|
180
|
+
# config.mcp_docs_url = '/docs/mcp-api'
|
|
181
|
+
# config.mcp_docs_url = 'https://docs.example.com/mcp'
|
|
182
|
+
|
|
183
|
+
# Token lifetimes (in seconds)
|
|
184
|
+
config.access_token_lifetime = 3600 # 1 hour
|
|
185
|
+
config.refresh_token_lifetime = 2_592_000 # 30 days
|
|
186
|
+
config.authorization_code_lifetime = 1800 # 30 minutes
|
|
187
|
+
|
|
188
|
+
# User data fetcher - CUSTOMIZE THIS
|
|
189
|
+
config.fetch_user_data = proc do |data|
|
|
190
|
+
user = User.find(data[:user_id])
|
|
191
|
+
org = Org.find(data[:org_id]) if data[:org_id]
|
|
192
|
+
|
|
193
|
+
# Return user data + API key (if you have one)
|
|
194
|
+
{
|
|
195
|
+
email: user.email,
|
|
196
|
+
api_key_id: org&.api_key&.id,
|
|
197
|
+
api_key_secret: org&.api_key&.secret
|
|
198
|
+
}
|
|
199
|
+
rescue ActiveRecord::RecordNotFound
|
|
200
|
+
{ email: 'unknown@example.com', api_key_id: nil, api_key_secret: nil }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Methods for authentication
|
|
204
|
+
config.current_user_method = :current_user
|
|
205
|
+
config.current_org_method = :current_org
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 3. Ensure Authentication Methods
|
|
210
|
+
|
|
211
|
+
Your `ApplicationController` should have these methods:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class ApplicationController < ActionController::Base
|
|
215
|
+
# For Devise users, these are already defined
|
|
216
|
+
# For custom auth, implement these methods:
|
|
217
|
+
|
|
218
|
+
def current_user
|
|
219
|
+
# Your logic to get the currently logged-in user
|
|
220
|
+
@current_user ||= User.find_by(id: session[:user_id])
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def current_org
|
|
224
|
+
# Your logic to get the current organization (if applicable)
|
|
225
|
+
@current_org ||= current_user&.current_org
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 4. Set Environment Variables
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# .env
|
|
234
|
+
MCP_HMAC_SECRET=your_secure_random_string_here
|
|
235
|
+
MCP_SERVER_PATH=/mcp # or /api/mcp, /assistant/api, etc.
|
|
236
|
+
MCP_DOCS_URL=/docs/mcp # optional
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 5. Restart Server
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
spring stop # Clear spring cache
|
|
243
|
+
rails server
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## OAuth Endpoints (Automatic)
|
|
247
|
+
|
|
248
|
+
MCP Auth provides these endpoints automatically:
|
|
249
|
+
|
|
250
|
+
### Discovery Endpoints
|
|
251
|
+
|
|
252
|
+
- `GET /.well-known/oauth-protected-resource` - RFC 9728 Protected Resource Metadata
|
|
253
|
+
- `GET /.well-known/oauth-authorization-server` - RFC 8414 Authorization Server Metadata
|
|
254
|
+
- `GET /.well-known/openid-configuration` - OpenID Connect Discovery
|
|
255
|
+
- `GET /.well-known/jwks.json` - JSON Web Key Set (empty for HMAC)
|
|
256
|
+
|
|
257
|
+
### OAuth Flow Endpoints
|
|
258
|
+
|
|
259
|
+
- `GET/POST /oauth/authorize` - Authorization endpoint (PKCE required)
|
|
260
|
+
- `POST /oauth/approve` - Consent approval endpoint
|
|
261
|
+
- `POST /oauth/token` - Token endpoint (authorization_code, refresh_token)
|
|
262
|
+
- `POST /oauth/register` - Dynamic client registration (RFC 7591)
|
|
263
|
+
- `POST /oauth/revoke` - Token revocation (RFC 7009)
|
|
264
|
+
- `POST /oauth/introspect` - Token introspection (RFC 7662)
|
|
265
|
+
- `GET /oauth/userinfo` - OpenID Connect UserInfo endpoint
|
|
266
|
+
|
|
267
|
+
## Usage
|
|
268
|
+
|
|
269
|
+
### Protecting Your MCP Server
|
|
270
|
+
|
|
271
|
+
MCP Auth automatically protects routes matching your configured `mcp_server_path`:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
# If mcp_server_path = '/mcp'
|
|
275
|
+
# All routes starting with /mcp/* require OAuth tokens
|
|
276
|
+
|
|
277
|
+
GET /mcp/tools # Protected ✅
|
|
278
|
+
GET /mcp/resources # Protected ✅
|
|
279
|
+
GET /mcp/prompts # Protected ✅
|
|
280
|
+
GET /other/endpoint # Not protected ❌
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### OAuth 2.1 Authorization Flow
|
|
284
|
+
|
|
285
|
+
#### 1. Register a Client (Dynamic Registration)
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
curl -X POST http://localhost:3000/oauth/register \
|
|
289
|
+
-H "Content-Type: application/json" \
|
|
290
|
+
-d '{
|
|
291
|
+
"client_name": "My MCP Client",
|
|
292
|
+
"redirect_uris": ["https://client.example.com/callback"],
|
|
293
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
294
|
+
"response_types": ["code"],
|
|
295
|
+
"scope": "mcp:read mcp:write"
|
|
296
|
+
}'
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Response:
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"client_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
303
|
+
"client_secret": "a1b2c3d4...",
|
|
304
|
+
"client_id_issued_at": 1234567890,
|
|
305
|
+
"client_secret_expires_at": 0,
|
|
306
|
+
"redirect_uris": ["https://client.example.com/callback"],
|
|
307
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
308
|
+
"response_types": ["code"],
|
|
309
|
+
"scope": "mcp:read mcp:write"
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### 2. Authorization Request with PKCE
|
|
314
|
+
|
|
315
|
+
Generate PKCE parameters:
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
// Generate code verifier (43-128 characters)
|
|
319
|
+
const codeVerifier = base64URLEncode(randomBytes(32));
|
|
320
|
+
|
|
321
|
+
// Generate code challenge
|
|
322
|
+
const codeChallenge = base64URLEncode(
|
|
323
|
+
sha256(codeVerifier)
|
|
324
|
+
);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Redirect user to authorization URL:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
GET /oauth/authorize?
|
|
331
|
+
response_type=code&
|
|
332
|
+
client_id=550e8400-e29b-41d4-a716-446655440000&
|
|
333
|
+
redirect_uri=https://client.example.com/callback&
|
|
334
|
+
scope=mcp:read+mcp:write&
|
|
335
|
+
state=random_state_string&
|
|
336
|
+
code_challenge=CODE_CHALLENGE&
|
|
337
|
+
code_challenge_method=S256&
|
|
338
|
+
resource=https://example.com/mcp
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
User will see consent screen and approve/deny access.
|
|
342
|
+
|
|
343
|
+
#### 3. Token Exchange
|
|
344
|
+
|
|
345
|
+
After user approves, exchange authorization code for tokens:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
curl -X POST https://example.com/oauth/token \
|
|
349
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
350
|
+
-d "grant_type=authorization_code" \
|
|
351
|
+
-d "code=AUTHORIZATION_CODE" \
|
|
352
|
+
-d "redirect_uri=https://client.example.com/callback" \
|
|
353
|
+
-d "code_verifier=CODE_VERIFIER" \
|
|
354
|
+
-d "client_id=550e8400-e29b-41d4-a716-446655440000" \
|
|
355
|
+
-d "resource=https://example.com/mcp"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Response:
|
|
359
|
+
```json
|
|
360
|
+
{
|
|
361
|
+
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
362
|
+
"token_type": "Bearer",
|
|
363
|
+
"expires_in": 3600,
|
|
364
|
+
"refresh_token": "a1b2c3d4e5f6...",
|
|
365
|
+
"scope": "mcp:read mcp:write"
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### 4. Access Protected Resources
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
curl https://example.com/mcp/tools \
|
|
373
|
+
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### 5. Refresh Token
|
|
377
|
+
|
|
378
|
+
When access token expires:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
curl -X POST https://example.com/oauth/token \
|
|
382
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
383
|
+
-d "grant_type=refresh_token" \
|
|
384
|
+
-d "refresh_token=a1b2c3d4e5f6..." \
|
|
385
|
+
-d "client_id=550e8400-e29b-41d4-a716-446655440000"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Helper Methods in Controllers
|
|
389
|
+
|
|
390
|
+
Access authentication data in your controllers:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
class MyController < ApplicationController
|
|
394
|
+
def index
|
|
395
|
+
if mcp_authenticated?
|
|
396
|
+
user_id = mcp_user_id # User ID from token
|
|
397
|
+
org_id = mcp_org_id # Org ID from token
|
|
398
|
+
email = mcp_email # Email from token
|
|
399
|
+
scope = mcp_scope # Token scopes
|
|
400
|
+
api_key = mcp_api_key # API key if configured
|
|
401
|
+
|
|
402
|
+
# Your logic here
|
|
403
|
+
render json: { user_id: user_id, email: email }
|
|
404
|
+
else
|
|
405
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Available helper methods:
|
|
412
|
+
|
|
413
|
+
- `mcp_authenticated?` - Returns true if request has valid token
|
|
414
|
+
- `mcp_user_id` - User ID from token
|
|
415
|
+
- `mcp_org_id` - Organization ID from token
|
|
416
|
+
- `mcp_email` - User email from token
|
|
417
|
+
- `mcp_scope` - Token scopes (space-separated string)
|
|
418
|
+
- `mcp_token` - The access token itself
|
|
419
|
+
- `mcp_api_key` - API key if configured in `fetch_user_data`
|
|
420
|
+
|
|
421
|
+
## Advanced Configuration
|
|
422
|
+
|
|
423
|
+
### Custom MCP Server Paths
|
|
424
|
+
|
|
425
|
+
If your MCP server is mounted at a different path:
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
# config/initializers/mcp_auth.rb
|
|
429
|
+
config.mcp_server_path = '/api/v1/assistant' # Custom path
|
|
430
|
+
|
|
431
|
+
# Your MCP server configuration
|
|
432
|
+
FastMcp.mount_in_rails(
|
|
433
|
+
Rails.application,
|
|
434
|
+
path_prefix: '/api/v1/assistant' # Must match mcp_server_path
|
|
435
|
+
)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Custom Documentation URL
|
|
439
|
+
|
|
440
|
+
Point to your API documentation:
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
# Path-based (will be prefixed with your domain)
|
|
444
|
+
config.mcp_docs_url = '/docs/mcp-api'
|
|
445
|
+
|
|
446
|
+
# Full URL to external docs
|
|
447
|
+
config.mcp_docs_url = 'https://docs.example.com/mcp-api'
|
|
448
|
+
|
|
449
|
+
# Default (if not set): {mcp_server_path}/docs
|
|
450
|
+
# Example: /mcp/docs
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Custom Consent Screen
|
|
454
|
+
|
|
455
|
+
Customize the OAuth consent screen to match your branding:
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
# 1. Enable custom consent view in config/initializers/mcp_auth.rb
|
|
459
|
+
config.use_custom_consent_view = true
|
|
460
|
+
|
|
461
|
+
# 2. Edit app/views/mcp/auth/consent.html.erb
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Available variables in the view:
|
|
465
|
+
|
|
466
|
+
- `@client_name` - Name of the OAuth client requesting access
|
|
467
|
+
- `@requested_scopes` - Array of human-readable scope descriptions
|
|
468
|
+
- `@authorization_params` - OAuth parameters (client_id, redirect_uri, etc.)
|
|
469
|
+
|
|
470
|
+
Example custom consent view:
|
|
471
|
+
|
|
472
|
+
```erb
|
|
473
|
+
<!DOCTYPE html>
|
|
474
|
+
<html>
|
|
475
|
+
<head>
|
|
476
|
+
<title>Authorization Request</title>
|
|
477
|
+
<style>
|
|
478
|
+
/* Your custom styles */
|
|
479
|
+
</style>
|
|
480
|
+
</head>
|
|
481
|
+
<body>
|
|
482
|
+
<div class="consent-container">
|
|
483
|
+
<h1><%= @client_name %> wants to access your account</h1>
|
|
484
|
+
|
|
485
|
+
<p>This application is requesting permission to:</p>
|
|
486
|
+
<ul>
|
|
487
|
+
<% @requested_scopes.each do |scope| %>
|
|
488
|
+
<li><%= scope %></li>
|
|
489
|
+
<% end %>
|
|
490
|
+
</ul>
|
|
491
|
+
|
|
492
|
+
<%= form_tag oauth_approve_path, method: :post do %>
|
|
493
|
+
<% @authorization_params.each do |key, value| %>
|
|
494
|
+
<%= hidden_field_tag key, value %>
|
|
495
|
+
<% end %>
|
|
496
|
+
|
|
497
|
+
<%= hidden_field_tag :approved, true %>
|
|
498
|
+
<%= submit_tag "Allow Access", class: "btn-primary" %>
|
|
499
|
+
<% end %>
|
|
500
|
+
|
|
501
|
+
<%= form_tag oauth_approve_path, method: :post do %>
|
|
502
|
+
<% @authorization_params.each do |key, value| %>
|
|
503
|
+
<%= hidden_field_tag key, value %>
|
|
504
|
+
<% end %>
|
|
505
|
+
|
|
506
|
+
<%= hidden_field_tag :approved, false %>
|
|
507
|
+
<%= submit_tag "Deny", class: "btn-secondary" %>
|
|
508
|
+
<% end %>
|
|
509
|
+
</div>
|
|
510
|
+
</body>
|
|
511
|
+
</html>
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Separate Authorization Server
|
|
515
|
+
|
|
516
|
+
If you want to use a separate authorization server:
|
|
517
|
+
|
|
518
|
+
```ruby
|
|
519
|
+
# config/initializers/mcp_auth.rb
|
|
520
|
+
config.authorization_server_url = 'https://auth.example.com'
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
This is useful for:
|
|
524
|
+
- Microservices architecture
|
|
525
|
+
- Multiple resource servers sharing one auth server
|
|
526
|
+
- Enterprise SSO integration
|
|
527
|
+
|
|
528
|
+
## Testing
|
|
529
|
+
|
|
530
|
+
### Test the Installation
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Start your server
|
|
534
|
+
rails server
|
|
535
|
+
|
|
536
|
+
# Test discovery endpoints
|
|
537
|
+
curl http://localhost:3000/.well-known/oauth-protected-resource
|
|
538
|
+
curl http://localhost:3000/.well-known/oauth-authorization-server
|
|
539
|
+
|
|
540
|
+
# Register a test client
|
|
541
|
+
curl -X POST http://localhost:3000/oauth/register \
|
|
542
|
+
-H "Content-Type: application/json" \
|
|
543
|
+
-d '{
|
|
544
|
+
"client_name": "Test Client",
|
|
545
|
+
"redirect_uris": ["http://localhost:3000/callback"]
|
|
546
|
+
}'
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### In Your Test Suite
|
|
550
|
+
|
|
551
|
+
Create tokens directly in tests:
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
RSpec.describe 'MCP API', type: :request do
|
|
555
|
+
let(:user) { create(:user) }
|
|
556
|
+
let(:org) { create(:org) }
|
|
557
|
+
|
|
558
|
+
let(:access_token) do
|
|
559
|
+
token_data = {
|
|
560
|
+
client_id: 'test-client',
|
|
561
|
+
scope: 'mcp:read mcp:write',
|
|
562
|
+
user_id: user.id,
|
|
563
|
+
org_id: org.id,
|
|
564
|
+
resource: 'http://localhost:3000/mcp'
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
Mcp::Auth::Services::TokenService.generate_access_token(
|
|
568
|
+
token_data,
|
|
569
|
+
base_url: 'http://localhost:3000'
|
|
570
|
+
)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it 'allows authenticated requests' do
|
|
574
|
+
get '/mcp/tools', headers: {
|
|
575
|
+
'Authorization' => "Bearer #{access_token}"
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
expect(response).to have_http_status(:success)
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
it 'rejects unauthenticated requests' do
|
|
582
|
+
get '/mcp/tools'
|
|
583
|
+
|
|
584
|
+
expect(response).to have_http_status(:unauthorized)
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## Rake Tasks
|
|
590
|
+
|
|
591
|
+
MCP Auth includes helpful maintenance tasks:
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
# Clean up expired tokens and codes (run daily)
|
|
595
|
+
rake mcp_auth:cleanup
|
|
596
|
+
|
|
597
|
+
# Show token statistics
|
|
598
|
+
rake mcp_auth:stats
|
|
599
|
+
|
|
600
|
+
# Revoke all tokens for a specific client
|
|
601
|
+
rake mcp_auth:revoke_client_tokens[CLIENT_ID]
|
|
602
|
+
|
|
603
|
+
# Revoke all tokens for a specific user
|
|
604
|
+
rake mcp_auth:revoke_user_tokens[USER_ID]
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Scheduled Cleanup
|
|
608
|
+
|
|
609
|
+
Add to your scheduler (cron, whenever, sidekiq-cron):
|
|
610
|
+
|
|
611
|
+
```ruby
|
|
612
|
+
# config/schedule.rb (whenever gem)
|
|
613
|
+
every 1.day, at: '3:00 am' do
|
|
614
|
+
rake 'mcp_auth:cleanup'
|
|
615
|
+
end
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## Security Best Practices
|
|
619
|
+
|
|
620
|
+
### 1. Use Strong Secrets
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
# Generate a strong secret
|
|
624
|
+
rails secret
|
|
625
|
+
|
|
626
|
+
# Set in environment
|
|
627
|
+
export MCP_HMAC_SECRET="your-256-bit-secret-here"
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### 2. Always Use HTTPS in Production
|
|
631
|
+
|
|
632
|
+
```ruby
|
|
633
|
+
# config/environments/production.rb
|
|
634
|
+
config.force_ssl = true
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
MCP Auth automatically enforces HTTPS for OAuth endpoints in production.
|
|
638
|
+
|
|
639
|
+
### 3. Keep Token Lifetimes Short
|
|
640
|
+
|
|
641
|
+
```ruby
|
|
642
|
+
# Recommended settings
|
|
643
|
+
config.access_token_lifetime = 3600 # 1 hour
|
|
644
|
+
config.refresh_token_lifetime = 2_592_000 # 30 days
|
|
645
|
+
config.authorization_code_lifetime = 1800 # 30 minutes
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### 4. Validate Redirect URIs
|
|
649
|
+
|
|
650
|
+
Only register trusted redirect URIs for your OAuth clients. MCP Auth validates exact URI matches.
|
|
651
|
+
|
|
652
|
+
### 5. Monitor Failed Authentications
|
|
653
|
+
|
|
654
|
+
Check logs regularly for suspicious activity:
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
grep "Token validation failed" log/production.log
|
|
658
|
+
grep "Authorization code is invalid" log/production.log
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### 6. Implement Rate Limiting
|
|
662
|
+
|
|
663
|
+
Use rack-attack or similar to prevent brute force attacks:
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
# config/initializers/rack_attack.rb
|
|
667
|
+
Rack::Attack.throttle('oauth/token', limit: 5, period: 1.minute) do |req|
|
|
668
|
+
req.ip if req.path == '/oauth/token' && req.post?
|
|
669
|
+
end
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### 7. Regular Token Cleanup
|
|
673
|
+
|
|
674
|
+
Run cleanup task daily to remove expired tokens:
|
|
675
|
+
|
|
676
|
+
```bash
|
|
677
|
+
rake mcp_auth:cleanup
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
## Troubleshooting
|
|
681
|
+
|
|
682
|
+
### Routes Return 404
|
|
683
|
+
|
|
684
|
+
**Problem**: OAuth endpoints return 404 or redirect to login
|
|
685
|
+
|
|
686
|
+
**Solution**: Ensure `mount Mcp::Auth::Engine => '/'` is at the very top of `config/routes.rb`, before any other routes (especially catch-all routes or Devise).
|
|
687
|
+
|
|
688
|
+
```ruby
|
|
689
|
+
Rails.application.routes.draw do
|
|
690
|
+
# THIS MUST BE FIRST!
|
|
691
|
+
mount Mcp::Auth::Engine => '/'
|
|
692
|
+
|
|
693
|
+
# Then other routes...
|
|
694
|
+
devise_for :users
|
|
695
|
+
# ...
|
|
696
|
+
end
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### "Invalid audience" Errors
|
|
700
|
+
|
|
701
|
+
**Problem**: Token validation fails with audience mismatch
|
|
702
|
+
|
|
703
|
+
**Solutions**:
|
|
704
|
+
- Check `config.mcp_server_path` matches your actual MCP server mount point
|
|
705
|
+
- Verify the `resource` parameter in OAuth requests matches the configured path
|
|
706
|
+
- Ensure tokens are being generated with the correct resource URL
|
|
707
|
+
|
|
708
|
+
```ruby
|
|
709
|
+
# config/initializers/mcp_auth.rb
|
|
710
|
+
config.mcp_server_path = '/mcp' # Must match FastMCP mount point
|
|
711
|
+
|
|
712
|
+
# When generating tokens
|
|
713
|
+
resource: 'https://example.com/mcp' # Must match mcp_server_path
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Token Validation Fails
|
|
717
|
+
|
|
718
|
+
**Problem**: Valid-looking tokens are rejected
|
|
719
|
+
|
|
720
|
+
**Solutions**:
|
|
721
|
+
- Check `oauth_secret` is set correctly and consistently
|
|
722
|
+
- Ensure server clocks are synchronized (JWT exp validation is time-sensitive)
|
|
723
|
+
- Verify token hasn't expired (check `exp` claim)
|
|
724
|
+
- Check token audience matches your MCP server
|
|
725
|
+
|
|
726
|
+
```bash
|
|
727
|
+
# Decode JWT to inspect claims (without verification)
|
|
728
|
+
ruby -rjwt -e "puts JWT.decode('YOUR_TOKEN', nil, false).inspect"
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### "Missing template" Errors
|
|
732
|
+
|
|
733
|
+
**Problem**: Missing template layouts/mcp_auth or consent view errors
|
|
734
|
+
|
|
735
|
+
**Solution**: Run the generator to create views:
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
rails generate mcp:auth:install
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### PKCE Validation Fails
|
|
742
|
+
|
|
743
|
+
**Problem**: "PKCE validation failed" error during token exchange
|
|
744
|
+
|
|
745
|
+
**Solutions**:
|
|
746
|
+
- Ensure `code_verifier` exactly matches what was used to generate `code_challenge`
|
|
747
|
+
- Verify code_challenge_method is 'S256'
|
|
748
|
+
- Check code_verifier is 43-128 characters long
|
|
749
|
+
- Ensure proper base64url encoding (no padding)
|
|
750
|
+
|
|
751
|
+
### Current User Not Found
|
|
752
|
+
|
|
753
|
+
**Problem**: `undefined method 'current_user'` errors
|
|
754
|
+
|
|
755
|
+
**Solution**: Ensure your ApplicationController defines these methods:
|
|
756
|
+
|
|
757
|
+
```ruby
|
|
758
|
+
class ApplicationController < ActionController::Base
|
|
759
|
+
def current_user
|
|
760
|
+
# Your authentication logic
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def current_org
|
|
764
|
+
# Your organization logic (optional)
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
Or configure different method names:
|
|
770
|
+
|
|
771
|
+
```ruby
|
|
772
|
+
config.current_user_method = :authenticated_user
|
|
773
|
+
config.current_org_method = :active_organization
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
## Standards Compliance
|
|
777
|
+
|
|
778
|
+
MCP Auth implements the following specifications:
|
|
779
|
+
|
|
780
|
+
- [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13) - Core authorization framework
|
|
781
|
+
- [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) - Dynamic Client Registration Protocol
|
|
782
|
+
- [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) - Proof Key for Code Exchange (PKCE)
|
|
783
|
+
- [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009) - Token Revocation
|
|
784
|
+
- [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) - Token Introspection
|
|
785
|
+
- [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) - Authorization Server Metadata
|
|
786
|
+
- [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) - Resource Indicators for OAuth 2.0
|
|
787
|
+
- [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) - OAuth 2.0 Protected Resource Metadata
|
|
788
|
+
- [MCP Authorization Spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) - Model Context Protocol Authorization
|
|
789
|
+
|
|
790
|
+
## Development
|
|
791
|
+
|
|
792
|
+
### Setting Up Development Environment
|
|
793
|
+
|
|
794
|
+
```bash
|
|
795
|
+
# Clone the repository
|
|
796
|
+
git clone https://github.com/SerhiiBorozenets/mcp-auth.git
|
|
797
|
+
cd mcp-auth
|
|
798
|
+
|
|
799
|
+
# Install dependencies
|
|
800
|
+
bundle install
|
|
801
|
+
|
|
802
|
+
# Run tests
|
|
803
|
+
bundle exec rspec
|
|
804
|
+
|
|
805
|
+
# Check code coverage
|
|
806
|
+
open coverage/index.html
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Running Tests
|
|
810
|
+
|
|
811
|
+
```bash
|
|
812
|
+
# Run all tests
|
|
813
|
+
bundle exec rspec
|
|
814
|
+
|
|
815
|
+
# Run specific test file
|
|
816
|
+
bundle exec rspec spec/services/token_service_spec.rb
|
|
817
|
+
|
|
818
|
+
# Run with documentation format
|
|
819
|
+
bundle exec rspec --format documentation
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### Code Quality
|
|
823
|
+
|
|
824
|
+
```bash
|
|
825
|
+
# Run RuboCop
|
|
826
|
+
bundle exec rubocop
|
|
827
|
+
|
|
828
|
+
# Auto-correct issues
|
|
829
|
+
bundle exec rubocop -a
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
## Contributing
|
|
833
|
+
|
|
834
|
+
We welcome contributions! Here's how:
|
|
835
|
+
|
|
836
|
+
1. Fork the repository
|
|
837
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
838
|
+
3. Add tests for your changes
|
|
839
|
+
4. Ensure all tests pass (`bundle exec rspec`)
|
|
840
|
+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
841
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
842
|
+
7. Open a Pull Request
|
|
843
|
+
|
|
844
|
+
Please ensure:
|
|
845
|
+
- All tests pass
|
|
846
|
+
- Code follows Ruby style guide (run `rubocop`)
|
|
847
|
+
- New features include tests and documentation
|
|
848
|
+
- Commits are well-described
|
|
849
|
+
|
|
850
|
+
## License
|
|
851
|
+
|
|
852
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
853
|
+
|
|
854
|
+
## Support
|
|
855
|
+
|
|
856
|
+
- **Issues**: https://github.com/SerhiiBorozenets/mcp-auth/issues
|
|
857
|
+
- **Documentation**: https://github.com/SerhiiBorozenets/mcp-auth
|
|
858
|
+
- **MCP Specification**: https://modelcontextprotocol.io
|
|
859
|
+
- **OAuth 2.1**: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
|
|
860
|
+
|
|
861
|
+
## Changelog
|
|
862
|
+
|
|
863
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
|
|
864
|
+
|
|
865
|
+
## Credits
|
|
866
|
+
|
|
867
|
+
Created by Serhii Borozenets
|
|
868
|
+
|
|
869
|
+
Built with ❤️ for the Model Context Protocol community.
|