Class: Axn::Webhooks::Verifiers::BasicAuth
- Inherits:
-
Object
- Object
- Axn::Webhooks::Verifiers::BasicAuth
- Defined in:
- lib/axn/webhooks/verifiers/basic_auth.rb
Overview
HTTP Basic auth (RFC 7617) as a verify strategy.
Unlike the signature strategies, Basic auth is a two-legged protocol: a client that does
NOT authenticate preemptively sends its first request with no Authorization header,
expects a 401 carrying WWW-Authenticate: Basic realm="…", and only then repeats the
request with credentials. Twilio documents exactly this behaviour for webhook URLs, and it
is why this strategy is a class rather than a bare lambda: #unauthorized_headers is what
Endpoint#to_response attaches to the 401, and without it the second leg never happens and
every webhook is silently dropped.
Constant Summary collapse
- DEFAULT_REALM =
"Webhook"- CONTROL_CHARS =
A control character can't be represented in an RFC 7230 quoted-string at all, and CR/LF would split the response header outright. Rejected at declaration time (this runs inside
Axn::Webhooks.inbound, i.e. at boot) rather than silently scrubbed: a realm is developer config, so a typo should fail the deploy, not ship a subtly malformed challenge. /[\x00-\x1F\x7F]/
Instance Attribute Summary collapse
-
#realm ⇒ Object
readonly
Returns the value of attribute realm.
Instance Method Summary collapse
- #call(request) ⇒ Object
-
#challenge_required?(request) ⇒ Boolean
Is this request an authentication attempt at all? False for anything carrying an
Authorizationheader — including a non-Basic scheme, which is a client that meant to authenticate and got it wrong, and stays a visible:credentials_missingrejection. -
#initialize(username:, password:, realm: DEFAULT_REALM) ⇒ BasicAuth
constructor
A new instance of BasicAuth.
-
#inspect ⇒ Object
A verifier holds credentials by definition, and Verify's per-call logging renders its
verifier:input — so the default Object#inspect would put the plaintext password in the application log on every single request. -
#pretty_print(printer) ⇒ Object
PP does not route through #inspect — Kernel#pretty_print walks instance variables directly — so without this
pp verifierleaks exactly what #inspect just redacted. -
#unauthorized_headers ⇒ Object
The RFC 7617 challenge.
Constructor Details
#initialize(username:, password:, realm: DEFAULT_REALM) ⇒ BasicAuth
Returns a new instance of BasicAuth.
26 27 28 29 30 31 32 |
# File 'lib/axn/webhooks/verifiers/basic_auth.rb', line 26 def initialize(username:, password:, realm: DEFAULT_REALM) raise Axn::Webhooks::Error, "verify :basic_auth realm cannot contain control characters" if realm.to_s.match?(CONTROL_CHARS) @username = username @password = password @realm = realm.to_s end |
Instance Attribute Details
#realm ⇒ Object (readonly)
Returns the value of attribute realm.
34 35 36 |
# File 'lib/axn/webhooks/verifiers/basic_auth.rb', line 34 def realm @realm end |
Instance Method Details
#call(request) ⇒ Object
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 |
# File 'lib/axn/webhooks/verifiers/basic_auth.rb', line 46 def call(request) # Fail closed on a misconfigured deploy rather than comparing against "": an unset # credential pair would otherwise authenticate `Authorization: Basic Og==` for anyone. # Blank-but-present counts as missing — CI and secret managers can both set an empty # string. Raising (not returning false) makes it a reported exception, since a 401 that # means "we are misconfigured" is indistinguishable from one that means "you are not # Twilio" and would otherwise present as an unexplained outage. # # Guarded BEFORE any #to_s (security audit): coercing first let non-Strings through, and # `false.to_s` is the non-empty String "false" — so a `false` credential pair sailed past # an emptiness check and collapsed the login to the guessable constant `false:false`. expected_username = Verifiers.require_secret!("verify :basic_auth", Resolvers.resolve(@username, request), label: "username") expected_password = Verifiers.require_secret!("verify :basic_auth", Resolvers.resolve(@password, request), label: "password") username, password = credentials(request) # Absent/non-Basic credentials are reported apart from wrong ones. Under RFC 7617 a bare # first request is the handshake working, not a failure, so it's the highest-volume # rejection on a healthy endpoint — conflating it with a real credential problem would # bury the latter in expected traffic. return Signature::CREDENTIALS_MISSING unless username # `&` rather than `&&` so the comparison doesn't short-circuit on the username. matched = secure_compare(username, expected_username) & secure_compare(password, expected_password) matched ? Signature::OK : Signature::CREDENTIALS_MISMATCH end |
#challenge_required?(request) ⇒ Boolean
Is this request an authentication attempt at all? False for anything carrying an
Authorization header — including a non-Basic scheme, which is a client that meant to
authenticate and got it wrong, and stays a visible :credentials_missing rejection.
True only for the bare first leg of the RFC 7617 handshake, which Endpoint answers with the challenge instead of running Verify: there is nothing to verify, and a reactive client sends one of these per successful webhook, so recording them as verify failures made the highest-volume outcome on a healthy endpoint a failure (PRO-3148).
A blank header counts as absent — an empty Authorization presents no credentials and no
scheme, so treating it as an attempt would 401 it with no telemetry either way.
83 |
# File 'lib/axn/webhooks/verifiers/basic_auth.rb', line 83 def challenge_required?(request) = request.header("Authorization").to_s.strip.empty? |
#inspect ⇒ Object
A verifier holds credentials by definition, and Verify's per-call logging renders its
verifier: input — so the default Object#inspect would put the plaintext password in the
application log on every single request. Same treatment Request gets, and for the same
reason. The realm is safe (and useful) to show: it's already broadcast in the challenge.
40 |
# File 'lib/axn/webhooks/verifiers/basic_auth.rb', line 40 def inspect = "#<#{self.class.name} realm=#{realm.inspect} credentials=[REDACTED]>" |
#pretty_print(printer) ⇒ Object
PP does not route through #inspect — Kernel#pretty_print walks instance variables
directly — so without this pp verifier leaks exactly what #inspect just redacted.
44 |
# File 'lib/axn/webhooks/verifiers/basic_auth.rb', line 44 def pretty_print(printer) = printer.text(inspect) |
#unauthorized_headers ⇒ Object
The RFC 7617 challenge. Lower-cased key per Rack 3's response-header SPEC (Response lower-cases keys anyway; spelled that way here so the two agree on sight).
The realm is an RFC 7230 quoted-string, so " and \ are escaped rather than stripped —
the realm a client displays should be the one that was configured. Stripping only quotes
would leave a trailing backslash escaping the closing quote (realm="Partner\"), which
is malformed enough that a client may reject the challenge and never retry — recreating
the exact silent-drop failure this strategy exists to prevent.
93 94 95 |
# File 'lib/axn/webhooks/verifiers/basic_auth.rb', line 93 def { "www-authenticate" => %(Basic realm="#{realm.gsub(/([\\"])/, '\\\\\1')}") } end |