Handling HTTP Digest Authentication using Ruby

You may have encountered an authentication-required header when making HTTP requests to various web services or websites. While the basic authentication scheme for HTTP simply requires you to submit username and password details for that particular URL, there are other protocols which enhance the level of security that can be applied to an HTTP connection. HTTP Digest Authentication, which has been specified by this RFC, is one such method than can be used to provide enhanced security for HTTP-based web service connections.

HTTP Digest Authentication is based primarily on two concepts; MD5 hashing and dynamically-generated nonce values. The nonce values are used to ensure that a particular call to the server is part of an ongoing conversation between that particular client and the server, without anyone else (hackers 😉 ) butting in.

To call a web service employing HTTP Digest authentication, you must first send an empty request to the server which will return a server-generated nonce value for you. Then you can proceed to the next steps in authentication, which involves hashing your username and password with the nonce in the middle, as well as setting various header values.

Recently, I implemented some Ruby code to call a web service using HTTP Digest authentication, and I thought of sharing my code. I based most of it on the HTTP Digest Auth Ruby Gem.

Here’s the code:

# Build header authorization block to send to given URI
    def build_header_auth(uri, version, httpmethod) response = get_auth_response(uri)
	    @cnonce = Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
        @@nonce_count += 1 response['www-authenticate'] =~ /^(\w+) (.*)/
        challenge = $2
        params = {}
        challenge.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }

        a_1 = "#{@username}:#{params['realm']}:#{@password}" #username, realm and password
        a_2 = "#{httpmethod}:#{uri}" #method and path
        request_digest = '' request_digest << Digest::MD5.hexdigest(a_1)
        request_digest << ':' << params['nonce']
        request_digest << ':' << ('%08x' % @@nonce_count)
        request_digest << ':' << @cnonce
        request_digest << ':' << params['qop']
        request_digest << ':' << Digest::MD5.hexdigest(a_2)

        header = []
        header << "Digest username=\"#{@username}\""
        header << "realm=\"#{params['realm']}\""
        header << "qop=#{params['qop']}"
        header << "algorithm=MD5"
        header << "uri=\"#{uri}\""
        header << "nonce=\"#{params['nonce']}\""
        header << "nc=#{'%08x' % @@nonce_count}"
        header << "cnonce=\"#{@cnonce}\""
        header << "response=\"#{Digest::MD5.hexdigest(request_digest)}\""
        header << "opaque=\"#{params['opaque']}\""
        header_auth_str = header.join(', ')

# Build request (allows reuse)
    def build_request()
        @http = Net::HTTP.new(@uri.host, @uri.port)
        @http.use_ssl = true
        @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end

# We need to get a response with a WWW-Authenticate request header
    def get_auth_response(uri)
        url = BASE_URL + uri
        @uri = URI.parse(url)
        build_request()
        req = Net::HTTP::Get.new(@uri.request_uri)
        response = @http.request(req)
        return response
    end

Good Luck!