Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions lib/mail_mcp/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class App < Sinatra::Base
token_endpoint: "#{base_url}/oauth/token",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
code_challenge_methods_supported: ["S256"]
})
end
Expand Down Expand Up @@ -106,9 +107,8 @@ class App < Sinatra::Base
post "/oauth/token" do
content_type :json

grant_type = params[:grant_type]
client_id = params[:client_id]
client_secret = params[:client_secret]
grant_type = params[:grant_type]
client_id, client_secret = client_credentials

client_config = begin
JwtService.decode_client_id(client_id.to_s)
Expand Down Expand Up @@ -146,6 +146,16 @@ class App < Sinatra::Base

private

# RFC 6749 section 2.3.1: clients may authenticate with HTTP Basic, whose
# user and password are the form-encoded client_id and client_secret, or
# with both in the request body.
def client_credentials
auth = Rack::Auth::Basic::Request.new(request.env)
return [params[:client_id], params[:client_secret]] unless auth.provided? && auth.basic?
Comment on lines +153 to +154

auth.credentials.map { |part| URI.decode_www_form_component(part) }
end

def resolve_mcp_context
auth = request.env["HTTP_AUTHORIZATION"]
return [nil, unauthorized_response("missing bearer token")] unless auth&.start_with?("Bearer ")
Expand Down
27 changes: 27 additions & 0 deletions spec/mail_mcp/app_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def app
expect(body["issuer"]).to eq(ENV.fetch("BASE_URL", nil))
expect(body["code_challenge_methods_supported"]).to include("S256")
expect(body).not_to have_key("registration_endpoint")
expect(body["token_endpoint_auth_methods_supported"]).to eq(%w[client_secret_basic client_secret_post])
end
end

Expand Down Expand Up @@ -97,6 +98,32 @@ def issue_code
expect(body["token_type"]).to eq("Bearer")
end

it "accepts client credentials sent with HTTP Basic authentication" do
basic_authorize URI.encode_www_form_component(client_id), URI.encode_www_form_component(client_secret)
Comment on lines +101 to +102
post "/oauth/token", {
grant_type: "authorization_code",
code: issue_code,
code_verifier: verifier,
redirect_uri: redirect_uri,
client_id: client_id
Comment on lines +107 to +108
}
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)["access_token"]).not_to be_nil
end

it "returns invalid_client for a wrong client_secret sent with HTTP Basic authentication" do
basic_authorize URI.encode_www_form_component(client_id), "wrong-secret"
post "/oauth/token", {
grant_type: "authorization_code",
code: issue_code,
code_verifier: verifier,
redirect_uri: redirect_uri,
client_id: client_id
}
expect(last_response.status).to eq(401)
expect(JSON.parse(last_response.body)["error"]).to eq("invalid_client")
end

it "issues a new access_token via refresh_token grant" do
post "/oauth/token", {
grant_type: "authorization_code",
Expand Down
Loading