Class: Hitch::AuthorizationRequest
- Inherits:
-
Object
- Object
- Hitch::AuthorizationRequest
- Includes:
- IssuerUrl, UriValidation
- Defined in:
- app/models/hitch/authorization_request.rb
Overview
The HTTP-free core of one authorize request: parameter and PKCE validation, client and redirect_uri resolution against whichever registration scheme the client_id belongs to, scope clamping, and response-redirect construction. The controller renders what this object decides.
Defined Under Namespace
Classes: Error
Constant Summary collapse
- RESPONSE_PARAMS =
Response parameters are stripped from the registered query before the response is appended. Defense in depth plus one real gap: a client that legitimately registered a query containing a response parameter would otherwise receive it twice, and first-wins query parsers (URLSearchParams, Go's Query().Get, Python's parse_qs) would read the registered value — the issuer mix-up RFC 9207 exists to prevent. The error parameters don't even need that gap: registration is unauthenticated, so an attacker can point their own client's redirect_uri at a legitimate client's callback carrying
?error=…, and §4.1.2 makes clients branch onerrorfirst — attacker-written UI copy inside the real client's trusted error surface. %w[code state iss error error_description error_uri].freeze
Instance Attribute Summary collapse
-
#params ⇒ Object
readonly
Returns the value of attribute params.
-
#principal ⇒ Object
readonly
Returns the value of attribute principal.
-
#resource ⇒ Object
readonly
The canonical resource (RFC 8707 audience), available once valid.
Instance Method Summary collapse
-
#audit_client_name ⇒ Object
The name the client claims for itself.
-
#client ⇒ Object
The resolved client, from whichever registration scheme the client_id belongs to: an https client_id is a Client ID Metadata Document reference (MCP 2026-07-28); anything else is an opaque DCR client_id.
- #client_id ⇒ Object
- #code_challenge ⇒ Object
- #code_challenge_method ⇒ Object
-
#deny? ⇒ Boolean
:decision is accepted on the consent POST but never echoed by the consent screen — a crafted authorize link must not pre-press Deny.
-
#display_client_name ⇒ Object
The consent screen's display name.
- #error ⇒ Object
-
#granted_scopes ⇒ Object
Intersect the requested scope with the server's supported_scopes allowlist.
-
#initialize(params, principal:) ⇒ AuthorizationRequest
constructor
A new instance of AuthorizationRequest.
-
#localhost_only_client? ⇒ Boolean
MCP 2026-07-28 security considerations: a metadata document "cannot prevent localhost URL impersonation by itself" — anyone can host a document claiming any name and point it at a loopback port, and nothing proves which program is listening there.
- #redirect_host ⇒ Object
- #redirect_uri ⇒ Object
-
#redirect_uri_for(**response) ⇒ Object
The redirect back to the validated redirect_uri, carrying
issunconditionally (RFC 9207 — byte-identical to the discovery issuer, which is why both come from the shared IssuerUrl derivation). - #state ⇒ Object
- #valid? ⇒ Boolean
Constructor Details
#initialize(params, principal:) ⇒ AuthorizationRequest
Returns a new instance of AuthorizationRequest.
29 30 31 32 |
# File 'app/models/hitch/authorization_request.rb', line 29 def initialize(params, principal:) @params = params @principal = principal end |
Instance Attribute Details
#params ⇒ Object (readonly)
Returns the value of attribute params.
27 28 29 |
# File 'app/models/hitch/authorization_request.rb', line 27 def params @params end |
#principal ⇒ Object (readonly)
Returns the value of attribute principal.
27 28 29 |
# File 'app/models/hitch/authorization_request.rb', line 27 def principal @principal end |
#resource ⇒ Object (readonly)
The canonical resource (RFC 8707 audience), available once valid.
57 58 59 |
# File 'app/models/hitch/authorization_request.rb', line 57 def resource @resource end |
Instance Method Details
#audit_client_name ⇒ Object
The name the client claims for itself. Attacker-controllable in both schemes; persisted on the token for audit fidelity only.
118 119 120 |
# File 'app/models/hitch/authorization_request.rb', line 118 def audit_client_name client&.client_name || friendly_client_name || "Unknown" end |
#client ⇒ Object
The resolved client, from whichever registration scheme the client_id belongs to: an https client_id is a Client ID Metadata Document reference (MCP 2026-07-28); anything else is an opaque DCR client_id. The two cannot collide. Resolved once — each question the flow asks (redirect validation, consent warning, audit name) would otherwise repeat the DB lookup or, for CIMD without a shared cache, the outbound fetch. nil (no such client) memoizes too.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'app/models/hitch/authorization_request.rb', line 66 def client return @client if defined?(@client) @client = if ClientIdMetadata.reference?(client_id) # A principal that cannot be counted must not drive outbound # fetches: the per-actor limit is the bound on amplification, so # no actor means no fetch and the client reads as unknown. actor = rate_limit_actor actor && ClientIdMetadata.resolve(client_id, actor: actor) else Client.find_by(client_id: client_id) end end |
#client_id ⇒ Object
50 |
# File 'app/models/hitch/authorization_request.rb', line 50 def client_id = params[:client_id] |
#code_challenge ⇒ Object
53 |
# File 'app/models/hitch/authorization_request.rb', line 53 def code_challenge = params[:code_challenge] |
#code_challenge_method ⇒ Object
54 |
# File 'app/models/hitch/authorization_request.rb', line 54 def code_challenge_method = params[:code_challenge_method] |
#deny? ⇒ Boolean
:decision is accepted on the consent POST but never echoed by the consent screen — a crafted authorize link must not pre-press Deny.
46 47 48 |
# File 'app/models/hitch/authorization_request.rb', line 46 def deny? params[:decision] == "deny" end |
#display_client_name ⇒ Object
The consent screen's display name. Never the client's declared name — that is attacker-controllable in both registration schemes — but a label derived from the verified redirect_uri host.
112 113 114 |
# File 'app/models/hitch/authorization_request.rb', line 112 def display_client_name friendly_client_name || redirect_host || "An application" end |
#error ⇒ Object
39 40 41 42 |
# File 'app/models/hitch/authorization_request.rb', line 39 def error valid? @error end |
#granted_scopes ⇒ Object
Intersect the requested scope with the server's supported_scopes allowlist. A client can only ever receive scopes the server actually supports (RFC 6749 §3.3 — the AS MAY narrow). An empty intersection falls back to the default scope so the token is never scopeless.
85 86 87 |
# File 'app/models/hitch/authorization_request.rb', line 85 def granted_scopes Hitch.configuration.clamp_scopes(params[:scope]) end |
#localhost_only_client? ⇒ Boolean
MCP 2026-07-28 security considerations: a metadata document "cannot prevent localhost URL impersonation by itself" — anyone can host a document claiming any name and point it at a loopback port, and nothing proves which program is listening there. The consent screen warns.
126 127 128 129 130 131 132 133 |
# File 'app/models/hitch/authorization_request.rb', line 126 def localhost_only_client? return false unless ClientIdMetadata.reference?(client_id) declared = registered_redirect_uris return false if declared.blank? declared.all? { |candidate| loopback_http_uri?(candidate) } end |
#redirect_host ⇒ Object
103 104 105 106 107 |
# File 'app/models/hitch/authorization_request.rb', line 103 def redirect_host URI.parse(redirect_uri.to_s).host rescue URI::InvalidURIError nil end |
#redirect_uri ⇒ Object
51 |
# File 'app/models/hitch/authorization_request.rb', line 51 def redirect_uri = params[:redirect_uri] |
#redirect_uri_for(**response) ⇒ Object
The redirect back to the validated redirect_uri, carrying iss
unconditionally (RFC 9207 — byte-identical to the discovery issuer,
which is why both come from the shared IssuerUrl derivation).
92 93 94 95 96 97 98 99 100 101 |
# File 'app/models/hitch/authorization_request.rb', line 92 def redirect_uri_for(**response) uri = URI.parse(redirect_uri) query_params = URI.decode_www_form(uri.query || "") .reject { |key, _| RESPONSE_PARAMS.include?(key) } response.merge(iss: issuer_url).each do |key, value| query_params << [ key.to_s, value ] if value.present? end uri.query = URI.encode_www_form(query_params) uri.to_s end |
#state ⇒ Object
52 |
# File 'app/models/hitch/authorization_request.rb', line 52 def state = params[:state] |
#valid? ⇒ Boolean
34 35 36 37 |
# File 'app/models/hitch/authorization_request.rb', line 34 def valid? @error = validate unless defined?(@error) @error.nil? end |