123456789_123456789_123456789_123456789_123456789_

Class: WEBrick::HTTPAuth::DigestAuth

Relationships & Source Files
Namespace Children
Classes:
Extension / Inclusion / Inheritance Descendants
Subclasses:
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
Inherits: Object
Defined in: lib/webrick/httpauth/digestauth.rb

Overview

RFC 2617 Digest Access Authentication for ::WEBrick

Use this class to add digest authentication to a ::WEBrick servlet.

Here is an example of how to set up DigestAuth:

config = { :Realm => 'DigestAuth example realm' }

htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
htdigest.set_passwd config[:Realm], 'username', 'password'
htdigest.flush

config[:UserDB] = htdigest

digest_auth = WEBrick::HTTPAuth::DigestAuth.new config

When using this as with a servlet be sure not to create a new DigestAuth object in the servlet's #initialize. By default ::WEBrick creates a new servlet instance for every request and the DigestAuth object must be used across requests.

Constant Summary

Authenticator - Included

AuthException, AuthScheme, RequestField, ResponseField, ResponseInfoField

Class Method Summary

Instance Attribute Summary

  • #algorithm readonly

    Digest authentication algorithm.

  • #qop readonly

    Quality of protection.

Authenticator - Included

#logger

The logger for this authenticator.

#realm

The realm this authenticator covers.

#userdb

The user database for this authenticator.

Instance Method Summary

Authenticator - Included

#check_init

Initializes the authenticator from config

#check_scheme

Ensures req has credentials that can be authenticated.

#error, #info, #log

Constructor Details

.new(config, default = Config::DigestAuth) ⇒ DigestAuth

Creates a new DigestAuth instance. Be sure to use the same DigestAuth instance for multiple requests as it saves state between requests in order to perform authentication.

See Config::DigestAuth for default configuration entries

You must supply the following configuration entries:

:Realm

The name of the realm being protected.

:UserDB

A database of usernames and passwords. A WEBrick::HTTPAuth::Htdigest instance should be used.

[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 87

def initialize(config, default=Config::DigestAuth)
  check_init(config)
  @config                 = default.dup.update(config)
  @algorithm              = @config[:Algorithm]
  @domain                 = @config[:Domain]
  @qop                    = @config[:Qop]
  @use_opaque             = @config[:UseOpaque]
  @use_next_nonce         = @config[:UseNextNonce]
  @check_nc               = @config[:CheckNc]
  @use_auth_info_header   = @config[:UseAuthenticationInfoHeader]
  @nonce_expire_period    = @config[:NonceExpirePeriod]
  @nonce_expire_delta     = @config[:NonceExpireDelta]
  @internet_explorer_hack = @config[:InternetExplorerHack]

  case @algorithm
  when 'MD5','MD5-sess'
    @h = Digest::MD5
  when 'SHA1','SHA1-sess'  # it is a bonus feature :-)
    @h = Digest::SHA1
  else
    msg = format('Algorithm "%s" is not supported.', @algorithm)
    raise ArgumentError.new(msg)
  end

  @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid)
  @opaques = {}
  @last_nonce_expire = Time.now
  @mutex = Thread::Mutex.new
end

Class Method Details

.make_passwd(realm, user, pass)

Used by UserDB to create a digest password entry

