Skip to content

Commit 32b1d34

Browse files
committed
Adds authorization callback handler
1 parent 6c83842 commit 32b1d34

14 files changed

Lines changed: 322 additions & 82 deletions

lib/mcp.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
require_relative "mcp/auth/server/provider"
2525
require_relative "mcp/auth/server/settings"
2626
require_relative "mcp/auth/server/uri_helper"
27-
require_relative "mcp/auth/server/client_registry"
28-
require_relative "mcp/auth/server/state_registry"
27+
require_relative "mcp/auth/server/registries/client_registry"
28+
require_relative "mcp/auth/server/registries/state_registry"
29+
require_relative "mcp/auth/server/registries/auth_code_registry"
30+
require_relative "mcp/auth/server/registries/token_registry"
31+
require_relative "mcp/auth/server/registries/in_memory_registry"
2932
require_relative "mcp/auth/server/request_parser"
3033
require_relative "mcp/auth/server/providers/mcp_authorization_server_provider"
3134
require_relative "mcp/auth/server/handlers/metadata_handler"
3235
require_relative "mcp/auth/server/handlers/registration_handler"
3336
require_relative "mcp/auth/server/handlers/authorization_handler"
37+
require_relative "mcp/auth/server/handlers/callback_handler"
3438

3539
module MCP
3640
class << self

lib/mcp/auth/server/client_registry.rb

