Module: Reputable::Verify
- Defined in:
- lib/reputable/verify.rb
Overview
Bot Signals — verify the signed ticket the widget (or the check endpoint) hands to your form. A ticket is a short-lived signed JWT carrying the visitor's signal report (network evidence, score, and whether a CAPTCHA was solved). Verify it offline against the published JWKS (no network call), with the hosted introspection endpoint as a fallback for environments that cannot fetch/cache the JWKS.
What you do with the report is up to you: the "outcome" field is an advisory assessment derived from YOUR key's thresholds.
Class Method Summary collapse
- .blank?(value) ⇒ Boolean
-
.challenge_passed?(ticket, **kwargs) ⇒ Boolean
Convenience predicate: did this ticket clear a CAPTCHA challenge?.
- .claim_matches?(claims, key, expected, alternate: nil) ⇒ Boolean
-
.introspect(ticket, customer_id: nil, session_ref: nil) ⇒ Object
Introspection fallback: POST the ticket to the hosted service, which verifies it server-side and returns the report.
- .normalize_origin(value) ⇒ Object
- .present?(value) ⇒ Boolean
- .return_origin_matches?(actual, expected) ⇒ Boolean
-
.verify_proof(proof, **kwargs) ⇒ Object
Legacy alias from the proof era.
-
.verify_ticket(ticket, **options) ⇒ Object
Verify a ticket and return its signal report hash (keys include "outcome", "challengePassed", "proxyScore", "rating", "signals", ...), or nil if invalid.
-
.verify_ticket_claims(ticket, customer_id: nil, session_ref: nil, jwks: nil, network_fallback: true, expected_key_id: nil, expected_mode: nil, expected_return_origin: nil) ⇒ Object
Verify a ticket and return the complete signed claims.
Class Method Details
.blank?(value) ⇒ Boolean
116 117 118 |
# File 'lib/reputable/verify.rb', line 116 def blank?(value) value.nil? || (value.respond_to?(:empty?) && value.empty?) end |
.challenge_passed?(ticket, **kwargs) ⇒ Boolean
Convenience predicate: did this ticket clear a CAPTCHA challenge?
79 80 81 82 83 84 |
# File 'lib/reputable/verify.rb', line 79 def challenge_passed?(ticket, **kwargs) report = verify_ticket(ticket, **kwargs) return false unless report !!(report["challengePassed"] || report[:challengePassed]) end |
.claim_matches?(claims, key, expected, alternate: nil) ⇒ Boolean
120 121 122 123 124 125 |
# File 'lib/reputable/verify.rb', line 120 def claim_matches?(claims, key, expected, alternate: nil) return true unless present?(expected) values = [claims[key], alternate && claims[alternate]].compact values.any? && values.all? { |value| value == expected } end |
.introspect(ticket, customer_id: nil, session_ref: nil) ⇒ Object
Introspection fallback: POST the ticket to the hosted service, which verifies it server-side and returns the report. Always treat a non-valid response as a rejection.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/reputable/verify.rb', line 89 def introspect(ticket, customer_id: nil, session_ref: nil) base = Measurement.measurement_base_url uri = URI("#{base}/v1/signals/introspect") payload = { ticket: ticket, customerId: customer_id, sessionRef: session_ref }.compact response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: Reputable.configuration.connect_timeout, read_timeout: Reputable.configuration.read_timeout) do |http| request = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json") request.body = JSON.generate(payload) http.request(request) end return nil unless response.is_a?(Net::HTTPSuccess) data = JSON.parse(response.body) return nil unless data["valid"] { "report" => data["report"] || data["result"], "sessionRef" => data["sessionRef"], "customerId" => data["customerId"] } rescue StandardError => e Reputable.logger&.warn "Reputable: ticket introspection failed: #{e.}" nil end |
.normalize_origin(value) ⇒ Object
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/reputable/verify.rb', line 135 def normalize_origin(value) uri = URI(value.to_s) return nil unless %w[http https].include?(uri.scheme) && present?(uri.host) default_port = (uri.scheme == "https" ? 443 : 80) port = uri.port == default_port ? "" : ":#{uri.port}" "#{uri.scheme}://#{uri.host.downcase}#{port}" rescue URI::InvalidURIError nil end |
.present?(value) ⇒ Boolean
112 113 114 |
# File 'lib/reputable/verify.rb', line 112 def present?(value) !blank?(value) end |
.return_origin_matches?(actual, expected) ⇒ Boolean
127 128 129 130 131 132 133 |
# File 'lib/reputable/verify.rb', line 127 def return_origin_matches?(actual, expected) return true unless present?(expected) normalized_actual = normalize_origin(actual) normalized_expected = normalize_origin(expected) normalized_actual && normalized_expected && normalized_actual == normalized_expected end |
.verify_proof(proof, **kwargs) ⇒ Object
Legacy alias from the proof era.
74 75 76 |
# File 'lib/reputable/verify.rb', line 74 def verify_proof(proof, **kwargs) verify_ticket(proof, **kwargs) end |
.verify_ticket(ticket, **options) ⇒ Object
Verify a ticket and return its signal report hash (keys include "outcome", "challengePassed", "proxyScore", "rating", "signals", ...), or nil if invalid.
Verification is offline-first (JWKS); if that yields nothing it falls back
to the hosted introspection endpoint. When session_ref is given it must
match the ticket's subject.
35 36 37 38 39 40 41 |
# File 'lib/reputable/verify.rb', line 35 def verify_ticket(ticket, **) claims = verify_ticket_claims(ticket, **) claims && (claims["report"] || claims["result"] || claims) rescue StandardError => e Reputable.logger&.warn "Reputable: verify_ticket failed: #{e.}" nil end |
.verify_ticket_claims(ticket, customer_id: nil, session_ref: nil, jwks: nil, network_fallback: true, expected_key_id: nil, expected_mode: nil, expected_return_origin: nil) ⇒ Object
Verify a ticket and return the complete signed claims. Set
network_fallback: false in latency-sensitive request paths to prohibit
both JWKS fetching and hosted introspection.
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 |
# File 'lib/reputable/verify.rb', line 46 def verify_ticket_claims(ticket, customer_id: nil, session_ref: nil, jwks: nil, network_fallback: true, expected_key_id: nil, expected_mode: nil, expected_return_origin: nil) return nil if blank?(ticket) offline_jwks = jwks || Reputable.configuration.measurement_result_jwks return nil if !network_fallback && offline_jwks.nil? claims = Measurement.verify_result_token( ticket, jwks: offline_jwks, expected_customer_id: customer_id ) claims ||= introspect(ticket, customer_id: customer_id, session_ref: session_ref) if network_fallback return nil unless claims return nil unless claim_matches?(claims, "sessionRef", session_ref, alternate: "sub") return nil unless claim_matches?(claims, "keyId", expected_key_id) return nil unless claim_matches?(claims, "mode", expected_mode) return nil unless return_origin_matches?(claims["returnOrigin"], expected_return_origin) claims rescue StandardError => e Reputable.logger&.warn "Reputable: verify_ticket_claims failed: #{e.}" nil end |