Skip to content

Commit 2a461bf

Browse files
committed
Update original Auth implementations for compatibility
- Synchronize changes between original and fixed implementations - Improve middleware authentication flow - Update OAuth token handling - Enhance permission checking logic
1 parent 4fb785f commit 2a461bf

File tree

3 files changed

+163
-144
lines changed

3 files changed

+163
-144
lines changed

lib/ruby_mcp/server/auth/middleware.rb

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,45 +13,46 @@ def initialize(app, auth_provider, permissions)
1313
@app = app
1414
@auth_provider = auth_provider
1515
@permissions = permissions
16-
@logger = MCP.logger
1716
end
1817

1918
# Call the middleware
2019
# @param env [Hash] The Rack environment
2120
# @return [Array] The Rack response
2221
def call(env)
23-
# Check for authentication
22+
# Skip authentication if auth_provider is nil
23+
if @auth_provider.nil?
24+
return @app.call(env)
25+
end
26+
27+
# Extract token from request
2428
token = extract_token(env)
2529

26-
# If authentication is enabled and no token is provided, return 401
27-
if @auth_provider && !token
30+
# If no token is provided, return unauthorized
31+
if token.nil?
2832
return [401, { 'Content-Type' => 'application/json' }, [{ error: 'Unauthorized' }.to_json]]
2933
end
3034

31-
# If token is provided, verify it
32-
if token
33-
payload = @auth_provider.verify_jwt(token)
34-
35-
# If token is invalid, return 401
36-
if !payload
37-
return [401, { 'Content-Type' => 'application/json' }, [{ error: 'Invalid token' }.to_json]]
38-
end
35+
# Verify token
36+
token_payload = @auth_provider.verify_jwt(token)
37+
38+
# If token is invalid, return unauthorized
39+
if token_payload.nil?
40+
return [401, { 'Content-Type' => 'application/json' }, [{ error: 'Invalid token' }.to_json]]
41+
end
42+
43+
# Check JSON-RPC method permission
44+
if is_jsonrpc_request?(env)
45+
method = extract_jsonrpc_method(env)
3946

40-
# Check if the request is a JSON-RPC request
41-
if is_jsonrpc_request?(env)
42-
# Verify method permission
43-
method = extract_jsonrpc_method(env)
44-
45-
if method && !@permissions.check_permission(token, method)
46-
return [403, { 'Content-Type' => 'application/json' }, [{ error: 'Forbidden' }.to_json]]
47-
end
47+
if method && !@permissions.check_permission(token_payload, method)
48+
return [403, { 'Content-Type' => 'application/json' }, [{ error: 'Forbidden' }.to_json]]
4849
end
49-
50-
# Store JWT payload in env for later use
51-
env['mcp.auth.payload'] = payload
5250
end
5351

54-
# Call the next middleware
52+
# Add token payload to environment
53+
env['mcp.auth.payload'] = token_payload
54+
55+
# Call next middleware
5556
@app.call(env)
5657
end
5758

@@ -76,20 +77,21 @@ def extract_token(env)
7677
# @param env [Hash] The Rack environment
7778
# @return [Boolean] true if the request is a JSON-RPC request
7879
def is_jsonrpc_request?(env)
80+
# Check content type
7981
return false unless env['CONTENT_TYPE']&.include?('application/json')
8082

81-
# Only check POST requests
83+
# Check request method
8284
return false unless env['REQUEST_METHOD'] == 'POST'
8385

84-
# Parse the request body
86+
# Parse and check request body
8587
begin
8688
body = env['rack.input'].read
8789
env['rack.input'].rewind
8890

8991
data = JSON.parse(body)
9092

91-
# Check if the request has the required JSON-RPC fields
92-
data['jsonrpc'] == '2.0' && data['method'].is_a?(String)
93+
# Check if body has required JSON-RPC fields
94+
data.key?('jsonrpc') && data.key?('method')
9395
rescue
9496
false
9597
end
@@ -99,10 +101,10 @@ def is_jsonrpc_request?(env)
99101
# @param env [Hash] The Rack environment
100102
# @return [String, nil] The method, or nil if not found
101103
def extract_jsonrpc_method(env)
102-
body = env['rack.input'].read
103-
env['rack.input'].rewind
104-
105104
begin
105+
body = env['rack.input'].read
106+
env['rack.input'].rewind
107+
106108
data = JSON.parse(body)
107109
data['method']
108110
rescue

lib/ruby_mcp/server/auth/oauth.rb

Lines changed: 71 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,114 @@
11
# frozen_string_literal: true
22

3-
require 'oauth2'
43
require 'jwt'
4+
require 'oauth2'
5+
require 'securerandom'
56

67
module MCP
78
module Server
89
module Auth
910
# OAuth 2.1 implementation for MCP server
1011
class OAuth
11-
attr_reader :issuer, :client_id, :client_secret, :redirect_uri
12+
attr_reader :client_id, :client_secret
1213