Lines changed: 0 additions & 34 deletions
This file was deleted.

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ def handle(request)
9494
redirect_uri_provided_explicitly: auth_request.redirect_uri_provided?,
9595
response_type: auth_request.response_type,
9696
)
97-
9897
location = @auth_server_provider.authorize(client_info:, auth_params:)
9998
headers = {
10099
"Cache-Control": "no-store",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../errors"
4+
require_relative "../../server/provider"
5+
6+
module MCP
7+
module Auth
8+
module Server
9+
module Handlers
10+
class CallbackHandler
11+
def initialize(
12+
auth_server_provider:,
13+
request_parser:
14+
)
15+
@auth_server_provider = auth_server_provider
16+
@request_parser = request_parser
17+
end
18+
19+
def handle(request)
20+
params_h = @request_parser.parse_query_params(request)
21+
code = params_h[:code]
22+
state = params_h[:state]
23+
if code.nil? || state.nil?
24+
return bad_request_error(error: "invalid_request", error_description: "missing code or state parameter")
25+
end
26+
27+
begin
28+
redirect_uri = @auth_server_provider.authorize_callback(code:, state:)
29+
headers = { Location: redirect_uri }
30+
31+
[302, headers, nil]
32+
rescue Errors::AuthorizationError => e
33+
bad_request_error(
34+
error: e.error_code,
35+
error_description: e.message,
36+
)
37+
rescue
38+
[500, {}, { error: "server_error" }]
39+
end
40+
end
41+
42+
private
43+
44+
def bad_request_error(error:, error_description:)
45+
[400, {}, { error:, error_description: }]
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
require "securerandom"
44
require "time"
5-
require "json"
65
require_relative "../../../serialization_utils"
76
require_relative "../../errors"
87
require_relative "../../models"

lib/mcp/auth/server/provider.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,22 @@ def authorize(client_info:, auth_params:)
153153
raise NotImplementedError, "#{self.class.name}#authorize is not implemented"
154154
end
155155

156+
# Handles the callback from the OAuth provider after the user has authorized
157+
# the application. This is called when the OAuth provider redirects back to
158+
# the MCP server's callback endpoint.
159+
#
160+
# Implementations should validate the code and state parameters, exchange the
161+
# authorization code with the OAuth provider if needed, and return a URL to
162+
# redirect the user back to the original client application.
163+
#
164+
# @param code [String] The authorization code from the OAuth provider
165+
# @param state [String] The state parameter that was passed to the authorize endpoint
166+
# @return [String] The URL to redirect the user to (typically the client's redirect_uri)
167+
# @raise [MCP::Auth::Errors::AuthorizeError] If the callback parameters are invalid
168+
def authorize_callback(code:, state:)
169+
raise NotImplementedError, "#{self.class.name}#handle_callback is not implemented"
170+
end
171+
156172
# Loads an AuthorizationCode by its code string.
157173
#
158174
# @param client_info [MCP::Auth::Server::OAuthClientInformationFull] The client that requested the authorization code.

lib/mcp/auth/server/providers/mcp_authorization_server_provider.rb

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# frozen_string_literal: true
22

33
require "securerandom"
4+
require "net/http"
45
require_relative "../../../serialization_utils"
5-
require_relative "../client_registry"
6-
require_relative "../state_registry"
76
require_relative "../provider"
87

98
module MCP
@@ -39,28 +38,33 @@ class McpAuthorizationServerProvider
3938
include OAuthAuthorizationServerProvider
4039
include SerializationUtils
4140

41+
FIVE_MINUTES_IN_SECONDS = 300
42+
4243
def initialize(
4344
auth_server_settings:,
44-
client_registry: nil,
45-
state_registry: nil
45+
client_registry:,
46+
state_registry:,
47+
auth_code_registry:,
48+
token_registry:
4649
)
4750
@settings = auth_server_settings
48-
@client_registry = client_registry || InMemoryClientRegistry.new
49-
@state_registry = state_registry || InMemoryStateRegistry.new
51+
@client_registry = client_registry
52+
@state_registry = state_registry
53+
@auth_code_registry = auth_code_registry
54+
@token_registry = token_registry
5055
end
5156

5257
def get_client(client_id)
53-
@client_registry.find_by_id(client_id)
58+
@client_registry.find_client(client_id)
5459
end
5560

5661
def register_client(client_info)
57-
@client_registry.create(client_info)
62+
@client_registry.create_client(client_info)
5863
end
5964

6065
def authorize(client_info:, auth_params:)
6166
state = auth_params.state || SecureRandom.hex(16)
62-
63-
@state_registry.create(state, to_h(auth_params))
67+
@state_registry.create_state(state, to_h(auth_params))
6468

6569
auth_url = URI(@settings.auth_server_authorization_endpoint)
6670
auth_url.query = URI.encode_www_form([
@@ -73,6 +77,67 @@ def authorize(client_info:, auth_params:)
7377

7478
auth_url.to_s
7579
end
80+
81+
def authorize_callback(code:, state:)
82+
state_data = @state_registry.find_state(state)
83+
raise Errors::AuthorizationError.invalid_request("invalid state parameter") if state_data.nil?
84+
85+
access_token = query_access_token!({
86+
client_id: @settings.client_id,
87+
client_secret: @settings.client_secret,
88+
code:,
89+
redirect_uri: @settings.mcp_callback_endpoint,
90+
grant_type: "authorization_code",
91+
})
92+
93+
mcp_auth_code = "mcp_#{SecureRandom.hex(16)}"
94+
auth_code = MCP::Auth::Server::AuthorizationCode.new(
95+
code: mcp_auth_code,
96+
client_id: state_data[:client_id],
97+
redirect_uri: state_data[:redirect_uri],
98+
redirect_uri_provided_explicitly: state_data[:redirect_uri_provided_explicitly],
99+
expires_at: Time.now.to_i + FIVE_MINUTES_IN_SECONDS,
100+
scopes: ["mcp:user"],
101+
code_challenge: state_data[:code_challenge],
102+
)
103+
@auth_code_registry.create_auth_code(mcp_auth_code, auth_code)
104+
@token_registry.create_token(mcp_auth_code, AccessToken.new(
105+
token: access_token,
106+
client_id: state_data[:client_id],
107+
scopes: @settings.auth_server_scopes,
108+
expires_at: nil,
109+
))
110+
111+
redirect_uri = URI(state_data[:redirect_uri])
112+
redirect_uri.query = URI.encode_www_form([
113+
["code", mcp_auth_code],
114+
["state", state],
115+
])
116+
@state_registry.delete_state(state)
117+
118+
redirect_uri
119+
end
120+
121+
private
122+
123+
def query_access_token!(data)
124+
uri = URI(@settings.auth_server_token_endpoint)
125+
response = Net::HTTP.post_form(uri, stringify_keys(data))
126+
raise Errors::AuthorizationError.new(
127+
error_code: Errors::AuthorizationError::INVALID_REQUEST,
128+
message: "failed to exchange code for token",
129+
) unless response.is_a?(Net::HTTPOK)
130+
131+
data = JSON.parse(response.body)
132+
if data.key?("error")
133+
raise Errors::AuthorizationError.new(
134+
error_code: data["error"],
135+
message: data["error_description"] || data["error"],
136+
)
137+
end
138+
139+
data["access_token"]
140+
end
76141
end
77142
end
78143
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module MCP
4+
module Auth
5+
module Server
6+
module Registries
7+
module AuthCodeRegistry
8+
def create_auth_code(code_id, data)
9+
raise NotImplementedError, "#{self.class.name}#create_auth_code is not implemented"
10+
end
11+
12+
def find_auth_code(code_id)
13+
raise NotImplementedError, "#{self.class.name}#find_auth_code is not implemented"
14+
end
15+
16+
def delete_auth_code(code_id)
17+
raise NotImplementedError, "#{self.class.name}#delete_auth_code is not implemented"
18+
end
19+
end
20+
end
21+
end
22+
end
23+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module MCP
4+
module Auth
5+
module Server
6+
module Registries
7+
module ClientRegistry
8+
def create_client(client_info)
9+
raise NotImplementedError, "#{self.class.name}#create_client is not implemented"
10+
end
11+
12+
def find_client(client_id)
13+
raise NotImplementedError, "#{self.class.name}#find_client is not implemented"
14+
end
15+
end
16+
end
17+
end
18+
end
19+
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "auth_code_registry"
4+
require_relative "client_registry"
5+
require_relative "state_registry"
6+
require_relative "token_registry"
7+
8+
module MCP
9+
module Auth
10+
module Server
11+
module Registries
12+
class InMemoryRegistry
13+
include AuthCodeRegistry
14+
include ClientRegistry
15+
include StateRegistry
16+
include TokenRegistry
17+
18+
def initialize
19+
@codes = {}
20+
@clients = {}
21+
@states = {}
22+
@tokens = {}
23+
end
24+
25+
def create_client(client_info)
26+
raise ArgumentError, "Client '#{client_info.client_id}' already exists" if @clients.key?(client_info.client_id)
27+
28+
@clients[client_info.client_id] = client_info
29+
end
30+
31+
def find_client(client_id)
32+
@clients[client_id]
33+
end
34+
35+
def create_auth_code(code_id, data)
36+
raise ArgumentError, "Code with id '#{code}' already exists" if @codes.key?(code_id)
37+
38+
@codes[code_id] = data
39+
end
40+
41+
def find_code(code_id)
42+
@codes[code_id]
43+
end
44+
45+
def delete_code(code_id)
46+
@codes.delete(code_id)
47+
end
48+
49+
def create_state(state_id, state)
50+
raise ArgumentError, "State with id '#{state_id}' already exists" if @states.key?(state_id)
51+
52+
@states[state_id] = state
53+
end
54+
55+
def find_state(state_id)
56+
@states[state_id]
57+
end
58+
59+
def delete_state(state_id)
60+
@states.delete(state_id)
61+
end
62+
63+
def create_token(token_id, token)
64+
raise ArgumentError, "Token with id '#{token_id}' already exists" if @tokens.key?(token_id)
65+
66+
@tokens[token_id] = token
67+
end
68+
69+
def find_token(token_id)
70+
@tokens[token_id]
71+
end
72+
73+
def delete_token(token_id)
74+
@tokens.delete(token_id)
75+
end
76+
end
77+
end
78+
end
79+
end
80+
end

0 commit comments

Comments
 (0)