Class: Hitch::RegistrationsController

Inherits:
PublicEndpointController
  • Object
show all
Includes:
CorsSupport, RegistrationAdmission, UriValidation
Defined in:
app/controllers/hitch/registrations_controller.rb

Overview

POST /oauth/register — Dynamic Client Registration (RFC 7591). MCP clients register before starting the OAuth flow. The client_name they send ("Claude Code", "ChatGPT", etc.) is attacker-controllable — we persist it for audit fidelity but consent UIs MUST NOT trust it for display (see authorize#new).

Constant Summary

Constants included from RegistrationAdmission

Hitch::RegistrationAdmission::ADMITTED_HEADER

Constants included from RequestAdmission

Hitch::RequestAdmission::MAX_REQUEST_BODY_BYTES

Instance Method Summary collapse

Instance Method Details

#createObject



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'app/controllers/hitch/registrations_controller.rb', line 14

def create
  # RegistrationAdmission has already parsed one bounded JSON object
  # and installed it here — the action never runs otherwise.
   = request.request_parameters

  # RFC 7591 §2: the authorization server is responsible for
  # enforcing its URI policy at registration. Without this, a
  # client could register `javascript:alert(1)` or
  # `http://attacker.test/cb` and try to use it at authorize.
  normalized = ()
  return if performed?

  candidate_uris = normalized.fetch(:redirect_uris)
  invalid = candidate_uris.reject { |uri| valid_redirect_uri?(uri) }
  if invalid.any?
    return oauth_error(
      "invalid_redirect_uri",
      "redirect_uris must contain only https URIs or RFC 8252 loopback http URIs"
    )
  end

  auth_method = (, "token_endpoint_auth_method") || "none"
  return if performed?

  unless Hitch::Client::TOKEN_ENDPOINT_AUTH_METHODS.include?(auth_method)
    return oauth_error(
      "invalid_client_metadata",
      "token_endpoint_auth_method must be none or client_secret_basic"
    )
  end

  application_type = (, "application_type")
  return if performed?

  client, client_secret = register_client(
    auth_method,
    client_name: ["client_name"],
    redirect_uris: candidate_uris,
    application_type: application_type
  )

  response_body = {
    client_id: client.client_id,
    client_id_issued_at: client.created_at.to_i,
    client_name: client.client_name,
    redirect_uris: client.redirect_uris,
    grant_types: [ "authorization_code" ],
    response_types: [ "code" ],
    scope: Hitch.configuration.supported_scopes.join(" "),
    token_endpoint_auth_method: client.token_endpoint_auth_method
  }.merge(
    # Echo what was actually STORED, not what was sent — RFC 7591
    # §3.2.1 makes the response the authoritative record of registered
    # metadata, so a client that sent an unrecognized value can tell
    # it was dropped by diffing its request against this response.
    # (Not that it reads as an explicit rejection: §2 defaults an
    # absent application_type to "web", so silence means the default,
    # not "you declared nothing".)
    #
    # Omitted rather than sent as null when undeclared. Scoped to this
    # one key deliberately: a blanket `.compact` would silently drop
    # any future nullable field, and §3.2.1 makes some of them —
    # `client_secret_expires_at` when a secret is issued — REQUIRED.
    client.application_type ? { application_type: client.application_type } : {}
  )
  if client_secret
    response_body.merge!(
      client_secret: client_secret,
      client_secret_issued_at: client.client_secret_issued_at.to_i,
      client_secret_expires_at: 0
    )
  end

  render json: response_body, status: :created
end