Skip to content

Commit 11fdc9c

Browse files
committed
Adds token handler logic tweaks to instantiation
1 parent 32b1d34 commit 11fdc9c

11 files changed

Lines changed: 315 additions & 35 deletions

File tree

lib/mcp.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
require_relative "mcp/auth/server/registries/in_memory_registry"
3232
require_relative "mcp/auth/server/request_parser"
3333
require_relative "mcp/auth/server/providers/mcp_authorization_server_provider"
34+
require_relative "mcp/auth/server/handlers"
3435
require_relative "mcp/auth/server/handlers/metadata_handler"
3536
require_relative "mcp/auth/server/handlers/registration_handler"
3637
require_relative "mcp/auth/server/handlers/authorization_handler"
3738
require_relative "mcp/auth/server/handlers/callback_handler"
39+
require_relative "mcp/auth/server/handlers/token_handler"
3840

3941
module MCP
4042
class << self

lib/mcp/auth/errors.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def initialize(error_code:, message: nil)
2525
end
2626
end
2727

28+
class ClientAuthenticationError < StandardError; end
29+
2830
class AuthorizationError < StandardError
2931
INVALID_REQUEST = "invalid_request"
3032
UNAUTHORIZED_CLIENT = "unauthorized_client"
@@ -45,6 +47,10 @@ class << self
4547
def invalid_request(message)
4648
AuthorizationError.new(error_code: INVALID_REQUEST, message:)
4749
end
50+
51+
def invalid_grant(message)
52+
AuthorizationError.new(error_code: INVALID_GRANT, message:)
53+
end
4854
end
4955
end
5056

lib/mcp/auth/models.rb

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ def validate_scopes!(requested_scopes)
125125
end
126126
end
127127

