File: webrick/httpauth/digestauth.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: WEBrick#20
  module: HTTPAuth#21
  class: DigestAuth#22
includes
  Authenticator ( WEBrick::HTTPAuth )
inherits from
  Object ( Builtin-Module )
has properties
constant: AuthScheme #25
constant: OpaqueInfo #26
attribute: algorithm [R] #27
attribute: qop [R] #27
class method: make_passwd / 3 #29
method: initialize / 2 #34
method: authenticate / 2 #65
method: challenge / 3 #75
constant: MustParams #99
constant: MustParamsAuth #100
method: _authenticate / 2 #102
method: split_param_value / 1 #230
method: generate_next_nonce / 1 #253
method: check_nonce / 2 #260
method: generate_opaque / 1 #286
method: check_opaque / 3 #303
method: check_uri / 2 #319
method: hexdigest / 1 #330
  class: ProxyDigestAuth#335
includes
  ProxyAuthenticator ( WEBrick::HTTPAuth )
inherits from
  DigestAuth ( WEBrick::HTTPAuth )
has properties
method: check_uri / 2 #338

Class Hierarchy

Code

   1  #
   2  # httpauth/digestauth.rb -- HTTP digest access authentication
   3  #
   4  # Author: IPR -- Internet Programming with Ruby -- writers
   5  # Copyright (c) 2003 Internet Programming with Ruby writers.
   6  # Copyright (c) 2003 H.M.
   7  #
   8  # The original implementation is provided by H.M.
   9  #   URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name=
  10  #        %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB
  11  #
  12  # $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
  13 
  14  require 'webrick/config'
  15  require 'webrick/httpstatus'
  16  require 'webrick/httpauth/authenticator'
  17  require 'digest/md5'
  18  require 'digest/sha1'
  19 
  20  module WEBrick
  21    module HTTPAuth
  22      class DigestAuth
  23        include Authenticator
  24 
  25        AuthScheme = "Digest"
  26        OpaqueInfo = Struct.new(:time, :nonce, :nc)
  27        attr_reader :algorithm, :qop
  28 
  29        def self.make_passwd(realm, user, pass)
  30          pass ||= ""
  31          Digest::MD5::hexdigest([user, realm, pass].join(":"))
  32        end
  33 
  34        def initialize(config, default=Config::DigestAuth)
  35          check_init(config)
  36          @config                 = default.dup.update(config)
  37          @algorithm              = @config[:Algorithm]
  38          @domain                 = @config[:Domain]
  39          @qop                    = @config[:Qop]
  40          @use_opaque             = @config[:UseOpaque]
  41          @use_next_nonce         = @config[:UseNextNonce]
  42          @check_nc               = @config[:CheckNc]
  43          @use_auth_info_header   = @config[:UseAuthenticationInfoHeader]
  44          @nonce_expire_period    = @config[:NonceExpirePeriod]
  45          @nonce_expire_delta     = @config[:NonceExpireDelta]
  46          @internet_explorer_hack = @config[:InternetExplorerHack]
  47          @opera_hack             = @config[:OperaHack]
  48 
  49          case @algorithm
  50          when 'MD5','MD5-sess'
  51            @h = Digest::MD5
  52          when 'SHA1','SHA1-sess'  # it is a bonus feature :-)
  53            @h = Digest::SHA1
  54          else
  55            msg = format('Alogrithm "%s" is not supported.', @algorithm)
  56            raise ArgumentError.new(msg)
  57          end
  58 
  59          @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid)
  60          @opaques = {}
  61          @last_nonce_expire = Time.now
  62          @mutex = Mutex.new
  63        end
  64 
  65        def authenticate(req, res)
  66          unless result = @mutex.synchronize{ _authenticate(req, res) }
  67            challenge(req, res)
  68          end
  69          if result == :nonce_is_stale
  70            challenge(req, res, true)
  71          end
  72          return true
  73        end
  74 
  75        def challenge(req, res, stale=false)
  76          nonce = generate_next_nonce(req)
  77          if @use_opaque
  78            opaque = generate_opaque(req)
  79            @opaques[opaque].nonce = nonce
  80          end
  81 
  82          param = Hash.new
  83          param["realm"]  = HTTPUtils::quote(@realm)
  84          param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain
  85          param["nonce"]  = HTTPUtils::quote(nonce)
  86          param["opaque"] = HTTPUtils::quote(opaque) if opaque
  87          param["stale"]  = stale.to_s
  88          param["algorithm"] = @algorithm
  89          param["qop"]    = HTTPUtils::quote(@qop.to_a.join(",")) if @qop
  90 
  91          res[@response_field] =
  92            "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ")
  93          info("%s: %s", @response_field, res[@response_field]) if $DEBUG
  94          raise @auth_exception
  95        end
  96 
  97        private
  98 
  99        MustParams = ['username','realm','nonce','uri','response']
 100        MustParamsAuth = ['cnonce','nc']
 101 
 102        def _authenticate(req, res)
 103          unless digest_credentials = check_scheme(req)
 104            return false
 105          end
 106 
 107          auth_req = split_param_value(digest_credentials)
 108          if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
 109            req_params = MustParams + MustParamsAuth
 110          else
 111            req_params = MustParams
 112          end
 113          req_params.each{|key|
 114            unless auth_req.has_key?(key)
 115              error('%s: parameter missing. "%s"', auth_req['username'], key)
 116              raise HTTPStatus::BadRequest
 117            end
 118          }
 119 
 120          if !check_uri(req, auth_req)
 121            raise HTTPStatus::BadRequest  
 122          end
 123 
 124          if auth_req['realm'] != @realm  
 125            error('%s: realm unmatch. "%s" for "%s"',
 126                  auth_req['username'], auth_req['realm'], @realm)
 127            return false
 128          end
 129 
 130          auth_req['algorithm'] ||= 'MD5' 
 131          if auth_req['algorithm'] != @algorithm &&
 132             (@opera_hack && auth_req['algorithm'] != @algorithm.upcase)
 133            error('%s: algorithm unmatch. "%s" for "%s"',
 134                  auth_req['username'], auth_req['algorithm'], @algorithm)
 135            return false
 136          end
 137 
 138          if (@qop.nil? && auth_req.has_key?('qop')) ||
 139             (@qop && (! @qop.member?(auth_req['qop'])))
 140            error('%s: the qop is not allowed. "%s"',
 141                  auth_req['username'], auth_req['qop'])
 142            return false
 143          end
 144 
 145          password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db)
 146          unless password
 147            error('%s: the user is not allowd.', auth_req['username'])
 148            return false
 149          end
 150 
 151          nonce_is_invalid = false
 152          if @use_opaque
 153            info("@opaque = %s", @opaque.inspect) if $DEBUG
 154            if !(opaque = auth_req['opaque'])
 155              error('%s: opaque is not given.', auth_req['username'])
 156              nonce_is_invalid = true
 157            elsif !(opaque_struct = @opaques[opaque])
 158              error('%s: invalid opaque is given.', auth_req['username'])
 159              nonce_is_invalid = true
 160            elsif !check_opaque(opaque_struct, req, auth_req)
 161              @opaques.delete(auth_req['opaque'])
 162              nonce_is_invalid = true
 163            end
 164          elsif !check_nonce(req, auth_req)
 165            nonce_is_invalid = true
 166          end
 167 
 168          if /-sess$/ =~ auth_req['algorithm'] ||
 169             (@opera_hack && /-SESS$/ =~ auth_req['algorithm'])
 170            ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce'])
 171          else
 172            ha1 = password
 173          end
 174 
 175          if auth_req['qop'] == "auth" || auth_req['qop'] == nil
 176            ha2 = hexdigest(req.request_method, auth_req['uri'])
 177            ha2_res = hexdigest("", auth_req['uri'])
 178          elsif auth_req['qop'] == "auth-int"
 179            ha2 = hexdigest(req.request_method, auth_req['uri'],
 180                            hexdigest(req.body))
 181            ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body))
 182          end
 183 
 184          if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
 185            param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key|
 186              auth_req[key]
 187            }.join(':')
 188            digest     = hexdigest(ha1, param2, ha2)
 189            digest_res = hexdigest(ha1, param2, ha2_res)
 190          else
 191            digest     = hexdigest(ha1, auth_req['nonce'], ha2)
 192            digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res)
 193          end
 194 
 195          if digest != auth_req['response']
 196            error("%s: digest unmatch.", auth_req['username'])
 197            return false
 198          elsif nonce_is_invalid
 199            error('%s: digest is valid, but nonce is not valid.',
 200                  auth_req['username'])
 201            return :nonce_is_stale
 202          elsif @use_auth_info_header
 203            auth_info = {
 204              'nextnonce' => generate_next_nonce(req),
 205              'rspauth'   => digest_res
 206            }
 207            if @use_opaque
 208              opaque_struct.time  = req.request_time
 209              opaque_struct.nonce = auth_info['nextnonce']
 210              opaque_struct.nc    = "%08x" % (auth_req['nc'].hex + 1)
 211            end
 212            if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
 213              ['qop','cnonce','nc'].each{|key|
 214                auth_info[key] = auth_req[key]
 215              }
 216            end
 217            res[@resp_info_field] = auth_info.keys.map{|key|
 218              if key == 'nc'
 219                key + '=' + auth_info[key]
 220              else
 221                key + "=" + HTTPUtils::quote(auth_info[key])
 222              end
 223            }.join(', ')
 224          end
 225          info('%s: authentication scceeded.', auth_req['username'])
 226          req.user = auth_req['username']
 227          return true
 228        end
 229 
 230        def split_param_value(string)
 231          ret = {}
 232          while string.size != 0
 233            case string           
 234            when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/
 235              key = $1
 236              matched = $2
 237              string = $'
 238              ret[key] = matched.gsub(/\\(.)/, "\\1")
 239            when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/
 240              key = $1
 241              matched = $2
 242              string = $'
 243              ret[key] = matched.clone
 244            when /^s*^,/
 245              string = $'
 246            else
 247              break
 248            end
 249          end
 250          ret
 251        end
 252 
 253        def generate_next_nonce(req)
 254          now = "%012d" % req.request_time.to_i
 255          pk  = hexdigest(now, @instance_key)[0,32]
 256          nonce = [now + ":" + pk].pack("m*").chop # it has 60 length of chars.
 257          nonce
 258        end
 259 
 260        def check_nonce(req, auth_req)
 261          username = auth_req['username']
 262          nonce = auth_req['nonce']
 263 
 264          pub_time, pk = nonce.unpack("m*")[0].split(":", 2)
 265          if (!pub_time || !pk)
 266            error("%s: empty nonce is given", username)
 267            return false
 268          elsif (hexdigest(pub_time, @instance_key)[0,32] != pk)
 269            error("%s: invalid private-key: %s for %s",
 270                  username, hexdigest(pub_time, @instance_key)[0,32], pk)
 271            return false
 272          end
 273 
 274          diff_time = req.request_time.to_i - pub_time.to_i
 275          if (diff_time < 0)
 276            error("%s: difference of time-stamp is negative.", username)
 277            return false
 278          elsif diff_time > @nonce_expire_period
 279            error("%s: nonce is expired.", username)
 280            return false
 281          end
 282 
 283          return true
 284        end
 285 
 286        def generate_opaque(req)
 287          @mutex.synchronize{
 288            now = req.request_time
 289            if now - @last_nonce_expire > @nonce_expire_delta
 290              @opaques.delete_if{|key,val|
 291                (now - val.time) > @nonce_expire_period
 292              }
 293              @last_nonce_expire = now
 294            end
 295            begin
 296              opaque = Utils::random_string(16)
 297            end while @opaques[opaque]
 298            @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001')
 299            opaque
 300          }
 301        end
 302 
 303        def check_opaque(opaque_struct, req, auth_req)
 304          if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce)
 305            error('%s: nonce unmatched. "%s" for "%s"',
 306                  auth_req['username'], auth_req['nonce'], opaque_struct.nonce)
 307            return false
 308          elsif !check_nonce(req, auth_req)
 309            return false
 310          end
 311          if (@check_nc && auth_req['nc'] != opaque_struct.nc)
 312            error('%s: nc unmatched."%s" for "%s"',
 313                  auth_req['username'], auth_req['nc'], opaque_struct.nc)
 314            return false
 315          end
 316          true
 317        end
 318 
 319        def check_uri(req, auth_req)
 320          uri = auth_req['uri']
 321          if uri != req.request_uri.to_s && uri != req.unparsed_uri &&
 322             (@internet_explorer_hack && uri != req.path)
 323            error('%s: uri unmatch. "%s" for "%s"', auth_req['username'], 
 324                  auth_req['uri'], req.request_uri.to_s)
 325            return false
 326          end
 327          true
 328        end
 329 
 330        def hexdigest(*args)
 331          @h.hexdigest(args.join(":"))
 332        end
 333      end
 334 
 335      class ProxyDigestAuth < DigestAuth
 336        include ProxyAuthenticator
 337 
 338        def check_uri(req, auth_req)
 339          return true
 340        end
 341      end
 342    end
 343  end