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
Changes from 5 commits
f98ac9d
a4147f7
07522dc
49562d7
e4f0b3d
67d4268
091c79f
8605374
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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} | ||
@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(' ')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not put the optional |
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
There was a problem hiding this comment.
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.