[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 69

def self.make_passwd(realm, user, pass)
  pass ||= ""
  Digest::MD5::hexdigest([user, realm, pass].join(":"))
end

Instance Attribute Details

#algorithm (readonly)

Digest authentication algorithm

[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 59

attr_reader :algorithm

#qop (readonly)

Quality of protection. RFC 2617 defines “auth” and “auth-int”

[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 64

attr_reader :qop

Instance Method Details

#_authenticate(req, res) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 163

def _authenticate(req, res)
  unless digest_credentials = check_scheme(req)
    return false
  end

  auth_req = split_param_value(digest_credentials)
  if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
    req_params = MustParams + MustParamsAuth
  else
    req_params = MustParams
  end
  req_params.each{|key|
    unless auth_req.has_key?(key)
      error('%s: parameter missing. "%s"', auth_req['username'], key)
      raise HTTPStatus::BadRequest
    end
  }

  if !check_uri(req, auth_req)
    raise HTTPStatus::BadRequest
  end

  if auth_req['realm'] != @realm
    error('%s: realm unmatch. "%s" for "%s"',
          auth_req['username'], auth_req['realm'], @realm)
    return false
  end

  auth_req['algorithm'] ||= 'MD5'
  if auth_req['algorithm'].upcase != @algorithm.upcase
    error('%s: algorithm unmatch. "%s" for "%s"',
          auth_req['username'], auth_req['algorithm'], @algorithm)
    return false
  end

  if (@qop.nil? && auth_req.has_key?('qop')) ||
     (@qop && (! @qop.member?(auth_req['qop'])))
    error('%s: the qop is not allowed. "%s"',
          auth_req['username'], auth_req['qop'])
    return false
  end

  password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db)
  unless password
    error('%s: the user is not allowed.', auth_req['username'])
    return false
  end

  nonce_is_invalid = false
  if @use_opaque
    info("@opaque = %s", @opaque.inspect) if $DEBUG
    if !(opaque = auth_req['opaque'])
      error('%s: opaque is not given.', auth_req['username'])
      nonce_is_invalid = true
    elsif !(opaque_struct = @opaques[opaque])
      error('%s: invalid opaque is given.', auth_req['username'])
      nonce_is_invalid = true
    elsif !check_opaque(opaque_struct, req, auth_req)
      @opaques.delete(auth_req['opaque'])
      nonce_is_invalid = true
    end
  elsif !check_nonce(req, auth_req)
    nonce_is_invalid = true
  end

  if /-sess$/i =~ auth_req['algorithm']
    ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce'])
  else
    ha1 = password
  end

  if auth_req['qop'] == "auth" || auth_req['qop'] == nil
    ha2 = hexdigest(req.request_method, auth_req['uri'])
    ha2_res = hexdigest("", auth_req['uri'])
  elsif auth_req['qop'] == "auth-int"
    body_digest = @h.new
    req.body { |chunk| body_digest.update(chunk) }
    body_digest = body_digest.hexdigest
    ha2 = hexdigest(req.request_method, auth_req['uri'], body_digest)
    ha2_res = hexdigest("", auth_req['uri'], body_digest)
  end

  if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
    param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key|
      auth_req[key]
    }.join(':')
    digest     = hexdigest(ha1, param2, ha2)
    digest_res = hexdigest(ha1, param2, ha2_res)
  else
    digest     = hexdigest(ha1, auth_req['nonce'], ha2)
    digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res)
  end

  if digest != auth_req['response']
    error("%s: digest unmatch.", auth_req['username'])
    return false
  elsif nonce_is_invalid
    error('%s: digest is valid, but nonce is not valid.',
          auth_req['username'])
    return :nonce_is_stale
  elsif @use_auth_info_header
    auth_info = {
      'nextnonce' => generate_next_nonce(req),
      'rspauth'   => digest_res
    }
    if @use_opaque
      opaque_struct.time  = req.request_time
      opaque_struct.nonce = auth_info['nextnonce']
      opaque_struct.nc    = "%08x" % (auth_req['nc'].hex + 1)
    end
    if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
      ['qop','cnonce','nc'].each{|key|
        auth_info[key] = auth_req[key]
      }
    end
    res[@resp_info_field] = auth_info.keys.map{|key|
      if key == 'nc'
        key + '=' + auth_info[key]
      else
        key + "=" + HTTPUtils::quote(auth_info[key])
      end
    }.join(', ')
  end
  info('%s: authentication succeeded.', auth_req['username'])
  req.user = auth_req['username']
  return true
end

#authenticate(req, res)

Authenticates a req and returns a 401 Unauthorized using res if the authentication was not correct.