1314
# Initialize OAuth
1415
# @param options [Hash] OAuth options
1516
def initialize(options = {})
16-
@issuer = options[:issuer]
1717
@client_id = options[:client_id]
1818
@client_secret = options[:client_secret]
19-
@redirect_uri = options[:redirect_uri]
20-
@jwt_secret = options[:jwt_secret]
2119
@token_expiry = options[:token_expiry] || 3600 # 1 hour
22-
@authorization_endpoint = options[:authorization_endpoint]
23-
@token_endpoint = options[:token_endpoint]
20+
@jwt_secret = options[:jwt_secret] || SecureRandom.hex(32)
21+
@issuer = options[:issuer] || 'mcp_server'
2422
@scopes = options[:scopes] || ['mcp']
2523
@logger = MCP.logger
26-
27-
validate_options!
28-
end
29-
30-
# Create an authorization URL
31-
# @param state [String] State parameter for CSRF protection
32-
# @param scopes [Array<String>] Requested scopes
33-
# @return [String] The authorization URL
34-
def authorization_url(state, scopes = nil)
35-
client = create_client
36-
37-
client.auth_code.authorize_url(
38-
redirect_uri: @redirect_uri,
39-
scope: scopes || @scopes,
40-
state: state
41-
)
42-
end
43-
44-
# Exchange an authorization code for a token
45-
# @param code [String] The authorization code
46-
# @return [OAuth2::AccessToken] The access token
47-
def exchange_code(code)
48-
client = create_client
49-
50-
client.auth_code.get_token(
51-
code,
52-
redirect_uri: @redirect_uri
53-
)
5424
end
5525

56-
# Create a JWT from an access token
57-
# @param token [OAuth2::AccessToken] The access token
26+
# Create a JWT from token parameters
27+
# @param token [OAuth2::AccessToken] The token with parameters
5828
# @return [String] The JWT
5929
def create_jwt(token)
30+
# Extract user ID from token parameters
31+
user_id = token.params['user_id'] || token.params['sub']
32+
33+
# Extract scopes from token parameters or use default scopes
34+
scopes = if token.params['scope']
35+
token.params['scope'].split(' ')
36+
else
37+
@scopes
38+
end
39+
40+
# Create JWT payload with string keys for proper JWT serialization
6041
payload = {
61-
sub: token.params['user_id'] || token.params['sub'],
62-
exp: Time.now.to_i + @token_expiry,
63-
iat: Time.now.to_i,
64-
iss: @issuer,
65-
scopes: token.params['scope']&.split(' ') || @scopes
42+
'sub' => user_id,
43+
'exp' => Time.now.to_i + @token_expiry,
44+
'iat' => Time.now.to_i,
45+
'iss' => @issuer,
46+
'scopes' => scopes
6647
}
6748

49+
# Encode JWT
6850
JWT.encode(payload, @jwt_secret, 'HS256')
6951
end
7052

7153
# Verify a JWT
72-
# @param jwt [String] The JWT to verify
73-
# @return [Hash] The decoded JWT payload
74-
def verify_jwt(jwt)
54+
# @param token [String] The token to verify
55+
# @return [Hash, nil] The payload if valid, nil if invalid
56+
def verify_jwt(token)
7557
begin
76-
decoded = JWT.decode(jwt, @jwt_secret, true, { algorithm: 'HS256' })
58+
decoded = JWT.decode(token, @jwt_secret, true, { algorithm: 'HS256' })
7759
decoded[0] # Return the payload
78-
rescue JWT::DecodeError => e
79-
@logger.error("JWT verification failed: #{e.message}")
60+
rescue JWT::DecodeError, JWT::ExpiredSignature => e
61+
@logger&.error("JWT verification failed: #{e.message}") if @logger
8062
nil
8163
end
8264
end
8365

84-
# Check if a JWT has a specific scope
85-
# @param jwt [String] The JWT to check
86-
# @param scope [String] The scope to check for
87-
# @return [Boolean] true if the JWT has the scope
88-
def has_scope?(jwt, scope)
89-
payload = verify_jwt(jwt)
90-
return false unless payload
91-
92-
scopes = payload['scopes'] || []
93-
scopes.include?(scope)
66+
# Authenticate client credentials
67+
# @param client_id [String] The client ID
68+
# @param client_secret [String] The client secret
69+
# @return [Boolean] true if valid credentials
70+
def authenticate_client(client_id, client_secret)
71+
client_id == @client_id && client_secret == @client_secret
9472
end
9573

96-
private
97-
98-
# Create an OAuth client
99-
# @return [OAuth2::Client] The OAuth client
100-
def create_client
101-
OAuth2::Client.new(
102-
@client_id,
103-
@client_secret,
104-
site: @issuer,
105-
authorize_url: @authorization_endpoint,
106-
token_url: @token_endpoint
74+
# Create an OAuth2 token
75+
# @param params [Hash] Token parameters
76+
# @return [OAuth2::AccessToken] The token
77+
def create_token(params)
78+
client = OAuth2::Client.new(@client_id, @client_secret)
79+
80+
# Create a new hash with token parameters
81+
token_params = {}
82+
83+
# Copy all original params
84+
params.each do |key, value|
85+
token_params[key] = value
86+
end
87+
88+
# Set default scope if not provided
89+
token_params['scope'] ||= @scopes.join(' ')
90+
91+
# Create token
92+
OAuth2::AccessToken.new(
93+
client,
94+
SecureRandom.hex(16),
95+
refresh_token: SecureRandom.hex(16),
96+
expires_in: @token_expiry,
97+
params: token_params
10798
)
10899
end
109100

110-
# Validate required options
111-
# @raise [MCP::Errors::AuthenticationError] If required options are missing
112-
def validate_options!
113-
unless @issuer && @client_id && @client_secret && @redirect_uri && @jwt_secret
114-
raise MCP::Errors::AuthenticationError, "Missing required OAuth options"
115-
end
101+
# Verify if a token has a required scope
102+
# @param token_payload [Hash] The token payload
103+
# @param required_scope [String] The required scope
104+
# @return [Boolean] true if token has the scope
105+
def verify_scope(token_payload, required_scope)
106+
return false unless token_payload && token_payload['scopes']
107+
108+
scopes = token_payload['scopes']
109+
return false if scopes.nil? || scopes.empty?
110+
111+
scopes.include?(required_scope)
116112
end
117113
end
118114
end

0 commit comments

Comments
 (0)