Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP Digest Authentication #201

Merged
merged 8 commits into from Jun 22, 2013
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions 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
28 changes: 28 additions & 0 deletions 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
114 changes: 114 additions & 0 deletions 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}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@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(' '))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

No need to split and rejoin the same string.

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.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]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

password = CGI.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].chomp('"').reverse.chomp('"').reverse
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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
48 changes: 48 additions & 0 deletions 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