[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 121

def authenticate(req, res)
  unless result = @mutex.synchronize{ _authenticate(req, res) }
    challenge(req, res)
  end
  if result == :nonce_is_stale
    challenge(req, res, true)
  end
  return true
end

#challenge(req, res, stale = false)

Returns a challenge response which asks for authentication information

Raises:

  • (@auth_exception)
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 134

def challenge(req, res, stale=false)
  nonce = generate_next_nonce(req)
  if @use_opaque
    opaque = generate_opaque(req)
    @opaques[opaque].nonce = nonce
  end

  param = Hash.new
  param["realm"]  = HTTPUtils::quote(@realm)
  param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain
  param["nonce"]  = HTTPUtils::quote(nonce)
  param["opaque"] = HTTPUtils::quote(opaque) if opaque
  param["stale"]  = stale.to_s
  param["algorithm"] = @algorithm
  param["qop"]    = HTTPUtils::quote(@qop.to_a.join(",")) if @qop

  res[@response_field] =
    "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ")
  info("%s: %s", @response_field, res[@response_field]) if $DEBUG
  raise @auth_exception
end

#check_nonce(req, auth_req) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 306

def check_nonce(req, auth_req)
  username = auth_req['username']
  nonce = auth_req['nonce']

  pub_time, pk = nonce.unpack("m*")[0].split(":", 2)
  if (!pub_time || !pk)
    error("%s: empty nonce is given", username)
    return false
  elsif (hexdigest(pub_time, @instance_key)[0,32] != pk)
    error("%s: invalid private-key: %s for %s",
          username, hexdigest(pub_time, @instance_key)[0,32], pk)
    return false
  end

  diff_time = req.request_time.to_i - pub_time.to_i
  if (diff_time < 0)
    error("%s: difference of time-stamp is negative.", username)
    return false
  elsif diff_time > @nonce_expire_period
    error("%s: nonce is expired.", username)
    return false
  end

  return true
end

#check_opaque(opaque_struct, req, auth_req) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 349

def check_opaque(opaque_struct, req, auth_req)
  if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce)
    error('%s: nonce unmatched. "%s" for "%s"',
          auth_req['username'], auth_req['nonce'], opaque_struct.nonce)
    return false
  elsif !check_nonce(req, auth_req)
    return false
  end
  if (@check_nc && auth_req['nc'] != opaque_struct.nc)
    error('%s: nc unmatched."%s" for "%s"',
          auth_req['username'], auth_req['nc'], opaque_struct.nc)
    return false
  end
  true
end

#check_uri(req, auth_req) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 365

def check_uri(req, auth_req)
  uri = auth_req['uri']
  if uri != req.request_uri.to_s && uri != req.unparsed_uri &&
     (@internet_explorer_hack && uri != req.path)
    error('%s: uri unmatch. "%s" for "%s"', auth_req['username'],
          auth_req['uri'], req.request_uri.to_s)
    return false
  end
  true
end

#generate_next_nonce(req) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 299

def generate_next_nonce(req)
  now = "%012d" % req.request_time.to_i
  pk  = hexdigest(now, @instance_key)[0,32]
  nonce = [now + ":" + pk].pack("m0") # it has 60 length of chars.
  nonce
end

#generate_opaque(req) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 332

def generate_opaque(req)
  @mutex.synchronize{
    now = req.request_time
    if now - @last_nonce_expire > @nonce_expire_delta
      @opaques.delete_if{|key,val|
        (now - val.time) > @nonce_expire_period
      }
      @last_nonce_expire = now
    end
    begin
      opaque = Utils::random_string(16)
    end while @opaques[opaque]
    @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001')
    opaque
  }
end

#hexdigest(*args) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 376

def hexdigest(*args)
  @h.hexdigest(args.join(":"))
end

#split_param_value(string) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpauth/digestauth.rb', line 291

def split_param_value(string)
  ret = {}
  string.scan(/\G\s*([\w\-.*%!]+)=\s*(?:\"((?>\\.|[^\"])*)\"|([^,\"]*))\s*,?/) do
    ret[$1] = $3 || $2.gsub(/\\(.)/, "\\1")
  end
  ret
end