Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

HTTP Digest Authentication #201

Merged
merged 8 commits into from

2 participants

@elhu

Hi,

This is a pull request for a middleware that allows em-http-request to handle servers requiring http digest authentication.

You will find an example, with a small server written using WEBRick and a client to demonstrate a use-case.
There are also some specs, it's still quite shallow but it should cover the basics.

If you have any questions, I'll be glad to answer them!

Best,

elhu added some commits
@elhu elhu Added middleware for digest authentication f98ac9d
@elhu elhu Rework of the WWW_AUTHENTICATE header parsing
The previous method was failing in a lot of corner cases. This one is
able to withstand what I've thrown to it so far.
a4147f7
@elhu elhu Example files for http digest auth
With both server and clients. I used WebRick for the server because it
comes standard with Ruby, and supports HTTP digest auth.
07522dc
@elhu elhu Basic specs for http digest auth 49562d7
@elhu elhu Fixed formatting
Added new lines at the end of files where needed.
e4f0b3d
lib/em-http/middleware/digest_auth.rb
((80 lines not shown))
+ "nonce=\"#{params[:nonce]}\"",
+ "response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",
+ ]
+ if params[:qop]
+ header << "qop=#{qop}"
+ header << "nc=#{'%08x' % @nonce_count}"
+ header << "cnonce=\"#{cnonce}\""
+ end
+ header << "opaque=\"#{params[:opaque]}\"" if params.key? :opaque
+ header.join(', ')
+ end
+
+ # Process the WWW_AUTHENTICATE header to get the authentication parameters
+ def get_params(www_authenticate)
+ www_authenticate.scan(/(\w+)=(.*?)(,|\z)/).each do |match|
+ @digest_params[match[0].to_sym] = match[1].chomp('"').reverse.chomp('"').reverse
@igrigorik Owner

Why not put the optional "s into the regex and just don't capture them? Seems simpler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/em-http/middleware/digest_auth.rb
@@ -0,0 +1,114 @@
+module EventMachine
+ module Middleware
+ require 'digest'
+ require 'securerandom'
+ require 'cgi'
+
+ class DigestAuth
+ attr_accessor :auth_digest, :is_digest_auth
+
+ def initialize(www_authenticate, opts = {})
+ @nonce_count = -1
+ @opts = {}
+ # Symbolize the opts hash's keys
+ opts.each {|k, v| @opts[k.to_sym] = v}
@igrigorik Owner

we try to use symbols in all configs, let's just stick to that here, no need for both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/em-http/middleware/digest_auth.rb
((5 lines not shown))
+ require 'cgi'
+
+ class DigestAuth
+ attr_accessor :auth_digest, :is_digest_auth
+
+ def initialize(www_authenticate, opts = {})
+ @nonce_count = -1
+ @opts = {}
+ # Symbolize the opts hash's keys
+ opts.each {|k, v| @opts[k.to_sym] = v}
+ @digest_params = {
+ algorithm: 'MD5' # MD5 is the default hashing algorithm
+ }
+ chunks = www_authenticate.split(' ')
+ if (@is_digest_auth = 'Digest' == chunks.shift)
+ get_params(chunks.join(' '))
@igrigorik Owner

get_params(www_authenticate) if www_authenticate =~ /^Digest/

No need to split and rejoin the same string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/em-http/middleware/digest_auth.rb
((31 lines not shown))
+ [head, body]
+ end
+
+ def response(resp)
+ # If the server responds with the Authentication-Info header, set the nonce to the new value
+ if @is_digest_auth && (authentication_info = resp.response_header['Authentication-Info'])
+ authentication_info =~ /nextnonce=(.*?)(,|\z)/
+ @digest_params[:nonce] = $1.chomp('"').reverse.chomp('"').reverse
+ end
+ end
+
+ def build_auth_digest(method, uri, params = nil)
+ params = @opts.merge(@digest_params) if !params
+ nonce_count = next_nonce
+
+ user = CGI.unescape params[:username]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@elhu

Hi,

I made a few changes according to your previous comments. Thanks a lot for the one about the " in the regexp by the way, I couldn't seem to wrap my head around it yesterday and your question was the trigger to finally have the clean regexp I was looking for.

