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 14require 'webrick/config' 15require 'webrick/httpstatus' 16require 'webrick/httpauth/authenticator' 17require 'digest/md5' 18require 'digest/sha1' 19 20module WEBrick 21 module HTTPAuth 22 23 ## 24 # RFC 2617 Digest Access Authentication for WEBrick 25 # 26 # Use this class to add digest authentication to a WEBrick servlet. 27 # 28 # Here is an example of how to set up DigestAuth: 29 # 30 # config = { :Realm => 'DigestAuth example realm' } 31 # 32 # htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' 33 # htdigest.set_passwd config[:Realm], 'username', 'password' 34 # htdigest.flush 35 # 36 # config[:UserDB] = htdigest 37 # 38 # digest_auth = WEBrick::HTTPAuth::DigestAuth.new config 39 # 40 # When using this as with a servlet be sure not to create a new DigestAuth 41 # object in the servlet's #initialize. By default WEBrick creates a new 42 # servlet instance for every request and the DigestAuth object must be 43 # used across requests. 44 45 class DigestAuth 46 include Authenticator 47 48 AuthScheme = "Digest" # :nodoc: 49 50 ## 51 # Struct containing the opaque portion of the digest authentication 52 53 OpaqueInfo = Struct.new(:time, :nonce, :nc) # :nodoc: 54 55 ## 56 # Digest authentication algorithm 57 58 attr_reader :algorithm 59 60 ## 61 # Quality of protection. RFC 2617 defines "auth" and "auth-int" 62 63 attr_reader :qop 64 65 ## 66 # Used by UserDB to create a digest password entry 67 68 def self.make_passwd(realm, user, pass) 69 pass ||= "" 70 Digest::MD5::hexdigest([user, realm, pass].join(":")) 71 end 72 73 ## 74 # Creates a new DigestAuth instance. Be sure to use the same DigestAuth 75 # instance for multiple requests as it saves state between requests in 76 # order to perform authentication. 77 # 78 # See WEBrick::Config::DigestAuth for default configuration entries 79 # 80 # You must supply the following configuration entries: 81 # 82 # :Realm:: The name of the realm being protected. 83 # :UserDB:: A database of usernames and passwords. 84 # A WEBrick::HTTPAuth::Htdigest instance should be used. 85 86 def initialize(config, default=Config::DigestAuth) 87 check_init(config) 88 @config = default.dup.update(config) 89 @algorithm = @config[:Algorithm] 90 @domain = @config[:Domain] 91 @qop = @config[:Qop] 92 @use_opaque = @config[:UseOpaque] 93 @use_next_nonce = @config[:UseNextNonce] 94 @check_nc = @config[:CheckNc] 95 @use_auth_info_header = @config[:UseAuthenticationInfoHeader] 96 @nonce_expire_period = @config[:NonceExpirePeriod] 97 @nonce_expire_delta = @config[:NonceExpireDelta] 98 @internet_explorer_hack = @config[:InternetExplorerHack] 99 100 case @algorithm 101 when 'MD5','MD5-sess' 102 @h = Digest::MD5 103 when 'SHA1','SHA1-sess' # it is a bonus feature :-) 104 @h = Digest::SHA1 105 else 106 msg = format('Algorithm "%s" is not supported.', @algorithm) 107 raise ArgumentError.new(msg) 108 end 109 110 @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid) 111 @opaques = {} 112 @last_nonce_expire = Time.now 113 @mutex = Mutex.new 114 end 115 116 ## 117 # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if 118 # the authentication was not correct. 119 120 def authenticate(req, res) 121 unless result = @mutex.synchronize{ _authenticate(req, res) } 122 challenge(req, res) 123 end 124 if result == :nonce_is_stale 125 challenge(req, res, true) 126 end 127 return true 128 end 129 130 ## 131 # Returns a challenge response which asks for for authentication 132 # information 133 134 def challenge(req, res, stale=false) 135 nonce = generate_next_nonce(req) 136 if @use_opaque 137 opaque = generate_opaque(req) 138 @opaques[opaque].nonce = nonce 139 end 140 141 param = Hash.new 142 param["realm"] = HTTPUtils::quote(@realm) 143 param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain 144 param["nonce"] = HTTPUtils::quote(nonce) 145 param["opaque"] = HTTPUtils::quote(opaque) if opaque 146 param["stale"] = stale.to_s 147 param["algorithm"] = @algorithm 148 param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop 149 150 res[@response_field] = 151 "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ") 152 info("%s: %s", @response_field, res[@response_field]) if $DEBUG 153 raise @auth_exception 154 end 155 156 private 157 158 # :stopdoc: 159 160 MustParams = ['username','realm','nonce','uri','response'] 161 MustParamsAuth = ['cnonce','nc'] 162 163 def _authenticate(req, res) 164 unless digest_credentials = check_scheme(req) 165 return false 166 end 167 168 auth_req = split_param_value(digest_credentials) 169 if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" 170 req_params = MustParams + MustParamsAuth 171 else 172 req_params = MustParams 173 end 174 req_params.each{|key| 175 unless auth_req.has_key?(key) 176 error('%s: parameter missing. "%s"', auth_req['username'], key) 177 raise HTTPStatus::BadRequest 178 end 179 } 180 181 if !check_uri(req, auth_req) 182 raise HTTPStatus::BadRequest 183 end 184 185 if auth_req['realm'] != @realm 186 error('%s: realm unmatch. "%s" for "%s"', 187 auth_req['username'], auth_req['realm'], @realm) 188 return false 189 end 190 191 auth_req['algorithm'] ||= 'MD5' 192 if auth_req['algorithm'].upcase != @algorithm.upcase 193 error('%s: algorithm unmatch. "%s" for "%s"', 194 auth_req['username'], auth_req['algorithm'], @algorithm) 195 return false 196 end 197 198 if (@qop.nil? && auth_req.has_key?('qop')) || 199 (@qop && (! @qop.member?(auth_req['qop']))) 200 error('%s: the qop is not allowed. "%s"', 201 auth_req['username'], auth_req['qop']) 202 return false 203 end 204 205 password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db) 206 unless password 207 error('%s: the user is not allowd.', auth_req['username']) 208 return false 209 end 210 211 nonce_is_invalid = false 212 if @use_opaque 213 info("@opaque = %s", @opaque.inspect) if $DEBUG 214 if !(opaque = auth_req['opaque']) 215 error('%s: opaque is not given.', auth_req['username']) 216 nonce_is_invalid = true 217 elsif !(opaque_struct = @opaques[opaque]) 218 error('%s: invalid opaque is given.', auth_req['username']) 219 nonce_is_invalid = true 220 elsif !check_opaque(opaque_struct, req, auth_req) 221 @opaques.delete(auth_req['opaque']) 222 nonce_is_invalid = true 223 end 224 elsif !check_nonce(req, auth_req) 225 nonce_is_invalid = true 226 end 227 228 if /-sess$/i =~ auth_req['algorithm'] 229 ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce']) 230 else 231 ha1 = password 232 end 233 234 if auth_req['qop'] == "auth" || auth_req['qop'] == nil 235 ha2 = hexdigest(req.request_method, auth_req['uri']) 236 ha2_res = hexdigest("", auth_req['uri']) 237 elsif auth_req['qop'] == "auth-int" 238 ha2 = hexdigest(req.request_method, auth_req['uri'], 239 hexdigest(req.body)) 240 ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body)) 241 end 242 243 if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" 244 param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key| 245 auth_req[key] 246 }.join(':') 247 digest = hexdigest(ha1, param2, ha2) 248 digest_res = hexdigest(ha1, param2, ha2_res) 249 else 250 digest = hexdigest(ha1, auth_req['nonce'], ha2) 251 digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res) 252 end 253 254 if digest != auth_req['response'] 255 error("%s: digest unmatch.", auth_req['username']) 256 return false 257 elsif nonce_is_invalid 258 error('%s: digest is valid, but nonce is not valid.', 259 auth_req['username']) 260 return :nonce_is_stale 261 elsif @use_auth_info_header 262 auth_info = { 263 'nextnonce' => generate_next_nonce(req), 264 'rspauth' => digest_res 265 } 266 if @use_opaque 267 opaque_struct.time = req.request_time 268 opaque_struct.nonce = auth_info['nextnonce'] 269 opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1) 270 end 271 if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" 272 ['qop','cnonce','nc'].each{|key| 273 auth_info[key] = auth_req[key] 274 } 275 end 276 res[@resp_info_field] = auth_info.keys.map{|key| 277 if key == 'nc' 278 key + '=' + auth_info[key] 279 else 280 key + "=" + HTTPUtils::quote(auth_info[key]) 281 end 282 }.join(', ') 283 end 284 info('%s: authentication succeeded.', auth_req['username']) 285 req.user = auth_req['username'] 286 return true 287 end 288 289 def split_param_value(string) 290 ret = {} 291 while string.bytesize != 0 292 case string 293 when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/ 294 key = $1 295 matched = $2 296 string = $' 297 ret[key] = matched.gsub(/\\(.)/, "\\1") 298 when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/ 299 key = $1 300 matched = $2 301 string = $' 302 ret[key] = matched.clone 303 when /^s*^,/ 304 string = $' 305 else 306 break 307 end 308 end 309 ret 310 end 311 312 def generate_next_nonce(req) 313 now = "%012d" % req.request_time.to_i 314 pk = hexdigest(now, @instance_key)[0,32] 315 nonce = [now + ":" + pk].pack("m*").chop # it has 60 length of chars. 316 nonce 317 end 318 319 def check_nonce(req, auth_req) 320 username = auth_req['username'] 321 nonce = auth_req['nonce'] 322 323 pub_time, pk = nonce.unpack("m*")[0].split(":", 2) 324 if (!pub_time || !pk) 325 error("%s: empty nonce is given", username) 326 return false 327 elsif (hexdigest(pub_time, @instance_key)[0,32] != pk) 328 error("%s: invalid private-key: %s for %s", 329 username, hexdigest(pub_time, @instance_key)[0,32], pk) 330 return false 331 end 332 333 diff_time = req.request_time.to_i - pub_time.to_i 334 if (diff_time < 0) 335 error("%s: difference of time-stamp is negative.", username) 336 return false 337 elsif diff_time > @nonce_expire_period 338 error("%s: nonce is expired.", username) 339 return false 340 end 341 342 return true 343 end 344 345 def generate_opaque(req) 346 @mutex.synchronize{ 347 now = req.request_time 348 if now - @last_nonce_expire > @nonce_expire_delta 349 @opaques.delete_if{|key,val| 350 (now - val.time) > @nonce_expire_period 351 } 352 @last_nonce_expire = now 353 end 354 begin 355 opaque = Utils::random_string(16) 356 end while @opaques[opaque] 357 @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001') 358 opaque 359 } 360 end 361 362 def check_opaque(opaque_struct, req, auth_req) 363 if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce) 364 error('%s: nonce unmatched. "%s" for "%s"', 365 auth_req['username'], auth_req['nonce'], opaque_struct.nonce) 366 return false 367 elsif !check_nonce(req, auth_req) 368 return false 369 end 370 if (@check_nc && auth_req['nc'] != opaque_struct.nc) 371 error('%s: nc unmatched."%s" for "%s"', 372 auth_req['username'], auth_req['nc'], opaque_struct.nc) 373 return false 374 end 375 true 376 end 377 378 def check_uri(req, auth_req) 379 uri = auth_req['uri'] 380 if uri != req.request_uri.to_s && uri != req.unparsed_uri && 381 (@internet_explorer_hack && uri != req.path) 382 error('%s: uri unmatch. "%s" for "%s"', auth_req['username'], 383 auth_req['uri'], req.request_uri.to_s) 384 return false 385 end 386 true 387 end 388 389 def hexdigest(*args) 390 @h.hexdigest(args.join(":")) 391 end 392 393 # :startdoc: 394 end 395 396 ## 397 # Digest authentication for proxy servers. See DigestAuth for details. 398 399 class ProxyDigestAuth < DigestAuth 400 include ProxyAuthenticator 401 402 private 403 def check_uri(req, auth_req) # :nodoc: 404 return true 405 end 406 end 407 end 408end 409