128+
def valid_grant_type?(grant_type)
129+
@grant_types.include?(grant_type)
130+
end
131+
128132
def valid_redirect_uri?(redirect_uri)
129133
@redirect_uris.include?(redirect_uri)
130134
end
@@ -155,6 +159,26 @@ def initialize(
155159
@client_id_issued_at = client_id_issued_at
156160
@client_secret_expires_at = client_secret_expires_at
157161
end
162+
163+
def authenticate!(request_client_id:, request_client_secret: nil)
164+
raise Errors::ClientAuthenticationError, "invalid client_id" if @client_id != request_client_id
165+
if @client_secret.nil?
166+
return
167+
end
168+
169+
raise Errors::ClientAuthenticationError, "client_secret mismatch" if @client_secret != request_client_secret
170+
raise Errors::ClientAuthenticationError, "client_secret has expired" if secret_expired?
171+
end
172+
173+
private
174+
175+
def secret_expired?
176+
if @client_secret_expires_at.nil?
177+
return false
178+
end
179+
180+
@client_secret_expires_at < Time.now.to_i
181+
end
158182
end
159183

160184
# Represents OAuth 2.0 Authorization Server Metadata as defined in RFC 8414.
@@ -289,21 +313,21 @@ class << self
289313
DEFAULT_REGISTRATION_PATH = "/register"
290314
DEFAULT_TOKEN_PATH = "/token"
291315

292-
def from_settings(auth_settings, **kwargs)
316+
def with_defaults(issuer_url:, client_registration_options:, **kwargs)
293317
metadata = OAuthMetadata.new(
294-
issuer: auth_settings.issuer_url,
295-
authorization_endpoint: auth_settings.issuer_url + DEFAULT_AUTHORIZE_PATH,
296-
token_endpoint: auth_settings.issuer_url + DEFAULT_TOKEN_PATH,
297-
scopes_supported: auth_settings.client_registration_options.valid_scopes,
318+
issuer: issuer_url,
319+
authorization_endpoint: issuer_url + DEFAULT_AUTHORIZE_PATH,
320+
token_endpoint: issuer_url + DEFAULT_TOKEN_PATH,
321+
scopes_supported: client_registration_options.valid_scopes,
298322
response_types_supported: ["code"],
299323
grant_types_supported: ["authorization_code", "refresh_token"],
300324
token_endpoint_auth_methods_supported: ["client_secret_post"],
301325
code_challenge_methods_supported: ["S256"],
302326
**kwargs,
303327
)
304328

305-
if auth_settings.client_registration_options.enabled
306-
metadata.registration_endpoint = auth_settings.issuer_url + DEFAULT_REGISTRATION_PATH
329+
if client_registration_options.enabled
330+
metadata.registration_endpoint = issuer_url + DEFAULT_REGISTRATION_PATH
307331
end
308332

309333
metadata

lib/mcp/auth/server/handlers.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module MCP
4+
module Auth
5+
module Server
6+
module Handlers
7+
class << self
8+
def create_handlers(auth_server_provider:, request_parser:)
9+
{
10+
oauth_authorization_server: MetadataHandler.new(auth_server_provider:),
11+
register: RegistrationHandler.new(auth_server_provider:, request_parser:),
12+
authorize: AuthorizationHandler.new(auth_server_provider:, request_parser:),
13+
callback: CallbackHandler.new(auth_server_provider:, request_parser:),
14+
token: TokenHandler.new(auth_server_provider:, request_parser:),
15+
}
16+
end
17+
end
18+
end
19+
end
20+
end
21+
end

lib/mcp/auth/server/handlers/authorization_handler.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,15 @@ def handle(request)
8787
client_info.validate_scopes!(scopes)
8888

8989
auth_params = AuthorizationParams.new(
90+
client_id: auth_request.client_id,
9091
state: auth_request.state,
9192
scopes:,
9293
code_challenge: auth_request.code_challenge,
9394
redirect_uri:,
9495
redirect_uri_provided_explicitly: auth_request.redirect_uri_provided?,
9596
response_type: auth_request.response_type,
9697
)
97-
location = @auth_server_provider.authorize(client_info:, auth_params:)
98+
location = @auth_server_provider.authorize(auth_params)
9899
headers = {
99100
"Cache-Control": "no-store",
100101
"Location": location,

lib/mcp/auth/server/handlers/metadata_handler.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ module Handlers
99
class MetadataHandler
1010
include SerializationUtils
1111

12-
def initialize(oauth_metadata)
13-
@oauth_metadata = oauth_metadata
12+
def initialize(auth_server_provider:)
13+
@auth_server_provider = auth_server_provider
1414
end
1515

1616
# returns [status, headers, body]
@@ -19,7 +19,7 @@ def handle(request)
1919
"Cache-Control": "public, max-age=3600",
2020
"Content-Type": "application/json",
2121
}
22-
[200, headers, to_h(@oauth_metadata)]
22+
[200, headers, to_h(@auth_server_provider.oauth_metadata)]
2323
end
2424
end
2525
end

lib/mcp/auth/server/handlers/registration_handler.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require_relative "../../../serialization_utils"
66
require_relative "../../errors"
77
require_relative "../../models"
8+
require_relative "../settings"
89

910
module MCP
1011
module Auth
@@ -15,11 +16,10 @@ class RegistrationHandler
1516

1617
def initialize(
1718
auth_server_provider:,
18-
client_registration_options:,
1919
request_parser:
2020
)
2121
@auth_server_provider = auth_server_provider
22-
@client_registration_options = client_registration_options
22+
@client_registration_options = auth_server_provider.client_registration_options
2323
@request_parser = request_parser
2424
end
2525

@@ -94,7 +94,7 @@ def client_secret_expires_at(client_metadata, issued_at)
9494

9595
def scope!(client_metadata)
9696
if client_metadata.scope.nil? && @client_registration_options.default_scopes
97-
return client_registration_options.default_scopes.join(" ")
97+
return @client_registration_options.default_scopes.join(" ")
9898
end
9999

100100
if client_metadata.scope
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# frozen_string_literal: true
2+
3+
require "digest"
4+
require "base64"
5+
require_relative "../../errors"
6+
require_relative "../../server/provider"
7+
require_relative "../../models"
8+
9+
module MCP
10+
module Auth
11+
module Server
12+
module Handlers
13+
class BaseRequest
14+
attr_reader :grant_type, :client_id, :client_secret
15+
16+
def initialize(
17+
grant_type:,
18+
client_id:,
19+
client_secret: nil
20+
)
21+
@grant_type = grant_type
22+
@client_id = client_id
23+
@client_secret = client_secret
24+
end
25+
end
26+
27+
class AuthorizationCodeRequest < BaseRequest
28+
attr_reader :code, :code_verifier, :redirect_uri
29+
30+
def initialize(
31+
code:,
32+
code_verifier:,
33+
redirect_uri: nil,
34+
**base_kwargs
35+
)
36+
super(**base_kwargs)
37+
38+
@code = code
39+
@code_verifier = code_verifier
40+
@redirect_uri = redirect_uri
41+
42+
if @grant_type != "authorization_code"
43+
raise Errors::AuthorizationError.invalid_request("grant_type must be authorization_code")
44+
end
45+
end
46+
end
47+
48+
class TokenHandler
49+
def initialize(
50+
auth_server_provider:,
51+
request_parser:
52+
)
53+
@auth_server_provider = auth_server_provider
54+
@request_parser = request_parser
55+
end
56+
57+
def handle(request)
58+
params_h = @request_parser.parse_query_params(request)
59+
request = AuthorizationCodeRequest.new(**params_h)
60+
61+
client_info = @auth_server_provider.get_client(request.client_id)
62+
validate_request_client!(request:, client_info:)
63+
64+
auth_code = @auth_server_provider.load_authorization_code(request.code)
65+
validate_auth_code!(request:, auth_code:)
66+
validate_pkce!(request:, auth_code:)
67+
tokens = @auth_server_provider.exchange_authorization_code(auth_code)
68+
69+
[200, {}, tokens]
70+
rescue Errors::ClientAuthenticationError => e
71+
bad_request_error(
72+
error_code: Errors::AuthorizationError::UNAUTHORIZED_CLIENT,
73+
error_description: e.message,
74+
)
75+
rescue Errors::AuthorizationError => e
76+
bad_request_error(
77+
error_codea: e.error_code,
78+
error_description: e.message,
79+
)
80+
rescue
81+
bad_request_error(
82+
error_code: Errors::AuthorizationError::SERVER_ERROR,
83+
error_description: "unexpected error",
84+
)
85+
end
86+
87+
private
88+
89+
def validate_request_client!(request:, client_info: nil)
90+
if client_info.nil?
91+
raise Errors::AuthorizationError.invalid_request("invalid client_id")
92+
end
93+
94+
client_info.authenticate!(
95+
request_client_id: request.client_id,
96+
request_client_secret: request.client_secret,
97+
)
98+
unless client_info.valid_grant_type?(request.grant_type)
99+
raise Errors::AuthorizationError.new(
100+
error_code: Errors::AuthorizationError::UNSUPPORTED_GRANT_TYPE,
101+
message: "supported grant type are #{client_info.grant_types}",
102+
)
103+
end
104+
end
105+
106+
def validate_auth_code!(request:, auth_code:)
107+
if auth_code.nil? || !auth_code.belongs_to_client?(request.client_id)
108+
# If auth code is for another client, pretend it's not there
109+
raise Errors::AuthorizationError.invalid_grant("authorization code does not exist")
110+
end
111+
112+
if auth_code.expired?
113+
raise Errors::AuthorizationError.invalid_grant("authorization code expired")
114+
end
115+
116+
# verify redirect_uri doesn't change between /authorize and /tokens
117+
# see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
118+
authorize_request_redirect_uri = auth_code.redirect_uri_provided_explicitly ? auth_code.redirect_uri : nil
119+
if request.redirect_uri != authorize_request_redirect_uri
120+
raise Errors::AuthorizationError.invalid_request("redirect_uri did not match the one when creating auth code")
121+
end
122+
end
123+
124+
def validate_pkce!(request:, auth_code:)
125+
sha256 = Digest::SHA256.digest(request.code_verifier.encode)
126+
request_code_challenge = Base64.urlsafe_encode64(sha256).tr("=", "")
127+
128+
unless auth_code.code_challenge_match?(request_code_challenge)
129+
# see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
130+
raise Errors::AuthorizationError.invalid_grant("incorrect code_verifier")
131+
end
132+
end
133+
134+
def bad_request_error(
135+
error_code:,
136+
error_description:
137+
)
138+
body = { error: error_code, error_description: }
139+
140+
[400, { "Cache-Control": "no-store", "Pragma": "no-cache" }, body]
141+
end
142+
end
143+
end
144+
end
145+
end
146+
end

0 commit comments

Comments
 (0)