@igrigorik igrigorik merged commit 8605374 into igrigorik:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 31, 2012
  1. @elhu
  2. @elhu

    Rework of the WWW_AUTHENTICATE header parsing

    elhu authored
    The previous method was failing in a lot of corner cases. This one is
    able to withstand what I've thrown to it so far.
  3. @elhu

    Example files for http digest auth

    elhu authored
    With both server and clients. I used WebRick for the server because it
    comes standard with Ruby, and supports HTTP digest auth.
  4. @elhu

    Basic specs for http digest auth

    elhu authored
  5. @elhu

    Fixed formatting

    elhu authored
    Added new lines at the end of files where needed.
Commits on Aug 1, 2012
  1. @elhu
  2. @elhu
  3. @elhu
This page is out of date. Refresh to see the latest.
View
25 examples/digest_auth/client.rb
@@ -0,0 +1,25 @@
+$: << 'lib' << '../../lib'
+
+require 'em-http'
+require 'em-http/middleware/digest_auth'
+
+digest_config = {
+ :username => 'digest_username',
+ :password => 'digest_password'
+}
+
+EM.run do
+
+ conn_handshake = EM::HttpRequest.new('http://localhost:3000')
+ http_handshake = conn_handshake.get
+
+ http_handshake.callback do
+ conn = EM::HttpRequest.new('http://localhost:3000')
+ conn.use EM::Middleware::DigestAuth, http_handshake.response_header['WWW_AUTHENTICATE'], digest_config
+ http = conn.get
+ http.callback do
+ puts http.response
+ EM.stop
+ end
+ end
+end
View
28 examples/digest_auth/server.rb
@@ -0,0 +1,28 @@
+require 'webrick'
+
+include WEBrick
+
+config = { :Realm => 'DigestAuth_REALM' }
+
+htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
+htdigest.set_passwd config[:Realm], 'digest_username', 'digest_password'
+htdigest.flush
+
+config[:UserDB] = htdigest
+
+digest_auth = WEBrick::HTTPAuth::DigestAuth.new config
+
+class TestServlet < HTTPServlet::AbstractServlet
+ def do_GET(req, res)
+ @options[0][:authenticator].authenticate req, res
+ res.body = "You are authenticated to see the super secret data\n"
+ end
+end
+
+s = HTTPServer.new(:Port => 3000)
+s.mount('/', TestServlet, {:authenticator => digest_auth})
+trap("INT") do
+ File.delete('my_password_file')
+ s.shutdown
+end
+s.start
View
112 lib/em-http/middleware/digest_auth.rb
@@ -0,0 +1,112 @@
+module EventMachine
+ module Middleware
+ require 'digest'
+ require 'securerandom'
+
+ class DigestAuth
+ include EventMachine::HttpEncoding
+
+ attr_accessor :auth_digest, :is_digest_auth
+
+ def initialize(www_authenticate, opts = {})
+ @nonce_count = -1
+ @opts = opts
+ @digest_params = {
+ algorithm: 'MD5' # MD5 is the default hashing algorithm
+ }
+ if (@is_digest_auth = www_authenticate =~ /^Digest/)
+ get_params(www_authenticate)
+ end
+ end
+
+ def request(client, head, body)
+ # Allow HTTP basic auth fallback
+ if @is_digest_auth
+ head['Authorization'] = build_auth_digest(client.req.method, client.req.uri.path, @opts.merge(@digest_params))
+ else
+ head['Authorization'] = [@opts[:username], @opts[:password]]
+ end
+ [head, body]
+ end
+
+ def response(resp)
+ # If the server responds with the Authentication-Info header, set the nonce to the new value
+ if @is_digest_auth && (authentication_info = resp.response_header['Authentication-Info'])
+ authentication_info =~ /nextnonce="?(.*?)"?(,|\z)/
+ @digest_params[:nonce] = $1
+ end
+ end
+
+ def build_auth_digest(method, uri, params = nil)
+ params = @opts.merge(@digest_params) if !params
+ nonce_count = next_nonce
+
+ user = unescape params[:username]
+ password = unescape params[:password]
+
+ splitted_algorithm = params[:algorithm].split('-')
+ sess = "-sess" if splitted_algorithm[1]
+ raw_algorithm = splitted_algorithm[0]
+ if %w(MD5 SHA1 SHA2 SHA256 SHA384 SHA512 RMD160).include? raw_algorithm
+ algorithm = eval("Digest::#{raw_algorithm}")
+ else
+ raise "Unknown algorithm: #{raw_algorithm}"
+ end
+ qop = params[:qop]
+ cnonce = make_cnonce if qop or sess
+ a1 = if sess
+ [
+ algorithm.hexdigest("#{params[:username]}:#{params[:realm]}:#{params[:password]}"),
+ params[:nonce],
+ cnonce,
+ ].join ':'
+ else
+ "#{params[:username]}:#{params[:realm]}:#{params[:password]}"
+ end
+ ha1 = algorithm.hexdigest a1
+ ha2 = algorithm.hexdigest "#{method}:#{uri}"
+
+ request_digest = [ha1, params[:nonce]]
+ request_digest.push(('%08x' % @nonce_count), cnonce, qop) if qop
+ request_digest << ha2
+ request_digest = request_digest.join ':'
+ header = [
+ "Digest username=\"#{params[:username]}\"",
+ "realm=\"#{params[:realm]}\"",
+ "algorithm=#{raw_algorithm}#{sess}",
+ "uri=\"#{uri}\"",
+ "nonce=\"#{params[:nonce]}\"",
+ "response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",
+ ]
+ if params[:qop]
+ header << "qop=#{qop}"
+ header << "nc=#{'%08x' % @nonce_count}"
+ header << "cnonce=\"#{cnonce}\""
+ end
+ header << "opaque=\"#{params[:opaque]}\"" if params.key? :opaque
+ header.join(', ')
+ end
+
+ # Process the WWW_AUTHENTICATE header to get the authentication parameters
+ def get_params(www_authenticate)
+ www_authenticate.scan(/(\w+)="?(.*?)"?(,|\z)/).each do |match|
+ @digest_params[match[0].to_sym] = match[1]
+ end
+ end
+
+ # Generate a client nonce
+ def make_cnonce
+ Digest::MD5.hexdigest [
+ Time.now.to_i,
+ $$,
+ SecureRandom.random_number(2**32),
+ ].join ':'
+ end
+
+ # Keep track of the nounce count
+ def next_nonce
+ @nonce_count += 1
+ end
+ end
+ end
+end
View
48 spec/digest_auth_spec.rb
@@ -0,0 +1,48 @@
+require 'helper'
+
+$: << 'lib' << '../lib'
+
+require 'em-http/middleware/digest_auth'
+
+describe 'Digest Auth Authentication header generation' do
+ before :each do
+ @reference_header = 'Digest username="digest_username", realm="DigestAuth_REALM", algorithm=MD5, uri="/", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", response="96829962ffc31fa2852f86dc7f9f609b", opaque="BzdNK3gsJ2ixTrBJ"'
+ end
+
+ it 'should generate the correct header' do
+ www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
+
+ params = {
+ username: 'digest_username',
+ password: 'digest_password'
+ }
+
+ middleware = EM::Middleware::DigestAuth.new(www_authenticate, params)
+ middleware.build_auth_digest('GET', '/').should == @reference_header
+ end
+
+ it 'should not generate the same header for a different user' do
+ www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
+
+ params = {
+ username: 'digest_username_2',
+ password: 'digest_password'
+ }
+
+ middleware = EM::Middleware::DigestAuth.new(www_authenticate, params)
+ middleware.build_auth_digest('GET', '/').should_not == @reference_header
+ end
+
+ it 'should not generate the same header if the nounce changes' do
+ www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg6", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
+
+ params = {
+ username: 'digest_username_2',
+ password: 'digest_password'
+ }
+
+ middleware = EM::Middleware::DigestAuth.new(www_authenticate, params)
+ middleware.build_auth_digest('GET', '/').should_not == @reference_header
+ end
+
+end
Something went wrong with that request. Please try again.