module ActionController::HttpAuthentication::Digest
def authenticate(request, realm, &password_procedure)
def authenticate(request, realm, &password_procedure) request.authorization && validate_digest_response(request, realm, &password_procedure) end
def authentication_header(controller, realm)
def authentication_header(controller, realm) secret_key = secret_token(controller.request) nonce = self.nonce(secret_key) opaque = opaque(secret_key) controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}") end
def authentication_request(controller, realm, message = nil)
def authentication_request(controller, realm, message = nil) message ||= "HTTP Digest: Access denied.\n" authentication_header(controller, realm) controller.status = 401 controller.response_body = message end
def decode_credentials(header)
def decode_credentials(header) ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair| key, value = pair.split("=", 2) [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")] end] end
def decode_credentials_header(request)
def decode_credentials_header(request) decode_credentials(request.authorization) end
def encode_credentials(http_method, credentials, password, password_is_ha1)
def encode_credentials(http_method, credentials, password, password_is_ha1) credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1) "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ") end
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
`password_is_ha1` is set to `true` by default, since best practice is to store
decoded `credentials` and the expected `password` Optional parameter
Returns the expected response for a request of `http_method` to `uri` with the
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true) ha1 = password_is_ha1 ? password : ha1(credentials, password) ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":")) OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":")) end
def ha1(credentials, password)
def ha1(credentials, password) OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":")) end
def nonce(secret_key, time = Time.now)
secret key from the Rails session secret generated upon creation of project.
The nonce is opaque to the client. Composed of Time, and hash of Time with
issues involved see Section 4 of this document.
or PATCH requests, and a time-stamp for GET requests. For more details on the
implementation might choose to use one-time nonces or digests for POST, PUT,
previously used digest, in order to protect against a replay attack. Or, an
An implementation might choose not to accept a previously used nonce or a
spoofing is not that hard.)
a single user often go through different proxies in the farm. Also, IP address
originally got it. However, that would break proxy farms, where requests from
server the ability to limit the reuse of the nonce to the same client that
including the IP address of the client in the nonce would appear to offer the
ETag prevents a replay request for an updated version of the resource. (Note:
the server can limit the time of the nonce's validity. The inclusion of the
from that header or if the time-stamp value is not recent enough. In this way
authentication header and reject the request if it did not match the nonce
server would recalculate the hash portion after receiving the client
private-key is data known only to the server. With a nonce of this form a
is the value of the HTTP ETag header associated with the requested entity, and
where time-stamp is a server-generated time or other non-repeating value, ETag
time-stamp H(time-stamp ":" ETag ":" private-key)
constructed as the base 64 encoding of
implementation depends on a good choice. A nonce might, for example, be
The contents of the nonce are implementation dependent. The quality of the
as a quoted string, the double-quote character is not allowed.
hexadecimal data. Specifically, since the string is passed in the header lines
401 response is made. It is recommended that this string be base64 or
A server-specified data string which should be uniquely generated each time a
Uses an MD5 digest based on time to generate a value to be used only once.
def nonce(secret_key, time = Time.now) t = time.to_i hashed = [t, secret_key] digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":")) ::Base64.strict_encode64("#{t}:#{digest}") end
def opaque(secret_key)
def opaque(secret_key) OpenSSL::Digest::MD5.hexdigest(secret_key) end
def secret_token(request)
def secret_token(request) key_generator = request.key_generator http_auth_salt = request.http_auth_salt key_generator.generate_key(http_auth_salt) end
def validate_digest_response(request, realm, &password_procedure)
expected value. First try the password as a ha1 digest password. If this
Returns false unless the request credentials response value matches the
def validate_digest_response(request, realm, &password_procedure) secret_key = secret_token(request) credentials = decode_credentials_header(request) valid_nonce = validate_nonce(secret_key, request, credentials[:nonce]) if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque] password = password_procedure.call(credentials[:username]) return false unless password method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD") uri = credentials[:uri] [true, false].any? do |trailing_question_mark| [true, false].any? do |password_is_ha1| _uri = trailing_question_mark ? uri + "?" : uri expected = expected_response(method, _uri, credentials, password, password_is_ha1) expected == credentials[:response] end end end end
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
the Stale directive is implemented. This would allow a user to use new nonce
or POST, and if the client is a browser or web service. Can be much shorter if
Might want a shorter timeout depending on whether the request is a PATCH, PUT,
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60) return false if value.nil? t = ::Base64.decode64(value).split(":").first.to_i nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end