Class: HTTP::Features::DigestAuth
- Inherits:
-
HTTP::Feature
- Object
- HTTP::Feature
- HTTP::Features::DigestAuth
- Defined in:
- lib/http/features/digest_auth.rb,
sig/http.rbs
Overview
Implements HTTP Digest Authentication (RFC 2617 / RFC 7616)
When a server responds with 401 and a Digest challenge, this feature automatically computes the digest response and retries the request with the correct Authorization header.
Constant Summary collapse
- ALGORITHMS =
Supported hash algorithms
{ "MD5" => Digest::MD5, "SHA-256" => Digest::SHA256, "MD5-sess" => Digest::MD5, "SHA-256-sess" => Digest::SHA256 }.freeze
- WWW_AUTHENTICATE =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
WWW-Authenticate header name
"WWW-Authenticate"
Instance Method Summary collapse
-
#around_request(request) {|HTTP::Request| ... } ⇒ HTTP::Response
Wraps the HTTP exchange to handle digest authentication challenges.
-
#authorize(request, response) ⇒ HTTP::Request
private
Build an authorized copy of the request using the digest challenge.
-
#build_auth(request, challenge) ⇒ String
private
Build the Authorization header value.
-
#build_header(username:, realm:, nonce:, uri:, qop:, nonce_count:, cnonce:, response:, opaque:, algorithm:) ⇒ String
private
Build the Digest Authorization header string.
-
#compute_auth_header(algorithm:, qop:, nonce:, cnonce:, nonce_count:, uri:, ha1:, ha2:, challenge:) ⇒ String
private
Compute digest and build the Authorization header string.
-
#compute_ha1(algorithm, realm, nonce, cnonce) ⇒ String
private
Compute HA1 per RFC 2617.
-
#compute_ha2(algorithm, method, uri) ⇒ String
private
Compute HA2 per RFC 2617.
-
#compute_response(algorithm, ha1, ha2, nonce:, nonce_count:, cnonce:, qop:) ⇒ String
private
Compute the final digest response value.
-
#digest_challenge?(response) ⇒ Boolean
private
Check if the response contains a digest authentication challenge.
-
#hex_digest(algorithm, data) ⇒ String
private
Compute a hex digest using the specified algorithm.
-
#initialize(user:, pass:) ⇒ DigestAuth
constructor
Initialize the DigestAuth feature.
-
#parse_challenge(header) ⇒ Hash{String => String}
private
Parse the WWW-Authenticate header into a parameter hash.
-
#select_qop(qop_str) ⇒ String?
private
Select the best qop value from the challenge.
Methods inherited from HTTP::Feature
#on_error, #on_request, #wrap_request, #wrap_response
Constructor Details
#initialize(user:, pass:) ⇒ DigestAuth
Initialize the DigestAuth feature
35 36 37 38 |
# File 'lib/http/features/digest_auth.rb', line 35 def initialize(user:, pass:) @user = user @pass = pass end |
Instance Method Details
#around_request(request) {|HTTP::Request| ... } ⇒ HTTP::Response
Wraps the HTTP exchange to handle digest authentication challenges
On a 401 with a Digest WWW-Authenticate header, flushes the error response, computes digest credentials, and retries the request.
53 54 55 56 57 58 59 |
# File 'lib/http/features/digest_auth.rb', line 53 def around_request(request) response = yield request return response unless digest_challenge?(response) response.flush yield (request, response) end |
#authorize(request, response) ⇒ HTTP::Request
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build an authorized copy of the request using the digest challenge
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/http/features/digest_auth.rb', line 79 def (request, response) www_auth = response.headers[WWW_AUTHENTICATE] #: String challenge = parse_challenge(www_auth) headers = request.headers.dup headers.set Headers::AUTHORIZATION, build_auth(request, challenge) Request.new( verb: request.verb, uri: request.uri, headers: headers, proxy: request.proxy, body: request.body.source, version: request.version, uri_normalizer: request.uri_normalizer ) end |
#build_auth(request, challenge) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build the Authorization header value
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/http/features/digest_auth.rb', line 116 def build_auth(request, challenge) algorithm = challenge.fetch("algorithm", "MD5") qop = select_qop(challenge["qop"]) nonce = challenge.fetch("nonce") cnonce = SecureRandom.hex(16) nonce_count = "00000001" uri = String(request.uri.request_uri) ha1 = compute_ha1(algorithm, challenge.fetch("realm"), nonce, cnonce) ha2 = compute_ha2(algorithm, String(request.verb).upcase, uri) compute_auth_header(algorithm: algorithm, qop: qop, nonce: nonce, cnonce: cnonce, nonce_count: nonce_count, uri: uri, ha1: ha1, ha2: ha2, challenge: challenge) end |
#build_header(username:, realm:, nonce:, uri:, qop:, nonce_count:, cnonce:, response:, opaque:, algorithm:) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build the Digest Authorization header string
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/http/features/digest_auth.rb', line 213 def build_header(username:, realm:, nonce:, uri:, qop:, nonce_count:, cnonce:, response:, opaque:, algorithm:) parts = [ %(username="#{username}"), %(realm="#{realm}"), %(nonce="#{nonce}"), %(uri="#{uri}") ] parts.push(%(qop=#{qop}), %(nc=#{nonce_count}), %(cnonce="#{cnonce}")) if qop parts << %(response="#{response}") parts << %(opaque="#{opaque}") if opaque parts << %(algorithm=#{algorithm}) "Digest #{parts.join(', ')}" end |
#compute_auth_header(algorithm:, qop:, nonce:, cnonce:, nonce_count:, uri:, ha1:, ha2:, challenge:) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compute digest and build the Authorization header string
135 136 137 138 139 140 141 142 |
# File 'lib/http/features/digest_auth.rb', line 135 def compute_auth_header(algorithm:, qop:, nonce:, cnonce:, nonce_count:, uri:, ha1:, ha2:, challenge:) response = compute_response(algorithm, ha1, ha2, nonce: nonce, nonce_count: nonce_count, cnonce: cnonce, qop: qop) build_header(username: @user, realm: challenge.fetch("realm"), nonce: nonce, uri: uri, qop: qop, nonce_count: nonce_count, cnonce: cnonce, response: response, opaque: challenge["opaque"], algorithm: algorithm) end |
#compute_ha1(algorithm, realm, nonce, cnonce) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compute HA1 per RFC 2617
162 163 164 165 166 167 168 169 170 |
# File 'lib/http/features/digest_auth.rb', line 162 def compute_ha1(algorithm, realm, nonce, cnonce) base = hex_digest(algorithm, "#{@user}:#{realm}:#{@pass}") if algorithm.end_with?("-sess") hex_digest(algorithm, "#{base}:#{nonce}:#{cnonce}") else base end end |
#compute_ha2(algorithm, method, uri) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compute HA2 per RFC 2617
176 177 178 |
# File 'lib/http/features/digest_auth.rb', line 176 def compute_ha2(algorithm, method, uri) hex_digest(algorithm, "#{method}:#{uri}") end |
#compute_response(algorithm, ha1, ha2, nonce:, nonce_count:, cnonce:, qop:) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compute the final digest response value
191 192 193 194 195 196 197 |
# File 'lib/http/features/digest_auth.rb', line 191 def compute_response(algorithm, ha1, ha2, nonce:, nonce_count:, cnonce:, qop:) if qop hex_digest(algorithm, "#{ha1}:#{nonce}:#{nonce_count}:#{cnonce}:#{qop}:#{ha2}") else hex_digest(algorithm, "#{ha1}:#{nonce}:#{ha2}") end end |
#digest_challenge?(response) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Check if the response contains a digest authentication challenge
68 69 70 71 |
# File 'lib/http/features/digest_auth.rb', line 68 def digest_challenge?(response) www_auth = response.headers[WWW_AUTHENTICATE] #: String? response.status.code == 401 && www_auth&.start_with?("Digest ") == true end |
#hex_digest(algorithm, data) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compute a hex digest using the specified algorithm
205 206 207 |
# File 'lib/http/features/digest_auth.rb', line 205 def hex_digest(algorithm, data) ALGORITHMS.fetch(algorithm.sub(/-sess\z/i, "")).hexdigest(data) end |
#parse_challenge(header) ⇒ Hash{String => String}
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Parse the WWW-Authenticate header into a parameter hash
101 102 103 104 105 106 107 108 |
# File 'lib/http/features/digest_auth.rb', line 101 def parse_challenge(header) params = {} #: Hash[String, String] header.sub(/\ADigest\s+/i, "").scan(/(\w+)=(?:"([^"]*)"|([\w-]+))/) do |match| key = match[0] #: String params[key] = format("%s", match[1] || match[2]) end params end |
#select_qop(qop_str) ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Select the best qop value from the challenge
149 150 151 152 153 154 155 156 |
# File 'lib/http/features/digest_auth.rb', line 149 def select_qop(qop_str) return unless qop_str qops = qop_str.split(",").map(&:strip) return "auth" if qops.include?("auth") qops.first end |